synheart_behavior 0.1.3 copy "synheart_behavior: ^0.1.3" to clipboard
synheart_behavior: ^0.1.3 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() {
    // End any active typing sessions when app goes to background
    BehaviorTextField.endAllTypingSessions();

    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(
      const 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,
                behavior: _behavior,
              ),
            ),
          );
        }
      });
    }
  }

  Future<void> _initializeSDK() async {
    try {
      final behavior = await SynheartBehavior.initialize(
        config: const BehaviorConfig(
          enableInputSignals: true,
          enableAttentionSignals: true,
          enableMotionLite: true, // Enable motion data collection for ML
        ),
      );

      // 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');

    // End any active typing sessions (keyboard might still be open)
    BehaviorTextField.endAllTypingSessions();

    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,
              behavior: _behavior,
            ),
          ),
        );
      }
    } 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: GestureDetector(
          // Dismiss keyboard when tapping outside text fields
          onTap: () {
            FocusScope.of(context).unfocus();
          },
          child: 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),
                ],

                // Typing Test Field
                Card(
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          'Typing Test',
                          style: Theme.of(context).textTheme.titleMedium,
                        ),
                        const SizedBox(height: 8),
                        BehaviorTextField(
                          decoration: const InputDecoration(
                            hintText: 'Type here to test typing events...',
                            border: OutlineInputBorder(),
                            labelText: 'Text Input',
                          ),
                          maxLines: 3,
                          onTypingEvent: (event) async {
                            // Send typing event to SDK
                            if (_behavior != null) {
                              await _behavior!.sendEvent(event);
                            }
                          },
                        ),
                        const SizedBox(height: 8),
                        Text(
                          'Typing events will appear in the event stream above',
                          style:
                              Theme.of(context).textTheme.bodySmall?.copyWith(
                                    color: Colors.grey[600],
                                  ),
                        ),
                      ],
                    ),
                  ),
                ),

                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 StatefulWidget {
  final BehaviorSessionSummary summary;
  final List<BehaviorEvent> events;
  final SynheartBehavior? behavior;

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

  @override
  State<SessionResultsScreen> createState() => _SessionResultsScreenState();
}

class _SessionResultsScreenState extends State<SessionResultsScreen> {
  DateTime? _selectedStartTime;
  DateTime? _selectedEndTime;

  @override
  void initState() {
    super.initState();
    // Initialize time range to session start/end
    final sessionStartUtc = DateTime.parse(widget.summary.startAt);
    final sessionEndUtc = DateTime.parse(widget.summary.endAt);
    _selectedStartTime = sessionStartUtc;
    _selectedEndTime = sessionEndUtc;
  }

  @override
  Widget build(BuildContext context) {
    // Sort events by timestamp (oldest first)
    final sortedEvents = List<BehaviorEvent>.from(widget.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(widget.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', widget.summary.sessionId),
                    _buildInfoRow(
                        'Start Time', _formatDateTime(widget.summary.startAt)),
                    _buildInfoRow(
                        'End Time', _formatDateTime(widget.summary.endAt)),
                    _buildInfoRow(
                        'Duration', _formatMs(widget.summary.durationMs)),
                    _buildInfoRow('Micro Session',
                        widget.summary.microSession ? 'Yes' : 'No'),
                    _buildInfoRow('OS', widget.summary.os),
                    if (widget.summary.appId != null)
                      _buildInfoRow('App ID', widget.summary.appId!),
                    if (widget.summary.appName != null)
                      _buildInfoRow('App Name', widget.summary.appName!),
                    _buildInfoRow('Session Spacing',
                        _formatMs(widget.summary.sessionSpacing)),
                    _buildInfoRow('Total Events', '${sortedEvents.length}'),
                    const SizedBox(height: 16),
                    const Divider(),
                    const SizedBox(height: 12),
                    // Time Range Picker Section
                    Text(
                      'Time Range Selection',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 12),
                    // Start Time Picker
                    Row(
                      children: [
                        Expanded(
                          child: ElevatedButton.icon(
                            onPressed: () => _pickStartTime(context),
                            icon: const Icon(Icons.access_time),
                            label: Text(
                              _selectedStartTime != null
                                  ? 'Start: ${_formatDateTimeWithSeconds(_selectedStartTime!.toLocal())}'
                                  : 'Pick Start Time',
                            ),
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 8),
                    // End Time Picker
                    Row(
                      children: [
                        Expanded(
                          child: ElevatedButton.icon(
                            onPressed: () => _pickEndTime(context),
                            icon: const Icon(Icons.access_time),
                            label: Text(
                              _selectedEndTime != null
                                  ? 'End: ${_formatDateTimeWithSeconds(_selectedEndTime!.toLocal())}'
                                  : 'Pick End Time',
                            ),
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 12),
                    // Calculate Button
                    SizedBox(
                      width: double.infinity,
                      child: ElevatedButton.icon(
                        onPressed: (_selectedStartTime != null &&
                                _selectedEndTime != null)
                            ? () => _calculateAndLog()
                            : null,
                        icon: const Icon(Icons.calculate),
                        label: const Text('Calculate Metrics'),
                        style: ElevatedButton.styleFrom(
                          padding: const EdgeInsets.symmetric(vertical: 12),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),

            const SizedBox(height: 16),

            // Motion Data Debug Card (temporary for debugging)
            Card(
              color: Colors.orange.withOpacity(0.1),
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Motion Data Debug',
                      style: Theme.of(context).textTheme.titleMedium?.copyWith(
                            color: Colors.orange[900],
                          ),
                    ),
                    const SizedBox(height: 8),
                    _buildInfoRow('Motion Data Available',
                        widget.summary.motionData != null ? 'Yes' : 'No'),
                    _buildInfoRow('Motion Data Count',
                        '${widget.summary.motionData?.length ?? 0} windows'),
                    _buildInfoRow('Motion State Available',
                        widget.summary.motionState != null ? 'Yes' : 'No'),
                    if (widget.summary.motionData != null &&
                        widget.summary.motionData!.isNotEmpty) ...[
                      const SizedBox(height: 8),
                      Text(
                        'First window sample (first 5 features):',
                        style: Theme.of(context).textTheme.bodySmall,
                      ),
                      const SizedBox(height: 4),
                      Text(
                        widget.summary.motionData!.first.features.entries
                            .take(5)
                            .map((e) =>
                                '${e.key}: ${e.value.toStringAsFixed(4)}')
                            .join('\n'),
                        style: Theme.of(context).textTheme.bodySmall?.copyWith(
                              fontFamily: 'monospace',
                              fontSize: 10,
                            ),
                      ),
                    ],
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // Motion State Card
            if (widget.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('Major State',
                          widget.summary.motionState!.majorState),
                      _buildInfoRow('Major State %',
                          '${(widget.summary.motionState!.majorStatePct * 100).toStringAsFixed(1)}%'),
                      _buildInfoRow(
                          'ML Model', widget.summary.motionState!.mlModel),
                      _buildInfoRow(
                          'Confidence',
                          widget.summary.motionState!.confidence
                              .toStringAsFixed(2)),
                      const SizedBox(height: 12),
                      const Divider(),
                      const SizedBox(height: 8),
                      Text(
                        'State Array (${widget.summary.motionState!.state.length} windows):',
                        style: Theme.of(context).textTheme.titleSmall,
                      ),
                      const SizedBox(height: 8),
                      // Display as JSON-like array format
                      Container(
                        padding: const EdgeInsets.all(12.0),
                        decoration: BoxDecoration(
                          color: Colors.grey[100],
                          borderRadius: BorderRadius.circular(8),
                        ),
                        child: SelectableText(
                          '[${widget.summary.motionState!.state.map((s) => '"$s"').join(', ')}]',
                          style:
                              Theme.of(context).textTheme.bodySmall?.copyWith(
                                    fontFamily: 'monospace',
                                    fontSize: 12,
                                  ),
                        ),
                      ),
                      const SizedBox(height: 8),
                      // Also show as readable list
                      Wrap(
                        spacing: 8,
                        runSpacing: 4,
                        children: widget.summary.motionState!.state
                            .asMap()
                            .entries
                            .map((entry) {
                          return Chip(
                            label: Text(
                              '${entry.key + 1}: ${entry.value}',
                              style: const TextStyle(fontSize: 11),
                            ),
                            padding: const EdgeInsets.symmetric(horizontal: 4),
                            materialTapTargetSize:
                                MaterialTapTargetSize.shrinkWrap,
                            visualDensity: VisualDensity.compact,
                          );
                        }).toList(),
                      ),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 16),
            ],

            // Motion Data Card (ML Features)
            if (widget.summary.motionData != null &&
                widget.summary.motionData!.isNotEmpty) ...[
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        'Motion Data (ML Features)',
                        style: Theme.of(context).textTheme.titleLarge,
                      ),
                      const SizedBox(height: 12),
                      _buildInfoRow(
                        'Data Points',
                        '${widget.summary.motionData!.length} time windows',
                      ),
                      _buildInfoRow(
                        'Time Window',
                        '5 seconds per window',
                      ),
                      _buildInfoRow(
                        'Features per Window',
                        '561 ML features',
                      ),
                      const SizedBox(height: 8),
                      Text(
                        'Sample Features (First window):',
                        style: Theme.of(context).textTheme.titleSmall,
                      ),
                      const SizedBox(height: 8),
                      ...widget.summary.motionData!.take(1).map((dataPoint) {
                        return ExpansionTile(
                          title: Text(
                            'Window: ${dataPoint.timestamp}',
                            style: const TextStyle(fontSize: 12),
                          ),
                          children: [
                            Padding(
                              padding: const EdgeInsets.all(8.0),
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  Text(
                                    'Total Features: ${dataPoint.features.length}',
                                    style: const TextStyle(
                                        fontWeight: FontWeight.bold),
                                  ),
                                  const SizedBox(height: 8),
                                  // Show first 20 features as examples
                                  ...dataPoint.features.entries
                                      .take(20)
                                      .map((entry) {
                                    return Padding(
                                      padding:
                                          const EdgeInsets.only(bottom: 4.0),
                                      child: Row(
                                        crossAxisAlignment:
                                            CrossAxisAlignment.start,
                                        children: [
                                          Expanded(
                                            flex: 2,
                                            child: Text(
                                              entry.key,
                                              style: const TextStyle(
                                                  fontSize: 10,
                                                  fontFamily: 'monospace'),
                                            ),
                                          ),
                                          Expanded(
                                            flex: 1,
                                            child: Text(
                                              entry.value.toStringAsFixed(4),
                                              style: const TextStyle(
                                                  fontSize: 10,
                                                  fontFamily: 'monospace'),
                                              textAlign: TextAlign.right,
                                            ),
                                          ),
                                        ],
                                      ),
                                    );
                                  }),
                                  if (dataPoint.features.length > 20)
                                    Padding(
                                      padding: const EdgeInsets.only(top: 8.0),
                                      child: Text(
                                        '... and ${dataPoint.features.length - 20} more features',
                                        style: Theme.of(context)
                                            .textTheme
                                            .bodySmall,
                                      ),
                                    ),
                                ],
                              ),
                            ),
                          ],
                        );
                      }),
                      if (widget.summary.motionData!.length > 1)
                        Padding(
                          padding: const EdgeInsets.all(8.0),
                          child: Text(
                            '... and ${widget.summary.motionData!.length - 1} more windows',
                            style: Theme.of(context).textTheme.bodySmall,
                          ),
                        ),
                    ],
                  ),
                ),
              ),
              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',
                        widget.summary.deviceContext.avgScreenBrightness
                            .toStringAsFixed(3)),
                    _buildInfoRow('Start Orientation',
                        widget.summary.deviceContext.startOrientation),
                    _buildInfoRow('Orientation Changes',
                        '${widget.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',
                        '${widget.summary.activitySummary.totalEvents}'),
                    _buildInfoRow('App Switch Count',
                        '${widget.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',
                        widget.summary.behavioralMetrics.interactionIntensity
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Task Switch Rate',
                        widget.summary.behavioralMetrics.taskSwitchRate
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Task Switch Cost',
                        _formatMs(
                            widget.summary.behavioralMetrics.taskSwitchCost)),
                    _buildInfoRow(
                        'Idle Time Ratio',
                        widget.summary.behavioralMetrics.idleTimeRatio
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Active Time Ratio',
                        widget.summary.behavioralMetrics.activeTimeRatio
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Notification Load',
                        widget.summary.behavioralMetrics.notificationLoad
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Burstiness',
                        widget.summary.behavioralMetrics.burstiness
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Distraction Score',
                        widget.summary.behavioralMetrics
                            .behavioralDistractionScore
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Focus Hint',
                        widget.summary.behavioralMetrics.focusHint
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Fragmented Idle Ratio',
                        widget.summary.behavioralMetrics.fragmentedIdleRatio
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Scroll Jitter Rate',
                        widget.summary.behavioralMetrics.scrollJitterRate
                            .toStringAsFixed(3)),
                    _buildInfoRow('Deep Focus Blocks',
                        '${widget.summary.behavioralMetrics.deepFocusBlocks.length}'),
                    if (widget.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),
                      ...widget.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',
                        '${widget.summary.notificationSummary.notificationCount}'),
                    _buildInfoRow('Notifications Ignored',
                        '${widget.summary.notificationSummary.notificationIgnored}'),
                    _buildInfoRow(
                        'Ignore Rate',
                        widget
                            .summary.notificationSummary.notificationIgnoreRate
                            .toStringAsFixed(3)),
                    _buildInfoRow(
                        'Clustering Index',
                        widget.summary.notificationSummary
                            .notificationClusteringIndex
                            .toStringAsFixed(3)),
                    _buildInfoRow('Call Count',
                        '${widget.summary.notificationSummary.callCount}'),
                    _buildInfoRow('Calls Ignored',
                        '${widget.summary.notificationSummary.callIgnored}'),
                  ],
                ),
              ),
            ),

            const SizedBox(height: 16),

            // Typing Session Summary Card
            if (widget.summary.typingSessionSummary != null) ...[
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Icon(Icons.keyboard, color: Colors.teal),
                          const SizedBox(width: 8),
                          Text(
                            'Typing Session Summary',
                            style: Theme.of(context).textTheme.titleLarge,
                          ),
                        ],
                      ),
                      const SizedBox(height: 12),
                      _buildInfoRow('Typing Sessions',
                          '${widget.summary.typingSessionSummary!.typingSessionCount}'),
                      _buildInfoRow(
                          'Avg Keystrokes/Session',
                          widget.summary.typingSessionSummary!
                              .averageKeystrokesPerSession
                              .toStringAsFixed(1)),
                      _buildInfoRow(
                          'Avg Session Duration',
                          _formatMs((widget.summary.typingSessionSummary!
                                      .averageTypingSessionDuration *
                                  1000)
                              .round())),
                      _buildInfoRow('Avg Typing Speed',
                          '${widget.summary.typingSessionSummary!.averageTypingSpeed.toStringAsFixed(2)} taps/s'),
                      _buildInfoRow(
                          'Avg Typing Gap',
                          _formatMs(widget
                              .summary.typingSessionSummary!.averageTypingGap
                              .round())),
                      _buildInfoRow(
                          'Average Inter-tap Interval',
                          _formatMs(widget.summary.typingSessionSummary!
                              .averageInterTapInterval
                              .round())),
                      _buildInfoRow(
                          'Cadence Stability',
                          widget.summary.typingSessionSummary!
                              .typingCadenceStability
                              .toStringAsFixed(3)),
                      _buildInfoRow(
                          'Burstiness',
                          widget
                              .summary.typingSessionSummary!.burstinessOfTyping
                              .toStringAsFixed(3)),
                      _buildInfoRow(
                          'Total Typing Duration',
                          _formatMs(widget.summary.typingSessionSummary!
                                  .totalTypingDuration *
                              1000)),
                      _buildInfoRow(
                          'Active Typing Ratio',
                          widget.summary.typingSessionSummary!.activeTypingRatio
                              .toStringAsFixed(3)),
                      _buildInfoRow(
                          'Typing Contribution to Intensity',
                          widget.summary.typingSessionSummary!
                              .typingContributionToInteractionIntensity
                              .toStringAsFixed(3)),
                      _buildInfoRow('Deep Typing Blocks',
                          '${widget.summary.typingSessionSummary!.deepTypingBlocks}'),
                      _buildInfoRow(
                          'Typing Fragmentation',
                          widget
                              .summary.typingSessionSummary!.typingFragmentation
                              .toStringAsFixed(3)),
                      if (widget.summary.typingSessionSummary!
                          .individualTypingSessions.isNotEmpty) ...[
                        const SizedBox(height: 16),
                        const Divider(),
                        const SizedBox(height: 8),
                        Text(
                          'Individual Typing Sessions (${widget.summary.typingSessionSummary!.individualTypingSessions.length})',
                          style: const TextStyle(
                            fontWeight: FontWeight.bold,
                            fontSize: 14,
                          ),
                        ),
                        const SizedBox(height: 8),
                        ...widget.summary.typingSessionSummary!
                            .individualTypingSessions
                            .asMap()
                            .entries
                            .map((entry) {
                          final index = entry.key;
                          final session = entry.value;
                          return ExpansionTile(
                            title: Text(
                              'Session ${index + 1}${session.deepTyping ? " (Deep Typing)" : ""}',
                              style: const TextStyle(fontSize: 14),
                            ),
                            subtitle: Text(
                              '${_formatMs(session.duration * 1000)} • ${session.typingTapCount} keystrokes',
                              style: const TextStyle(fontSize: 12),
                            ),
                            children: [
                              Padding(
                                padding: const EdgeInsets.all(16.0),
                                child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    _buildInfoRow('Start Time',
                                        _formatDateTime(session.startAt)),
                                    _buildInfoRow('End Time',
                                        _formatDateTime(session.endAt)),
                                    _buildInfoRow('Duration',
                                        _formatMs(session.duration * 1000)),
                                    _buildInfoRow('Deep Typing',
                                        session.deepTyping ? 'Yes' : 'No'),
                                    _buildInfoRow('Keystrokes',
                                        '${session.typingTapCount}'),
                                    _buildInfoRow('Typing Speed',
                                        '${session.typingSpeed.toStringAsFixed(2)} taps/s'),
                                    _buildInfoRow(
                                        'Mean Inter-Tap Interval',
                                        _formatMs(session.meanInterTapIntervalMs
                                            .round())),
                                    _buildInfoRow(
                                        'Cadence Variability',
                                        _formatMs(session
                                            .typingCadenceVariability
                                            .round())),
                                    _buildInfoRow(
                                        'Cadence Stability',
                                        session.typingCadenceStability
                                            .toStringAsFixed(3)),
                                    _buildInfoRow('Gap Count',
                                        '${session.typingGapCount}'),
                                    _buildInfoRow(
                                        'Gap Ratio',
                                        session.typingGapRatio
                                            .toStringAsFixed(3)),
                                    _buildInfoRow(
                                        'Burstiness',
                                        session.typingBurstiness
                                            .toStringAsFixed(3)),
                                    _buildInfoRow(
                                        'Activity Ratio',
                                        session.typingActivityRatio
                                            .toStringAsFixed(3)),
                                    _buildInfoRow(
                                        'Interaction Intensity',
                                        session.typingInteractionIntensity
                                            .toStringAsFixed(3)),
                                  ],
                                ),
                              ),
                            ],
                          );
                        }),
                      ],
                    ],
                  ),
                ),
              ),
              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',
                        widget.summary.systemState.internetState
                            ? 'Connected'
                            : 'Disconnected'),
                    _buildInfoRow('Do Not Disturb',
                        widget.summary.systemState.doNotDisturb ? 'On' : 'Off'),
                    _buildInfoRow('Charging',
                        widget.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,
                        );
                      }),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _pickStartTime(BuildContext context) async {
    final sessionStartUtc = DateTime.parse(widget.summary.startAt);
    final sessionEndUtc = DateTime.parse(widget.summary.endAt);
    final sessionStartLocal = sessionStartUtc.toLocal();
    final sessionEndLocal = sessionEndUtc.toLocal();

    final selected = await _showDateTimePicker(
      context: context,
      title: 'Select Start Time',
      initialDateTime: _selectedStartTime?.toLocal() ?? sessionStartLocal,
      firstDate: sessionStartLocal.subtract(const Duration(days: 1)),
      lastDate: sessionEndLocal.add(const Duration(days: 1)),
    );

    if (selected != null) {
      setState(() {
        _selectedStartTime = selected.toUtc();
      });
    }
  }

  Future<void> _pickEndTime(BuildContext context) async {
    final sessionStartUtc = DateTime.parse(widget.summary.startAt);
    final sessionEndUtc = DateTime.parse(widget.summary.endAt);
    final sessionStartLocal = sessionStartUtc.toLocal();
    final sessionEndLocal = sessionEndUtc.toLocal();

    final selected = await _showDateTimePicker(
      context: context,
      title: 'Select End Time',
      initialDateTime: _selectedEndTime?.toLocal() ??
          (_selectedStartTime?.toLocal() ?? sessionStartLocal),
      firstDate: _selectedStartTime?.toLocal() ?? sessionStartLocal,
      lastDate: sessionEndLocal.add(const Duration(days: 1)),
    );

    if (selected != null) {
      setState(() {
        _selectedEndTime = selected.toUtc();
      });
    }
  }

  Future<void> _calculateAndLog() async {
    if (_selectedStartTime == null || _selectedEndTime == null) {
      print('ERROR: Start time or end time is null');
      return;
    }

    if (widget.behavior == null) {
      print('ERROR: Behavior SDK is not available');
      return;
    }

    // Validate that start time is before end time
    if (_selectedStartTime!.isAfter(_selectedEndTime!)) {
      print('');
      print('========================================');
      print('ERROR: INVALID TIME RANGE');
      print('========================================');
      print('Start time must be before end time!');
      print('');
      print('Selected Start (UTC): ${_selectedStartTime!.toIso8601String()}');
      print('Selected End (UTC): ${_selectedEndTime!.toIso8601String()}');
      print(
          'Duration: ${_formatMs(_selectedEndTime!.difference(_selectedStartTime!).inMilliseconds)}');
      print('========================================');
      print('');
      return;
    }

    // Validate time range is within session duration (with 1 second tolerance)
    final sessionStartUtc = DateTime.parse(widget.summary.startAt);
    final sessionEndUtc = DateTime.parse(widget.summary.endAt);
    final sessionStartMs = sessionStartUtc.millisecondsSinceEpoch;
    final sessionEndMs = sessionEndUtc.millisecondsSinceEpoch;
    final selectedStartMs = _selectedStartTime!.millisecondsSinceEpoch;
    final selectedEndMs = _selectedEndTime!.millisecondsSinceEpoch;
    const toleranceMs = 1000; // 1 second tolerance

    if (selectedStartMs < (sessionStartMs - toleranceMs) ||
        selectedEndMs > (sessionEndMs + toleranceMs)) {
      print('');
      print('========================================');
      print('ERROR: TIME RANGE OUT OF BOUNDS');
      print('========================================');
      print('Session Start (UTC): ${sessionStartUtc.toIso8601String()}');
      print('Session End (UTC): ${sessionEndUtc.toIso8601String()}');
      print(
          'Session Duration: ${_formatMs((sessionEndMs - sessionStartMs).toInt())}');
      print('');
      print('Selected Start (UTC): ${_selectedStartTime!.toIso8601String()}');
      print('Selected End (UTC): ${_selectedEndTime!.toIso8601String()}');
      print(
          'Selected Duration: ${_formatMs((selectedEndMs - selectedStartMs).toInt())}');
      print('');
      if (selectedStartMs < (sessionStartMs - toleranceMs)) {
        final diffMs = sessionStartMs - selectedStartMs;
        print(
            'ERROR: Selected start time is ${_formatMs(diffMs.toInt())} before session start time');
      }
      if (selectedEndMs > (sessionEndMs + toleranceMs)) {
        final diffMs = selectedEndMs - sessionEndMs;
        print(
            'ERROR: Selected end time is ${_formatMs(diffMs.toInt())} after session end time');
      }
      print('========================================');
      print('');
      return;
    }

    final startTimestampSeconds =
        _selectedStartTime!.millisecondsSinceEpoch ~/ 1000;
    final endTimestampSeconds =
        _selectedEndTime!.millisecondsSinceEpoch ~/ 1000;

    print('========================================');
    print('CALCULATE METRICS FOR TIME RANGE');
    print('========================================');
    print('Session ID: ${widget.summary.sessionId}');
    print('Start Time (UTC): ${_selectedStartTime!.toIso8601String()}');
    print('End Time (UTC): ${_selectedEndTime!.toIso8601String()}');
    print(
        'Start Time (Local): ${_selectedStartTime!.toLocal().toIso8601String()}');
    print('End Time (Local): ${_selectedEndTime!.toLocal().toIso8601String()}');
    print('Start Timestamp (seconds): $startTimestampSeconds');
    print('End Timestamp (seconds): $endTimestampSeconds');
    print('Duration: ${endTimestampSeconds - startTimestampSeconds} seconds');
    print(
        'Duration: ${_formatMs((endTimestampSeconds - startTimestampSeconds) * 1000)}');
    print('========================================');
    print('');

    try {
      print('Calling calculateMetricsForTimeRange...');
      final result = await widget.behavior!.calculateMetricsForTimeRange(
        startTimestampSeconds: startTimestampSeconds,
        endTimestampSeconds: endTimestampSeconds,
        sessionId: widget.summary.sessionId,
      );

      // Convert to Map<String, dynamic> safely
      final metrics = Map<String, dynamic>.from(result);

      print('');
      print('========================================');
      print('SESSION BEHAVIOR METRICS');
      print('========================================');
      print('');
      print('"session behavior" : {');
      print('    "session_id": "${widget.summary.sessionId}",');
      print('    "start_at": "${_selectedStartTime!.toIso8601String()}",');
      print('    "end_at": "${_selectedEndTime!.toIso8601String()}",');
      print('    "micro_session": ${widget.summary.microSession},');
      print('    "OS": "${widget.summary.os}",');
      if (widget.summary.appId != null) {
        print('    "app_id": "${widget.summary.appId}",');
      }
      print('    "session_spacing": ${widget.summary.sessionSpacing},');

      // Motion State
      if (metrics['motion_state'] != null) {
        final motionState =
            Map<String, dynamic>.from(metrics['motion_state'] as Map);
        print('    "motion_state": {');
        if (motionState['major_state'] != null) {
          print('        "state": "${motionState['major_state']}",');
        }
        if (motionState['ml_model'] != null) {
          print('        "ml_model": "${motionState['ml_model']}",');
        }
        if (motionState['confidence'] != null) {
          print('        "confidence": ${motionState['confidence']}');
        }
        print('    },');
      }

      // Device Context
      if (metrics['device_context'] != null) {
        final deviceContext =
            Map<String, dynamic>.from(metrics['device_context'] as Map);
        print('    "device_context": {');
        print(
            '      "avg_screen_brightness": ${deviceContext['avg_screen_brightness'] ?? 0},');
        print(
            '      "start_orientation": "${deviceContext['start_orientation'] ?? 'N/A'}",');
        print(
            '      "orientation_changes": ${deviceContext['orientation_changes'] ?? 0}');
        print('    },');
      }

      // Activity Summary
      if (metrics['activity_summary'] != null) {
        final activitySummary =
            Map<String, dynamic>.from(metrics['activity_summary'] as Map);
        print('  "activity_summary": {');
        print('    "total_events": ${activitySummary['total_events'] ?? 0},');
        print(
            '    "app_switch_count": ${activitySummary['app_switch_count'] ?? 0}');
        print('  },');
      }

      // Behavioral Metrics
      if (metrics['behavioral_metrics'] != null) {
        final behavioralMetrics =
            Map<String, dynamic>.from(metrics['behavioral_metrics'] as Map);
        print('  "behavioral_metrics": {');
        print(
            '      "interaction_intensity": ${behavioralMetrics['interaction_intensity'] ?? 0},');
        print(
            '      "task_switch_rate": ${behavioralMetrics['task_switch_rate'] ?? 0},');
        print(
            '      "task_switch_cost": ${behavioralMetrics['task_switch_cost'] ?? 0},');
        print(
            '      "idle_time_ratio": ${behavioralMetrics['idle_time_ratio'] ?? 0},');
        print(
            '      "active_time_ratio": ${behavioralMetrics['active_time_ratio'] ?? 0},');
        print(
            '      "notification_load": ${behavioralMetrics['notification_load'] ?? 0},');
        print('      "burstiness": ${behavioralMetrics['burstiness'] ?? 0},');
        print(
            '      "behavioral_distraction_score": ${behavioralMetrics['behavioral_distraction_score'] ?? 0},');

        if (behavioralMetrics['deep_focus_blocks'] != null) {
          final deepFocusBlocks =
              behavioralMetrics['deep_focus_blocks'] as List;
          print('      "deep_focus_blocks": [');
          for (var i = 0; i < deepFocusBlocks.length; i++) {
            final block = Map<String, dynamic>.from(deepFocusBlocks[i] as Map);
            print('        {');
            print('          "start_at": "${block['start_at'] ?? ''}",');
            print('          "end_at": "${block['end_at'] ?? ''}",');
            print('          "duration_ms": ${block['duration_ms'] ?? 0}');
            if (i < deepFocusBlocks.length - 1) {
              print('        },');
            } else {
              print('        }');
            }
          }
          print('      ]');
        }
        print('  },');
      }

      // Notification Summary
      if (metrics['notification_summary'] != null) {
        final notificationSummary =
            Map<String, dynamic>.from(metrics['notification_summary'] as Map);
        print('  "notification_summary": {');
        print(
            '    "notification_count": ${notificationSummary['notification_count'] ?? 0},');
        print(
            '    "notification_ignored": ${notificationSummary['notification_ignored'] ?? 0},');
        print(
            '    "notification_ignore_rate": ${notificationSummary['notification_ignore_rate'] ?? 0},');
        print(
            '    "notification_clustering_index": ${notificationSummary['notification_clustering_index'] ?? 0},');
        print('    "call_count": ${notificationSummary['call_count'] ?? 0},');
        print(
            '    "call_ignored": ${notificationSummary['call_ignored'] ?? 0}');
        print('  },');
      }

      // System State
      if (metrics['system_state'] != null) {
        final systemState =
            Map<String, dynamic>.from(metrics['system_state'] as Map);
        print('  "system_state": {');
        print(
            '    "internet_state": ${systemState['internet_state'] ?? false},');
        print(
            '    "do_not_disturb": ${systemState['do_not_disturb'] ?? false},');
        print('    "charging": ${systemState['charging'] ?? false}');
        print('  }');
      }

      print('}');
      print('');
      print('========================================');
    } catch (e, stackTrace) {
      print('');
      print('ERROR calculating metrics: $e');
      print('Stack trace: $stackTrace');
      print('');
    }
  }

  Future<DateTime?> _showDateTimePicker({
    required BuildContext context,
    required String title,
    required DateTime initialDateTime,
    required DateTime firstDate,
    required DateTime lastDate,
  }) async {
    DateTime selectedDate = initialDateTime;
    int hour = initialDateTime.hour;
    int minute = initialDateTime.minute;
    int second = initialDateTime.second;

    return showDialog<DateTime>(
      context: context,
      builder: (BuildContext context) {
        return StatefulBuilder(
          builder: (context, setState) {
            return AlertDialog(
              title: Text(title),
              content: SingleChildScrollView(
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    // Date picker section
                    const Text(
                      'Date',
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 8),
                    InkWell(
                      onTap: () async {
                        final pickedDate = await showDatePicker(
                          context: context,
                          initialDate: selectedDate,
                          firstDate: firstDate,
                          lastDate: lastDate,
                        );
                        if (pickedDate != null) {
                          setState(() {
                            selectedDate = DateTime(
                              pickedDate.year,
                              pickedDate.month,
                              pickedDate.day,
                              hour,
                              minute,
                              second,
                            );
                          });
                        }
                      },
                      child: Container(
                        padding: const EdgeInsets.all(12),
                        decoration: BoxDecoration(
                          border: Border.all(color: Colors.grey),
                          borderRadius: BorderRadius.circular(4),
                        ),
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            Text(
                              '${selectedDate.year}-${selectedDate.month.toString().padLeft(2, '0')}-${selectedDate.day.toString().padLeft(2, '0')}',
                              style: const TextStyle(fontSize: 16),
                            ),
                            const Icon(Icons.calendar_today),
                          ],
                        ),
                      ),
                    ),
                    const SizedBox(height: 24),
                    // Time picker section
                    const Text(
                      'Time (HH:MM:SS)',
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        // Hour
                        Column(
                          children: [
                            IconButton(
                              icon: const Icon(Icons.arrow_drop_up),
                              onPressed: () {
                                setState(() {
                                  hour = (hour + 1) % 24;
                                  selectedDate = DateTime(
                                    selectedDate.year,
                                    selectedDate.month,
                                    selectedDate.day,
                                    hour,
                                    minute,
                                    second,
                                  );
                                });
                              },
                            ),
                            Container(
                              width: 60,
                              padding: const EdgeInsets.symmetric(vertical: 8),
                              decoration: BoxDecoration(
                                border: Border.all(color: Colors.grey),
                                borderRadius: BorderRadius.circular(4),
                              ),
                              child: Text(
                                hour.toString().padLeft(2, '0'),
                                textAlign: TextAlign.center,
                                style: const TextStyle(fontSize: 24),
                              ),
                            ),
                            IconButton(
                              icon: const Icon(Icons.arrow_drop_down),
                              onPressed: () {
                                setState(() {
                                  hour = (hour - 1 + 24) % 24;
                                  selectedDate = DateTime(
                                    selectedDate.year,
                                    selectedDate.month,
                                    selectedDate.day,
                                    hour,
                                    minute,
                                    second,
                                  );
                                });
                              },
                            ),
                          ],
                        ),
                        const Text(':', style: TextStyle(fontSize: 24)),
                        // Minute
                        Column(
                          children: [
                            IconButton(
                              icon: const Icon(Icons.arrow_drop_up),
                              onPressed: () {
                                setState(() {
                                  minute = (minute + 1) % 60;
                                  selectedDate = DateTime(
                                    selectedDate.year,
                                    selectedDate.month,
                                    selectedDate.day,
                                    hour,
                                    minute,
                                    second,
                                  );
                                });
                              },
                            ),
                            Container(
                              width: 60,
                              padding: const EdgeInsets.symmetric(vertical: 8),
                              decoration: BoxDecoration(
                                border: Border.all(color: Colors.grey),
                                borderRadius: BorderRadius.circular(4),
                              ),
                              child: Text(
                                minute.toString().padLeft(2, '0'),
                                textAlign: TextAlign.center,
                                style: const TextStyle(fontSize: 24),
                              ),
                            ),
                            IconButton(
                              icon: const Icon(Icons.arrow_drop_down),
                              onPressed: () {
                                setState(() {
                                  minute = (minute - 1 + 60) % 60;
                                  selectedDate = DateTime(
                                    selectedDate.year,
                                    selectedDate.month,
                                    selectedDate.day,
                                    hour,
                                    minute,
                                    second,
                                  );
                                });
                              },
                            ),
                          ],
                        ),
                        const Text(':', style: TextStyle(fontSize: 24)),
                        // Second
                        Column(
                          children: [
                            IconButton(
                              icon: const Icon(Icons.arrow_drop_up),
                              onPressed: () {
                                setState(() {
                                  second = (second + 1) % 60;
                                  selectedDate = DateTime(
                                    selectedDate.year,
                                    selectedDate.month,
                                    selectedDate.day,
                                    hour,
                                    minute,
                                    second,
                                  );
                                });
                              },
                            ),
                            Container(
                              width: 60,
                              padding: const EdgeInsets.symmetric(vertical: 8),
                              decoration: BoxDecoration(
                                border: Border.all(color: Colors.grey),
                                borderRadius: BorderRadius.circular(4),
                              ),
                              child: Text(
                                second.toString().padLeft(2, '0'),
                                textAlign: TextAlign.center,
                                style: const TextStyle(fontSize: 24),
                              ),
                            ),
                            IconButton(
                              icon: const Icon(Icons.arrow_drop_down),
                              onPressed: () {
                                setState(() {
                                  second = (second - 1 + 60) % 60;
                                  selectedDate = DateTime(
                                    selectedDate.year,
                                    selectedDate.month,
                                    selectedDate.day,
                                    hour,
                                    minute,
                                    second,
                                  );
                                });
                              },
                            ),
                          ],
                        ),
                      ],
                    ),
                  ],
                ),
              ),
              actions: [
                TextButton(
                  onPressed: () => Navigator.of(context).pop(),
                  child: const Text('Cancel'),
                ),
                TextButton(
                  onPressed: () {
                    Navigator.of(context).pop(selectedDate);
                  },
                  child: const Text('OK'),
                ),
              ],
            );
          },
        );
      },
    );
  }

  String _formatDateTimeWithSeconds(DateTime dateTime) {
    return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} '
        '${dateTime.hour.toString().padLeft(2, '0')}:'
        '${dateTime.minute.toString().padLeft(2, '0')}:'
        '${dateTime.second.toString().padLeft(2, '0')}';
  }

  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(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Flexible(
                      child: Text(
                        '${entry.key}: ',
                        style: Theme.of(context).textTheme.bodySmall?.copyWith(
                              fontWeight: FontWeight.w500,
                              fontFamily: 'monospace',
                            ),
                      ),
                    ),
                    Flexible(
                      flex: 2,
                      child: Text(
                        entry.value.toString(),
                        style: Theme.of(context).textTheme.bodySmall?.copyWith(
                              fontFamily: 'monospace',
                            ),
                        overflow: TextOverflow.ellipsis,
                        maxLines: 2,
                      ),
                    ),
                  ],
                ),
              )),
        ],
      ),
    );
  }

  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;
      case BehaviorEventType.typing:
        return Colors.teal;
    }
  }

  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
416
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
Contributing

Topics

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

Documentation

API reference

License

unknown (license)

Dependencies

flutter, flutter_onnxruntime

More

Packages that depend on synheart_behavior

Packages that implement synheart_behavior