flutter_app_usage_kit 0.1.0 copy "flutter_app_usage_kit: ^0.1.0" to clipboard
flutter_app_usage_kit: ^0.1.0 copied to clipboard

PlatformAndroid

Android UsageStatsManager plugin for querying per-app screen time, foreground events, and installed app metadata.

example/lib/main.dart

import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:flutter_app_usage_kit/flutter_app_usage_kit.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'App Usage Kit',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const _HomeShell(),
    );
  }
}

// ---------------------------------------------------------------------------
// Shell with bottom nav
// ---------------------------------------------------------------------------

class _HomeShell extends StatefulWidget {
  const _HomeShell();

  @override
  State<_HomeShell> createState() => _HomeShellState();
}

class _HomeShellState extends State<_HomeShell> {
  int _tab = 0;

  static const _tabs = [_TodayUsageTab(), _RecentEventsTab(), _SettingsTab()];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _tabs[_tab],
      bottomNavigationBar: NavigationBar(
        selectedIndex: _tab,
        onDestinationSelected: (i) => setState(() => _tab = i),
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.bar_chart),
            label: "Today's Usage",
          ),
          NavigationDestination(
            icon: Icon(Icons.list_alt),
            label: 'Recent Events',
          ),
          NavigationDestination(icon: Icon(Icons.settings), label: 'Settings'),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Tab 1 — Today's Usage
// ---------------------------------------------------------------------------

class _TodayUsageTab extends StatefulWidget {
  const _TodayUsageTab();

  @override
  State<_TodayUsageTab> createState() => _TodayUsageTabState();
}

class _TodayUsageTabState extends State<_TodayUsageTab>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  List<UsageInfo>? _stats;
  Map<String, AppInfo> _appInfoCache = {};
  String? _error;
  bool _loading = false;

  @override
  void initState() {
    super.initState();
    _load();
  }

  Future<void> _load() async {
    if (_loading) return;
    setState(() {
      _loading = true;
      _error = null;
    });
    try {
      final now = DateTime.now();
      final start = DateTime(now.year, now.month, now.day);
      final stats = await AppUsageKit.queryUsageStats(start: start, end: now);
      stats.sort(
        (a, b) => b.totalForegroundTime.compareTo(a.totalForegroundTime),
      );
      final nonZero = stats
          .where((s) => s.totalForegroundTime.inSeconds > 0)
          .toList();

      // Batch fetch app names (no icons — fast)
      final cache = <String, AppInfo>{};
      for (final s in nonZero) {
        final info = await AppUsageKit.getAppInfo(s.packageName);
        if (info != null) cache[s.packageName] = info;
      }
      if (mounted) {
        setState(() {
          _stats = nonZero;
          _appInfoCache = cache;
        });
      }
    } on PermissionDeniedException {
      if (mounted) setState(() => _error = 'permission_denied');
    } catch (e) {
      if (mounted) setState(() => _error = e.toString());
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return CustomScrollView(
      slivers: [
        SliverAppBar.large(
          title: const Text("Today's Usage"),
          actions: [
            if (!_loading)
              IconButton(icon: const Icon(Icons.refresh), onPressed: _load),
          ],
        ),
        if (_loading)
          const SliverFillRemaining(
            child: Center(child: CircularProgressIndicator()),
          )
        else if (_error == 'permission_denied')
          SliverFillRemaining(child: _PermissionBanner(onGranted: _load))
        else if (_error != null)
          SliverFillRemaining(
            child: _ErrorBanner(message: _error!, onRetry: _load),
          )
        else if (_stats == null || _stats!.isEmpty)
          const SliverFillRemaining(
            child: Center(child: Text('No usage data for today yet.')),
          )
        else
          SliverList.builder(
            itemCount: _stats!.length,
            itemBuilder: (ctx, i) {
              final s = _stats![i];
              final info = _appInfoCache[s.packageName];
              return ListTile(
                leading: _AppIcon(
                  iconPng: info?.iconPng,
                  packageName: s.packageName,
                ),
                title: Text(
                  info?.appName ?? s.packageName,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
                subtitle: Text(
                  s.packageName,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                  style: Theme.of(ctx).textTheme.bodySmall,
                ),
                trailing: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    Text(
                      _formatDuration(s.totalForegroundTime),
                      style: Theme.of(ctx).textTheme.titleMedium,
                    ),
                    if (s.launchCount > 0)
                      Text(
                        '${s.launchCount}× launched',
                        style: Theme.of(ctx).textTheme.bodySmall,
                      ),
                  ],
                ),
              );
            },
          ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Tab 2 — Recent Events (last 1 hour)
// ---------------------------------------------------------------------------

class _RecentEventsTab extends StatefulWidget {
  const _RecentEventsTab();

  @override
  State<_RecentEventsTab> createState() => _RecentEventsTabState();
}

class _RecentEventsTabState extends State<_RecentEventsTab>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  List<UsageEvent>? _events;
  String? _error;
  bool _loading = false;

  @override
  void initState() {
    super.initState();
    _load();
  }

  Future<void> _load() async {
    if (_loading) return;
    setState(() {
      _loading = true;
      _error = null;
    });
    try {
      final now = DateTime.now();
      final start = now.subtract(const Duration(hours: 1));
      final events = await AppUsageKit.queryEvents(start: start, end: now);
      events.sort((a, b) => b.timestamp.compareTo(a.timestamp));
      if (mounted) setState(() => _events = events);
    } on PermissionDeniedException {
      if (mounted) setState(() => _error = 'permission_denied');
    } catch (e) {
      if (mounted) setState(() => _error = e.toString());
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return CustomScrollView(
      slivers: [
        SliverAppBar.large(
          title: const Text('Recent Events'),
          actions: [
            if (!_loading)
              IconButton(icon: const Icon(Icons.refresh), onPressed: _load),
          ],
        ),
        if (_loading)
          const SliverFillRemaining(
            child: Center(child: CircularProgressIndicator()),
          )
        else if (_error == 'permission_denied')
          SliverFillRemaining(child: _PermissionBanner(onGranted: _load))
        else if (_error != null)
          SliverFillRemaining(
            child: _ErrorBanner(message: _error!, onRetry: _load),
          )
        else if (_events == null || _events!.isEmpty)
          const SliverFillRemaining(
            child: Center(child: Text('No events in the last hour.')),
          )
        else
          SliverList.builder(
            itemCount: _events!.length,
            itemBuilder: (ctx, i) {
              final e = _events![i];
              return ListTile(
                leading: _EventTypeIcon(type: e.type),
                title: Text(
                  e.packageName,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
                subtitle: Text(
                  e.className ?? e.type.name,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                  style: Theme.of(ctx).textTheme.bodySmall,
                ),
                trailing: Text(
                  _formatTime(e.timestamp),
                  style: Theme.of(ctx).textTheme.bodySmall,
                ),
              );
            },
          ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Tab 3 — Settings
// ---------------------------------------------------------------------------

class _SettingsTab extends StatefulWidget {
  const _SettingsTab();

  @override
  State<_SettingsTab> createState() => _SettingsTabState();
}

class _SettingsTabState extends State<_SettingsTab>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  bool? _hasPermission;
  int? _installedAppCount;
  bool _includeSystemApps = false;
  bool _loadingApps = false;

  @override
  void initState() {
    super.initState();
    _checkPermission();
  }

  Future<void> _checkPermission() async {
    final granted = await AppUsageKit.hasPermission();
    if (mounted) setState(() => _hasPermission = granted);
  }

  Future<void> _openSettings() async {
    await AppUsageKit.openPermissionSettings();
    await Future<void>.delayed(const Duration(seconds: 1));
    await _checkPermission();
  }

  Future<void> _loadAppCount() async {
    setState(() => _loadingApps = true);
    final apps = await AppUsageKit.getInstalledApps(
      includeSystemApps: _includeSystemApps,
    );
    if (mounted) {
      setState(() {
        _installedAppCount = apps.length;
        _loadingApps = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    final cs = Theme.of(context).colorScheme;
    return CustomScrollView(
      slivers: [
        const SliverAppBar.large(title: Text('Settings')),
        SliverList(
          delegate: SliverChildListDelegate([
            // Permission card
            Card(
              margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Usage Access Permission',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    Row(
                      children: [
                        Icon(
                          _hasPermission == true
                              ? Icons.check_circle
                              : Icons.cancel,
                          color: _hasPermission == true ? cs.primary : cs.error,
                        ),
                        const SizedBox(width: 8),
                        Text(
                          _hasPermission == null
                              ? 'Checking…'
                              : _hasPermission!
                              ? 'Granted'
                              : 'Not granted',
                        ),
                      ],
                    ),
                    if (_hasPermission == false) ...[
                      const SizedBox(height: 12),
                      FilledButton.icon(
                        icon: const Icon(Icons.open_in_new),
                        label: const Text('Open Usage Access Settings'),
                        onPressed: _openSettings,
                      ),
                    ],
                    const SizedBox(height: 8),
                    TextButton(
                      onPressed: _checkPermission,
                      child: const Text('Re-check permission'),
                    ),
                  ],
                ),
              ),
            ),

            // Installed apps card
            Card(
              margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Installed Apps',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    SwitchListTile(
                      contentPadding: EdgeInsets.zero,
                      title: const Text('Include system apps'),
                      value: _includeSystemApps,
                      onChanged: (v) => setState(() {
                        _includeSystemApps = v;
                        _installedAppCount = null;
                      }),
                    ),
                    if (_installedAppCount != null)
                      Padding(
                        padding: const EdgeInsets.only(bottom: 8),
                        child: Text(
                          'Found $_installedAppCount apps',
                          style: Theme.of(context).textTheme.bodyLarge,
                        ),
                      ),
                    FilledButton.tonal(
                      onPressed: _loadingApps ? null : _loadAppCount,
                      child: _loadingApps
                          ? const SizedBox(
                              width: 16,
                              height: 16,
                              child: CircularProgressIndicator(strokeWidth: 2),
                            )
                          : const Text('Count installed apps'),
                    ),
                  ],
                ),
              ),
            ),

            // Plugin info card
            Card(
              margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Plugin Info',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    const _InfoRow(
                      label: 'Package',
                      value: 'flutter_app_usage_kit',
                    ),
                    const _InfoRow(label: 'Platform', value: 'Android only'),
                    const _InfoRow(
                      label: 'Min SDK',
                      value: 'API 21 (Android 5.0)',
                    ),
                    const _InfoRow(
                      label: 'Data source',
                      value: 'UsageStatsManager',
                    ),
                  ],
                ),
              ),
            ),
          ]),
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Shared widgets
// ---------------------------------------------------------------------------

class _PermissionBanner extends StatelessWidget {
  const _PermissionBanner({required this.onGranted});
  final VoidCallback onGranted;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(32),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              Icons.lock_outline,
              size: 64,
              color: Theme.of(context).colorScheme.secondary,
            ),
            const SizedBox(height: 16),
            Text(
              'Usage Access Required',
              style: Theme.of(context).textTheme.titleLarge,
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 8),
            const Text(
              'Grant "Usage Access" permission in Settings to see app usage data.',
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 24),
            FilledButton.icon(
              icon: const Icon(Icons.open_in_new),
              label: const Text('Open Settings'),
              onPressed: () async {
                await AppUsageKit.openPermissionSettings();
                await Future<void>.delayed(const Duration(seconds: 1));
                onGranted();
              },
            ),
          ],
        ),
      ),
    );
  }
}

class _ErrorBanner extends StatelessWidget {
  const _ErrorBanner({required this.message, required this.onRetry});
  final String message;
  final VoidCallback onRetry;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(32),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              Icons.error_outline,
              size: 64,
              color: Theme.of(context).colorScheme.error,
            ),
            const SizedBox(height: 16),
            Text(message, textAlign: TextAlign.center),
            const SizedBox(height: 16),
            FilledButton(onPressed: onRetry, child: const Text('Retry')),
          ],
        ),
      ),
    );
  }
}

class _AppIcon extends StatelessWidget {
  const _AppIcon({required this.iconPng, required this.packageName});
  final Uint8List? iconPng;
  final String packageName;

  @override
  Widget build(BuildContext context) {
    if (iconPng != null) {
      return CircleAvatar(
        backgroundImage: MemoryImage(iconPng!),
        backgroundColor: Colors.transparent,
      );
    }
    return CircleAvatar(
      child: Text(packageName.isNotEmpty ? packageName[0].toUpperCase() : '?'),
    );
  }
}

class _EventTypeIcon extends StatelessWidget {
  const _EventTypeIcon({required this.type});
  final UsageEventType type;

  @override
  Widget build(BuildContext context) {
    final (icon, color) = switch (type) {
      UsageEventType.foreground => (Icons.play_arrow, Colors.green),
      UsageEventType.background => (Icons.pause, Colors.orange),
      UsageEventType.screenInteractive => (Icons.brightness_high, Colors.blue),
      UsageEventType.screenNonInteractive => (
        Icons.brightness_3,
        Colors.blueGrey,
      ),
      UsageEventType.keyguardShown => (Icons.lock, Colors.purple),
      UsageEventType.keyguardHidden => (Icons.lock_open, Colors.teal),
      UsageEventType.unknown => (Icons.help_outline, Colors.grey),
    };
    return Icon(icon, color: color);
  }
}

class _InfoRow extends StatelessWidget {
  const _InfoRow({required this.label, required this.value});
  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        children: [
          SizedBox(
            width: 90,
            child: Text(label, style: Theme.of(context).textTheme.bodySmall),
          ),
          Expanded(child: Text(value)),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

String _formatDuration(Duration d) {
  if (d.inHours >= 1) {
    final h = d.inHours;
    final m = d.inMinutes.remainder(60);
    return '${h}h ${m}m';
  }
  if (d.inMinutes >= 1) return '${d.inMinutes}m ${d.inSeconds.remainder(60)}s';
  return '${d.inSeconds}s';
}

String _formatTime(DateTime dt) {
  final h = dt.hour.toString().padLeft(2, '0');
  final m = dt.minute.toString().padLeft(2, '0');
  final s = dt.second.toString().padLeft(2, '0');
  return '$h:$m:$s';
}
1
likes
160
points
40
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Android UsageStatsManager plugin for querying per-app screen time, foreground events, and installed app metadata.

Homepage
Repository (GitHub)
View/report issues

Topics

#android #usage-stats #screen-time #analytics

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on flutter_app_usage_kit

Packages that implement flutter_app_usage_kit