synheart_behavior 0.0.1 copy "synheart_behavior: ^0.0.1" to clipboard
synheart_behavior: ^0.0.1 copied to clipboard

A lightweight, privacy-preserving mobile SDK that collects digital behavioral signals from smartphones.

example/lib/main.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:synheart_behavior/synheart_behavior.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Synheart Behavior Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const BehaviorDemoPage(),
    );
  }
}

class BehaviorDemoPage extends StatefulWidget {
  const BehaviorDemoPage({super.key});

  @override
  State<BehaviorDemoPage> createState() => _BehaviorDemoPageState();
}

class _BehaviorDemoPageState extends State<BehaviorDemoPage>
    with WidgetsBindingObserver {
  // Constants
  static const int _maxEventsToKeep = 50;
  static const int _eventRetentionMinutes = 5;
  static const int _backgroundSessionTimeoutMinutes = 1; // 1 minute

  SynheartBehavior? _behavior;
  BehaviorSession? _currentSession;
  BehaviorStats? _currentStats;
  List<BehaviorEvent> _events = [];
  List<BehaviorEvent> _sessionEvents =
      []; // Events collected during current session
  bool _isInitialized = false;
  bool _isSessionActive = false;
  bool _sessionAutoEnded = false; // Track if session was auto-ended
  BehaviorSessionSummary? _autoEndedSummary; // Store summary if auto-ended
  List<BehaviorEvent> _autoEndedEvents = []; // Store events if auto-ended
  Timer? _backgroundTimer;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _initializeSDK();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);

    if (state == AppLifecycleState.paused ||
        state == AppLifecycleState.inactive) {
      // App went to background
      _onAppBackgrounded();
    } else if (state == AppLifecycleState.resumed) {
      // App returned to foreground
      _onAppForegrounded();
    }
  }

  void _onAppBackgrounded() {
    if (!_isSessionActive || _currentSession == null) return;

    print(
        'App went to background. Starting ${_backgroundSessionTimeoutMinutes} minute timer...');

    // Cancel any existing timer
    _backgroundTimer?.cancel();

    // Start timer to auto-end session after 1 minute
    _backgroundTimer = Timer(
      Duration(minutes: _backgroundSessionTimeoutMinutes),
      () {
        if (_isSessionActive && _currentSession != null) {
          print('Background timeout reached. Auto-ending session...');
          _endSession(autoEnded: true);
        }
      },
    );
  }

  void _onAppForegrounded() {
    // Cancel the timer if app returned before timeout
    _backgroundTimer?.cancel();

    // If session was auto-ended while in background, show results
    if (_sessionAutoEnded && _autoEndedSummary != null && mounted) {
      print(
          'App returned to foreground. Session was auto-ended. Showing results...');

      // Store values in local variables to avoid null issues in callback
      final summary = _autoEndedSummary!;
      final events = List<BehaviorEvent>.from(_autoEndedEvents);

      // Reset auto-ended flags immediately
      _sessionAutoEnded = false;
      _autoEndedSummary = null;
      _autoEndedEvents = [];

      // Delay slightly to ensure widget is fully mounted
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (mounted && context.mounted) {
          Navigator.of(context).push(
            MaterialPageRoute(
              builder: (context) => SessionResultsScreen(
                summary: summary,
                events: events,
              ),
            ),
          );
        }
      });
    }
  }

  Future<void> _initializeSDK() async {
    try {
      final behavior = await SynheartBehavior.initialize(
        config: const BehaviorConfig(
          enableInputSignals: true,
          enableAttentionSignals: true,
          enableMotionLite: false,
        ),
      );

      // Listen to raw events
      behavior.onEvent.listen((event) {
        setState(() {
          _events.insert(0, event);
          // Keep only recent events within retention period
          final now = DateTime.now().millisecondsSinceEpoch;
          final cutoffTime = now - (_eventRetentionMinutes * 60 * 1000);
          _events = _events
              .where((e) {
                try {
                  final eventTime =
                      DateTime.parse(e.timestamp).millisecondsSinceEpoch;
                  return eventTime >= cutoffTime;
                } catch (e) {
                  return false; // Remove events with invalid timestamps
                }
              })
              .take(_maxEventsToKeep)
              .toList();

          // Store events for current session (if session is active)
          if (_isSessionActive && _currentSession != null) {
            if (event.sessionId == _currentSession!.sessionId) {
              _sessionEvents.add(event);
            }
          }
        });
      });

      // Check and request notification permission
      await _checkAndRequestNotificationPermission(behavior);

      // Auto-start a session for testing
      final session = await behavior.startSession();

      setState(() {
        _behavior = behavior;
        _currentSession = session;
        _isInitialized = true;
        _isSessionActive = true;
      });
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Failed to initialize SDK: $e')),
        );
      }
    }
  }

  Future<void> _startSession() async {
    if (_behavior == null) return;

    try {
      final session = await _behavior!.startSession();
      setState(() {
        _currentSession = session;
        _isSessionActive = true;
        _sessionEvents = []; // Clear previous session events
      });
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Failed to start session: $e')),
        );
      }
    }
  }

  Future<void> _endSession({bool autoEnded = false}) async {
    print('_endSession called (autoEnded: $autoEnded)');
    print('_currentSession: $_currentSession');
    print('_isSessionActive: $_isSessionActive');

    if (_currentSession == null) {
      print('ERROR: _currentSession is null!');
      if (mounted && !autoEnded) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('No active session to end')),
        );
      }
      return;
    }

    // Cancel background timer if it exists
    _backgroundTimer?.cancel();
    _backgroundTimer = null;

    try {
      print('Calling _currentSession!.end()...');
      print('Session ID being ended: ${_currentSession!.sessionId}');
      final summary = await _currentSession!.end().timeout(
        const Duration(seconds: 15),
        onTimeout: () {
          throw Exception('Session end timed out after 15 seconds');
        },
      );
      print('Session ended successfully. Summary: ${summary.sessionId}');
      final sessionEvents = List<BehaviorEvent>.from(_sessionEvents);
      print('Session events count: ${sessionEvents.length}');

      setState(() {
        _currentSession = null;
        _isSessionActive = false;
      });

      if (autoEnded) {
        // Store summary and events to show when app returns to foreground
        _sessionAutoEnded = true;
        _autoEndedSummary = summary;
        _autoEndedEvents = sessionEvents;
        print(
            'Session auto-ended. Will show results when app returns to foreground.');
      } else if (mounted) {
        print('Navigating to SessionResultsScreen...');
        // Navigate to session results screen immediately
        Navigator.of(context).push(
          MaterialPageRoute(
            builder: (context) => SessionResultsScreen(
              summary: summary,
              events: sessionEvents,
            ),
          ),
        );
      }
    } catch (e, stackTrace) {
      print('ERROR ending session: $e');
      print('Stack trace: $stackTrace');
      if (mounted && !autoEnded) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Failed to end session: $e')),
        );
      }
    }
  }

  Future<void> _refreshStats() async {
    if (_behavior == null) return;

    try {
      final stats = await _behavior!.getCurrentStats();
      setState(() {
        _currentStats = stats;
      });
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Failed to get stats: $e')),
        );
      }
    }
  }

  Future<void> _checkAndRequestNotificationPermission(
      [SynheartBehavior? behavior]) async {
    final sdk = behavior ?? _behavior;
    if (sdk == null) return;

    try {
      final hasPermission = await sdk.checkNotificationPermission();
      if (!hasPermission) {
        if (mounted) {
          final shouldRequest = await showDialog<bool>(
            context: context,
            builder: (context) => AlertDialog(
              title: const Text('Notification Permission'),
              content: const Text(
                'To track notification events, please enable notification access.\n\n'
                'On Android: You will be taken to system settings to enable notification access.\n'
                'On iOS: A permission dialog will appear.',
              ),
              actions: [
                TextButton(
                  onPressed: () => Navigator.of(context).pop(false),
                  child: const Text('Cancel'),
                ),
                TextButton(
                  onPressed: () => Navigator.of(context).pop(true),
                  child: const Text('Enable'),
                ),
              ],
            ),
          );

          if (shouldRequest == true) {
            await sdk.requestNotificationPermission();
            if (mounted) {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(
                  content: Text(
                    'Please enable notification access in settings if prompted.',
                  ),
                ),
              );
            }
          }
        }
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
              content: Text('Failed to check notification permission: $e')),
        );
      }
    }
  }

  Future<void> _checkAndRequestCallPermission(
      [SynheartBehavior? behavior]) async {
    final sdk = behavior ?? _behavior;
    if (sdk == null) return;

    try {
      final hasPermission = await sdk.checkCallPermission();
      if (!hasPermission) {
        if (mounted) {
          final shouldRequest = await showDialog<bool>(
            context: context,
            builder: (context) => AlertDialog(
              title: const Text('Call Permission'),
              content: const Text(
                'To track call events, please enable phone state access.\n\n'
                'On Android: A permission dialog will appear.\n'
                'On iOS: Call monitoring works automatically.',
              ),
              actions: [
                TextButton(
                  onPressed: () => Navigator.of(context).pop(false),
                  child: const Text('Cancel'),
                ),
                TextButton(
                  onPressed: () => Navigator.of(context).pop(true),
                  child: const Text('Enable'),
                ),
              ],
            ),
          );

          if (shouldRequest == true) {
            await sdk.requestCallPermission();
            if (mounted) {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(
                  content: Text(
                    'Please grant phone state permission if prompted.',
                  ),
                ),
              );
            }
          }
        }
      } else {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('Call permission already granted.'),
            ),
          );
        }
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Failed to check call permission: $e')),
        );
      }
    }
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _backgroundTimer?.cancel();
    _behavior?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    Widget content = BehaviorGestureDetector(
      behavior: _behavior,
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: const Text('Synheart Behavior Demo'),
        ),
        body: SingleChildScrollView(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              // Status Card
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'SDK Status',
                        style: Theme.of(context).textTheme.titleLarge,
                      ),
                      const SizedBox(height: 8),
                      Row(
                        children: [
                          Icon(
                            _isInitialized ? Icons.check_circle : Icons.error,
                            color: _isInitialized ? Colors.green : Colors.red,
                          ),
                          const SizedBox(width: 8),
                          Text(
                            _isInitialized ? 'Initialized' : 'Not Initialized',
                          ),
                        ],
                      ),
                      if (_isSessionActive) ...[
                        const SizedBox(height: 8),
                        Row(
                          children: [
                            const Icon(
                              Icons.play_circle,
                              color: Colors.blue,
                            ),
                            const SizedBox(width: 8),
                            Text(
                                'Session Active: ${_currentSession?.sessionId}'),
                          ],
                        ),
                      ],
                    ],
                  ),
                ),
              ),

              const SizedBox(height: 16),

              // Controls
              Row(
                children: [
                  Expanded(
                    child: ElevatedButton(
                      onPressed: _isInitialized && !_isSessionActive
                          ? () {
                              print('Start Session button clicked!');
                              _startSession();
                            }
                          : null,
                      child: const Text('Start Session'),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: Builder(
                      builder: (context) => ElevatedButton(
                        onPressed: _isInitialized && _isSessionActive
                            ? () {
                                print('=== END SESSION BUTTON CLICKED ===');
                                print('_isInitialized: $_isInitialized');
                                print('_isSessionActive: $_isSessionActive');
                                print('_currentSession: $_currentSession');
                                print(
                                    '_currentSession?.sessionId: ${_currentSession?.sessionId}');
                                _endSession();
                              }
                            : () {
                                print(
                                    '=== END SESSION BUTTON CLICKED (DISABLED) ===');
                                print('_isInitialized: $_isInitialized');
                                print('_isSessionActive: $_isSessionActive');
                                ScaffoldMessenger.of(context).showSnackBar(
                                  SnackBar(
                                    content: Text(
                                      'Button disabled: initialized=$_isInitialized, active=$_isSessionActive',
                                    ),
                                  ),
                                );
                              },
                        style: ElevatedButton.styleFrom(
                          backgroundColor: _isInitialized && _isSessionActive
                              ? Colors.red
                              : Colors.grey,
                          foregroundColor: Colors.white,
                        ),
                        child: Text(
                          _isInitialized && _isSessionActive
                              ? 'End Session'
                              : 'End Session (Disabled)',
                        ),
                      ),
                    ),
                  ),
                ],
              ),

              const SizedBox(height: 8),

              ElevatedButton(
                onPressed: _isInitialized ? _refreshStats : null,
                child: const Text('Refresh Stats'),
              ),

              const SizedBox(height: 8),

              ElevatedButton(
                onPressed: _isInitialized && _behavior != null
                    ? () => _checkAndRequestNotificationPermission(_behavior!)
                    : null,
                child: const Text('Request Notification Permission'),
              ),

              const SizedBox(height: 8),

              ElevatedButton(
                onPressed: _isInitialized && _behavior != null
                    ? () => _checkAndRequestCallPermission(_behavior!)
                    : null,
                child: const Text('Request Call Permission'),
              ),

              const SizedBox(height: 16),
              if (!_isSessionActive) ...[
                // Stats Card
                if (_currentStats != null)
                  Card(
                    child: Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            'Current Stats',
                            style: Theme.of(context).textTheme.titleLarge,
                          ),
                          const SizedBox(height: 8),
                          // _buildStatRow('Typing Cadence',
                          //     _currentStats!.typingCadence?.toStringAsFixed(2)),
                          _buildStatRow(
                              'Scroll Velocity',
                              _currentStats!.scrollVelocity
                                  ?.toStringAsFixed(2)),
                          _buildStatRow('App Switches/min',
                              _currentStats!.appSwitchesPerMinute.toString()),
                          _buildStatRow(
                              'Stability Index',
                              _currentStats!.stabilityIndex
                                  ?.toStringAsFixed(2)),
                        ],
                      ),
                    ),
                  ),

                const SizedBox(height: 16),

                const SizedBox(height: 16),
              ],

              // Interactive Test Area - Always visible, especially during session
              const SizedBox(height: 16),
              // Test list - removed for now to avoid blocking interactions
              ListView.builder(
                shrinkWrap: true,
                physics: const NeverScrollableScrollPhysics(),
                itemCount: 10,
                itemBuilder: (context, index) {
                  return Card(
                      child: Padding(
                          padding: const EdgeInsets.all(16.0),
                          child: Text('Item $index')));
                },
              ),
              const SizedBox(height: 100),
              const Text('Scroll down to see more content'),
            ],
          ),
        ),
      ),
    );

    // BehaviorGestureDetector already wraps the Scaffold above
    return content;
  }

  Widget _buildStatRow(String label, String? value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label),
          Text(
            value ?? 'N/A',
            style: const TextStyle(fontWeight: FontWeight.bold),
          ),
        ],
      ),
    );
  }

  // Color _getEventTypeColor(BehaviorEventType eventType) {
  //   switch (eventType) {
  //     case BehaviorEventType.scroll:
  //       return Colors.blue;
  //     case BehaviorEventType.tap:
  //       return Colors.green;
  //     case BehaviorEventType.swipe:
  //       return Colors.orange;
  //     case BehaviorEventType.call:
  //       return Colors.red;
  //     case BehaviorEventType.notification:
  //       return Colors.purple;
  //   }
  // }
}

/// Screen to display session results with events timeline and behavior metrics
class SessionResultsScreen extends StatelessWidget {
  final BehaviorSessionSummary summary;
  final List<BehaviorEvent> events;

  const SessionResultsScreen({
    super.key,
    required this.summary,
    required this.events,
  });

  @override
  Widget build(BuildContext context) {
    // Sort events by timestamp (oldest first)
    final sortedEvents = List<BehaviorEvent>.from(events)
      ..sort((a, b) {
        try {
          final timeA = DateTime.parse(a.timestamp);
          final timeB = DateTime.parse(b.timestamp);
          return timeA.compareTo(timeB);
        } catch (e) {
          return 0;
        }
      });

    // Calculate relative time from session start
    final sessionStart = DateTime.parse(summary.startAt);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Session Results'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Session Info Card
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Session Information',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                    const SizedBox(height: 12),
                    _buildInfoRow('Session ID', summary.sessionId),
                    _buildInfoRow(
                        'Start Time', _formatDateTime(summary.startAt)),
                    _buildInfoRow('End Time', _formatDateTime(summary.endAt)),
                    _buildInfoRow('Duration', _formatMs(summary.durationMs)),
                    _buildInfoRow(
                        'Micro Session', summary.microSession ? 'Yes' : 'No'),
                    _buildInfoRow('OS', summary.os),
                    if (summary.appId != null)
                      _buildInfoRow('App ID', summary.appId!),
                    if (summary.appName != null)
                      _buildInfoRow('App Name', summary.appName!),
                    _buildInfoRow(
                        'Session Spacing', _formatMs(summary.sessionSpacing)),
                    _buildInfoRow('Total Events', '${sortedEvents.length}'),
                  ],
                ),
              ),
            ),

            const SizedBox(height: 16),

            // Motion State Card
            if (summary.motionState != null) ...[
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Motion State',
                        style: Theme.of(context).textTheme.titleLarge,
                      ),
                      const SizedBox(height: 12),
                      _buildInfoRow('State', summary.motionState!.state),
                      _buildInfoRow('ML Model', summary.motionState!.mlModel),
                      _buildInfoRow('Confidence',
                          summary.motionState!.confidence.toStringAsFixed(3)),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 16),
            ],

            // Device Context Card
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Device Context',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                    const SizedBox(height: 12),
                    _buildInfoRow(
                        'Avg Screen Brightness',
                        summary.deviceContext.avgScreenBrightness
                            .toStringAsFixed(3)),
                    _buildInfoRow('Start Orientation',
                        summary.deviceContext.startOrientation),
                    _buildInfoRow('Orientation Changes',
                        '${summary.deviceContext.orientationChanges}'),
                  ],
                ),
              ),
            ),

            const SizedBox(height: 16),

            // Activity Summary Card
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Activity Summary',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                    const SizedBox(height: 12),
                    _buildInfoRow('Total Events',
                        '${summary.activitySummary.totalEvents}'),
                    _buildInfoRow('App Switch Count',
                        '${summary.activitySummary.appSwitchCount}'),
                  ],
                ),
              ),
            ),

            const SizedBox(height: 16),

            // Behavior Metrics Card
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Behavior Metrics',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                    const SizedBox(height: 12),
                    _buildInfoRow(
                        'Interaction Intensity',
                        summary.behavioralMetrics.interactionIntensity
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Task Switch Rate',
                        summary.behavioralMetrics.taskSwitchRate
                            .toStringAsFixed(3)),
                    _buildInfoRow('Task Switch Cost',
                        _formatMs(summary.behavioralMetrics.taskSwitchCost)),
                    _buildInfoRow(
                        'Idle Time Ratio',
                        summary.behavioralMetrics.idleTimeRatio
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Active Time Ratio',
                        summary.behavioralMetrics.activeTimeRatio
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Notification Load',
                        summary.behavioralMetrics.notificationLoad
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Burstiness',
                        summary.behavioralMetrics.burstiness
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Distraction Score',
                        summary.behavioralMetrics.behavioralDistractionScore
                            .toStringAsFixed(3)),
                    _buildInfoRow('Focus Hint',
                        summary.behavioralMetrics.focusHint.toStringAsFixed(3)),
                    _buildInfoRow(
                        'Fragmented Idle Ratio',
                        summary.behavioralMetrics.fragmentedIdleRatio
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Scroll Jitter Rate',
                        summary.behavioralMetrics.scrollJitterRate
                            .toStringAsFixed(3)),
                    _buildInfoRow('Deep Focus Blocks',
                        '${summary.behavioralMetrics.deepFocusBlocks.length}'),
                    if (summary
                        .behavioralMetrics.deepFocusBlocks.isNotEmpty) ...[
                      const SizedBox(height: 8),
                      const Text(
                        'Deep Focus Block Details:',
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 14,
                        ),
                      ),
                      const SizedBox(height: 4),
                      ...summary.behavioralMetrics.deepFocusBlocks
                          .asMap()
                          .entries
                          .map((entry) {
                        final index = entry.key;
                        final block = entry.value;
                        return Padding(
                          padding: const EdgeInsets.only(left: 8, top: 4),
                          child: Text(
                            'Block ${index + 1}: ${_formatDateTime(block.startAt)} - ${_formatDateTime(block.endAt)} (${_formatMs(block.durationMs)})',
                            style: Theme.of(context).textTheme.bodySmall,
                          ),
                        );
                      }),
                    ],
                  ],
                ),
              ),
            ),

            const SizedBox(height: 16),

            // Notification Summary Card
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Notification Summary',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                    const SizedBox(height: 12),
                    _buildInfoRow('Notification Count',
                        '${summary.notificationSummary.notificationCount}'),
                    _buildInfoRow('Notifications Ignored',
                        '${summary.notificationSummary.notificationIgnored}'),
                    _buildInfoRow(
                        'Ignore Rate',
                        summary.notificationSummary.notificationIgnoreRate
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Clustering Index',
                        summary.notificationSummary.notificationClusteringIndex
                            .toStringAsFixed(3)),
                    _buildInfoRow('Call Count',
                        '${summary.notificationSummary.callCount}'),
                    _buildInfoRow('Calls Ignored',
                        '${summary.notificationSummary.callIgnored}'),
                  ],
                ),
              ),
            ),

            const SizedBox(height: 16),

            // System State Card
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'System State',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                    const SizedBox(height: 12),
                    _buildInfoRow(
                        'Internet',
                        summary.systemState.internetState
                            ? 'Connected'
                            : 'Disconnected'),
                    _buildInfoRow('Do Not Disturb',
                        summary.systemState.doNotDisturb ? 'On' : 'Off'),
                    _buildInfoRow('Charging',
                        summary.systemState.charging ? 'Yes' : 'No'),
                  ],
                ),
              ),
            ),

            const SizedBox(height: 16),

            // Events Timeline
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Events Timeline (${sortedEvents.length} events)',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                    const SizedBox(height: 12),
                    if (sortedEvents.isEmpty)
                      const Padding(
                        padding: EdgeInsets.all(16.0),
                        child: Center(
                          child:
                              Text('No events collected during this session.'),
                        ),
                      )
                    else
                      ...sortedEvents.asMap().entries.map((entry) {
                        final index = entry.key;
                        final event = entry.value;
                        final eventTime = DateTime.parse(event.timestamp);
                        final relativeTime = eventTime.difference(sessionStart);
                        final relativeTimeMs = relativeTime.inMilliseconds;

                        return _buildEventTimelineItem(
                          context,
                          event,
                          index + 1,
                          relativeTimeMs,
                        );
                      }),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            label,
            style: const TextStyle(fontWeight: FontWeight.w500),
          ),
          Text(
            value,
            style: const TextStyle(fontWeight: FontWeight.bold),
          ),
        ],
      ),
    );
  }

  Widget _buildEventTimelineItem(
    BuildContext context,
    BehaviorEvent event,
    int eventNumber,
    int relativeTimeMs,
  ) {
    return Container(
      margin: const EdgeInsets.only(bottom: 12.0),
      padding: const EdgeInsets.all(12.0),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.grey[300]!),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Container(
                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                decoration: BoxDecoration(
                  color: _getEventTypeColor(event.eventType),
                  borderRadius: BorderRadius.circular(4),
                ),
                child: Text(
                  event.eventType.name.toUpperCase(),
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 12,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
              Text(
                '+${_formatMs(relativeTimeMs)}',
                style: Theme.of(context).textTheme.bodySmall?.copyWith(
                      fontFamily: 'monospace',
                      fontWeight: FontWeight.bold,
                    ),
              ),
            ],
          ),
          const SizedBox(height: 8),
          Text(
            'Time: ${_formatDateTime(event.timestamp)}',
            style: Theme.of(context).textTheme.bodySmall,
          ),
          const SizedBox(height: 8),
          const Text(
            'Metrics:',
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 12,
            ),
          ),
          const SizedBox(height: 4),
          ...event.metrics.entries.map((entry) => Padding(
                padding: const EdgeInsets.only(left: 8, top: 2),
                child: Row(
                  children: [
                    Text(
                      '${entry.key}: ',
                      style: Theme.of(context).textTheme.bodySmall?.copyWith(
                            fontWeight: FontWeight.w500,
                            fontFamily: 'monospace',
                          ),
                    ),
                    Text(
                      entry.value.toString(),
                      style: Theme.of(context).textTheme.bodySmall?.copyWith(
                            fontFamily: 'monospace',
                          ),
                    ),
                  ],
                ),
              )),
        ],
      ),
    );
  }

  Color _getEventTypeColor(BehaviorEventType eventType) {
    switch (eventType) {
      case BehaviorEventType.scroll:
        return Colors.blue;
      case BehaviorEventType.tap:
        return Colors.green;
      case BehaviorEventType.swipe:
        return Colors.orange;
      case BehaviorEventType.call:
        return Colors.red;
      case BehaviorEventType.notification:
        return Colors.purple;
    }
  }

  String _formatDateTime(String isoString) {
    try {
      final dateTime = DateTime.parse(isoString);
      return '${dateTime.hour.toString().padLeft(2, '0')}:'
          '${dateTime.minute.toString().padLeft(2, '0')}:'
          '${dateTime.second.toString().padLeft(2, '0')}.'
          '${(dateTime.millisecond ~/ 100).toString()}';
    } catch (e) {
      return isoString;
    }
  }

  String _formatMs(int milliseconds) {
    if (milliseconds < 1000) {
      return '${milliseconds}ms';
    } else if (milliseconds < 60000) {
      return '${(milliseconds / 1000).toStringAsFixed(1)}s';
    } else {
      return '${(milliseconds / 60000).toStringAsFixed(1)}m';
    }
  }
}
0
likes
140
points
39
downloads

Publisher

verified publishersynheart.ai

Weekly Downloads

A lightweight, privacy-preserving mobile SDK that collects digital behavioral signals from smartphones.

Repository (GitHub)
View/report issues

Topics

#behavior #digital-phenotyping #mobile-sensing #flutter #privacy

Documentation

API reference

License

unknown (license)

Dependencies

flutter

More

Packages that depend on synheart_behavior

Packages that implement synheart_behavior