flutter_device_apps 0.6.0 copy "flutter_device_apps: ^0.6.0" to clipboard
flutter_device_apps: ^0.6.0 copied to clipboard

PlatformAndroid

App-facing API for listing/inspecting installed apps (federated).

example/lib/main.dart

import 'dart:async';

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Device Apps Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const AppManagerScreen(),
    );
  }
}

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

  @override
  State<AppManagerScreen> createState() => _AppManagerScreenState();
}

class _AppManagerScreenState extends State<AppManagerScreen> {
  List<AppInfo> _apps = [];
  bool _loading = false;
  String _statusMessage = '';
  AppInfo? _selectedApp;
  List<String>? _selectedAppPermissions;
  StreamSubscription<AppChangeEvent>? _appChangeSubscription;
  bool _isMonitoring = false;
  final List<String> _changeEvents = [];

  // Filtering options
  bool _includeSystem = false;
  bool _onlyLaunchable = true;
  bool _includeIcons = false;

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

  @override
  void dispose() {
    _appChangeSubscription?.cancel();
    super.dispose();
  }

  Future<void> _loadApps() async {
    setState(() {
      _loading = true;
      _statusMessage = 'Loading apps...';
    });

    try {
      final apps = await FlutterDeviceApps.listApps(
        includeSystem: _includeSystem,
        onlyLaunchable: _onlyLaunchable,
        includeIcons: _includeIcons,
      );

      setState(() {
        _apps = apps;
        _statusMessage = 'Found ${apps.length} apps';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Error loading apps: $e';
      });
    } finally {
      setState(() {
        _loading = false;
      });
    }
  }

  Future<void> _getAppDetails(String packageName) async {
    setState(() {
      _loading = true;
      _statusMessage = 'Loading app details...';
      _selectedAppPermissions = null; // Clear previous permissions
    });

    try {
      final app = await FlutterDeviceApps.getApp(packageName, includeIcon: true);
      if (app != null) {
        setState(() {
          _selectedApp = app;
          _statusMessage = 'App details loaded';
        });
        // Load permissions automatically
        _getRequestedPermissions(packageName);
      } else {
        setState(() {
          _statusMessage = 'App not found';
        });
      }
    } catch (e) {
      setState(() {
        _statusMessage = 'Error loading app details: $e';
      });
    } finally {
      setState(() {
        _loading = false;
      });
    }
  }

  Future<void> _openApp(String packageName) async {
    try {
      final success = await FlutterDeviceApps.openApp(packageName);
      setState(() {
        _statusMessage = success
            ? 'App opened successfully'
            : 'Failed to open app (not launchable?)';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Error opening app: $e';
      });
    }
  }

  Future<void> _openAppSettings(String packageName) async {
    try {
      final success = await FlutterDeviceApps.openAppSettings(packageName);
      setState(() {
        _statusMessage = success ? 'App settings opened' : 'Failed to open app settings';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Error opening app settings: $e';
      });
    }
  }

  Future<void> _uninstallApp(String packageName) async {
    try {
      final success = await FlutterDeviceApps.uninstallApp(packageName);
      setState(() {
        _statusMessage = success ? 'Uninstall dialog opened' : 'Failed to open uninstall dialog';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Error opening uninstall dialog: $e';
      });
    }
  }

  Future<void> _getInstallerStore(String packageName) async {
    try {
      final store = await FlutterDeviceApps.getInstallerStore(packageName);
      setState(() {
        _statusMessage = store != null
            ? 'Installer: ${_getStoreDisplayName(store)}'
            : 'Unknown installer (sideloaded?)';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Error getting installer info: $e';
      });
    }
  }

  Future<void> _getRequestedPermissions(String packageName) async {
    try {
      final permissions = await FlutterDeviceApps.getRequestedPermissions(packageName);
      setState(() {
        _selectedAppPermissions = permissions;
        _statusMessage = permissions != null
            ? 'Found ${permissions.length} permissions'
            : 'No permissions info available';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Error getting permissions: $e';
        _selectedAppPermissions = null;
      });
    }
  }

  Future<void> _toggleAppMonitoring() async {
    try {
      if (_isMonitoring) {
        await _appChangeSubscription?.cancel();
        setState(() {
          _isMonitoring = false;
          _statusMessage = 'Stopped monitoring app changes';
        });
      } else {
        _appChangeSubscription = FlutterDeviceApps.appChanges.listen(
          (event) {
            final eventText = '${event.type?.name.toUpperCase()} → ${event.packageName}';
            setState(() {
              _changeEvents.insert(0, eventText);
              if (_changeEvents.length > 10) {
                _changeEvents.removeLast();
              }
              _statusMessage = 'App change detected: $eventText';
            });
          },
          onError: (error) {
            setState(() {
              _statusMessage = 'Monitoring error: $error';
            });
          },
        );
        setState(() {
          _isMonitoring = true;
          _statusMessage = 'Started monitoring app changes';
        });
      }
    } catch (e) {
      setState(() {
        _statusMessage = 'Error toggling monitoring: $e';
      });
    }
  }

  String _formatDateTime(DateTime? dateTime) {
    if (dateTime == null) return 'N/A';
    return '${dateTime.day}/${dateTime.month}/${dateTime.year} ${dateTime.hour}:${dateTime.minute.toString().padLeft(2, '0')}';
  }

  String _getStoreDisplayName(String? store) {
    if (store == null) return 'Unknown/Sideloaded';

    final storeNames = {
      'com.android.vending': 'Google Play Store',
      'com.amazon.venezia': 'Amazon Appstore',
      'com.sec.android.app.samsungapps': 'Samsung Galaxy Store',
      'com.huawei.appmarket': 'Huawei AppGallery',
    };

    return storeNames[store] ?? store;
  }

  String _getCategoryName(int? category) {
    if (category == null) return 'N/A';

    // Android ApplicationInfo category constants (API 26+)
    const categories = {
      -1: 'Undefined', // CATEGORY_UNDEFINED
      0: 'Game', // CATEGORY_GAME
      1: 'Audio', // CATEGORY_AUDIO
      2: 'Video', // CATEGORY_VIDEO
      3: 'Image', // CATEGORY_IMAGE
      4: 'Social', // CATEGORY_SOCIAL
      5: 'News', // CATEGORY_NEWS
      6: 'Maps', // CATEGORY_MAPS
      7: 'Productivity', // CATEGORY_PRODUCTIVITY
      8: 'Accessibility', // CATEGORY_ACCESSIBILITY (API 31+)
    };

    return categories[category] ?? 'Unknown ($category)';
  }

  String _getInstallLocationName(int? location) {
    if (location == null) return 'N/A';

    // Android PackageInfo installLocation constants
    const locations = {-1: 'Auto', 0: 'Internal Only', 1: 'Prefer External', 2: 'Internal'};

    return locations[location] ?? 'Unknown ($location)';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Flutter Device Apps Example'),
        actions: [
          IconButton(
            onPressed: _toggleAppMonitoring,
            icon: Icon(_isMonitoring ? Icons.stop : Icons.play_arrow),
            tooltip: _isMonitoring ? 'Stop Monitoring' : 'Start Monitoring',
          ),
        ],
      ),
      body: Column(
        children: [
          // Status bar
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(8.0),
            color: Theme.of(context).colorScheme.surfaceContainerHighest,
            child: Text(
              _statusMessage.isEmpty ? 'Ready' : _statusMessage,
              style: Theme.of(context).textTheme.bodyMedium,
              textAlign: TextAlign.center,
            ),
          ),
          // Filter section
          _buildFilterSection(),
          // Recent changes (if monitoring)
          if (_isMonitoring && _changeEvents.isNotEmpty) _buildChangeEventsSection(),
          // Main content
          Expanded(child: _buildMainContent()),
        ],
      ),
    );
  }

  Widget _buildFilterSection() {
    return Card(
      margin: const EdgeInsets.all(8.0),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Filter Options', style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 12),

            // Checkbox options in a responsive wrap
            Wrap(
              spacing: 24.0,
              runSpacing: 8.0,
              children: [
                _buildCompactCheckbox(
                  'System Apps',
                  _includeSystem,
                  (value) => setState(() => _includeSystem = value ?? false),
                ),
                _buildCompactCheckbox(
                  'Launchable Only',
                  _onlyLaunchable,
                  (value) => setState(() => _onlyLaunchable = value ?? true),
                ),
                _buildCompactCheckbox(
                  'Include Icons',
                  _includeIcons,
                  (value) => setState(() => _includeIcons = value ?? false),
                ),
              ],
            ),

            const SizedBox(height: 16),

            // Refresh button
            Center(
              child: ElevatedButton.icon(
                onPressed: _loading ? null : _loadApps,
                icon: _loading
                    ? const SizedBox(
                        width: 16,
                        height: 16,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      )
                    : const Icon(Icons.refresh, size: 18),
                label: const Text('Refresh Apps'),
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildChangeEventsSection() {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 8.0),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Recent Changes', style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 4),
            ...(_changeEvents
                .take(3)
                .map((event) => Text('• $event', style: Theme.of(context).textTheme.bodySmall))),
          ],
        ),
      ),
    );
  }

  Widget _buildMainContent() {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // App list
        Expanded(flex: 4, child: _buildAppList()),
        // App details
        Expanded(flex: 6, child: _buildAppDetails()),
      ],
    );
  }

  Widget _buildAppList() {
    return Card(
      margin: const EdgeInsets.only(left: 8.0, right: 4.0, bottom: 8.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(
              'Installed Apps (${_apps.length})',
              style: Theme.of(context).textTheme.titleMedium,
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _apps.length,
              itemBuilder: (context, index) {
                final app = _apps[index];
                return ListTile(
                  leading: app.iconBytes != null
                      ? Image.memory(
                          app.iconBytes!,
                          width: 28,
                          height: 28,
                          errorBuilder: (context, error, stackTrace) =>
                              const Icon(Icons.android, size: 28),
                        )
                      : const Icon(Icons.android, size: 28),
                  contentPadding: EdgeInsets.zero,
                  title: Text(
                    app.appName ?? 'Unknown',
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                  subtitle: Text(
                    app.packageName ?? '',
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                  onTap: () => _getAppDetails(app.packageName ?? ''),
                  dense: true,
                );
              },
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildAppDetails() {
    return Card(
      margin: const EdgeInsets.only(left: 4.0, right: 8.0, bottom: 8.0),
      child: _selectedApp == null
          ? const Center(
              child: Text(
                'Select an app to view details',
                style: TextStyle(fontSize: 16, color: Colors.grey),
              ),
            )
          : SingleChildScrollView(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // App icon and name
                  Row(
                    children: [
                      if (_selectedApp!.iconBytes != null)
                        Image.memory(
                          _selectedApp!.iconBytes!,
                          width: 64,
                          height: 64,
                          errorBuilder: (context, error, stackTrace) =>
                              const Icon(Icons.android, size: 64),
                        )
                      else
                        const Icon(Icons.android, size: 64),
                      const SizedBox(width: 16),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              _selectedApp!.appName ?? 'Unknown',
                              style: Theme.of(context).textTheme.titleLarge,
                              maxLines: 2,
                              overflow: TextOverflow.ellipsis,
                            ),
                            Text(
                              _selectedApp!.packageName ?? '',
                              style: Theme.of(context).textTheme.bodyMedium,
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),

                  const SizedBox(height: 16),

                  // App details
                  _buildDetailRow(
                    'Version',
                    '${_selectedApp!.versionName ?? 'N/A'} (${_selectedApp!.versionCode ?? 'N/A'})',
                  ),
                  _buildDetailRow('First Install', _formatDateTime(_selectedApp!.firstInstallTime)),
                  _buildDetailRow('Last Update', _formatDateTime(_selectedApp!.lastUpdateTime)),
                  _buildDetailRow('System App', _selectedApp!.isSystem == true ? 'Yes' : 'No'),
                  _buildDetailRow(
                    'Enabled',
                    _selectedApp!.enabled == true
                        ? 'Yes'
                        : _selectedApp!.enabled == false
                        ? 'No'
                        : 'N/A',
                  ),
                  const SizedBox(height: 8),

                  _buildDetailRow('Category', _getCategoryName(_selectedApp!.category)),
                  _buildDetailRow(
                    'Target SDK',
                    _selectedApp!.targetSdkVersion?.toString() ?? 'N/A',
                  ),
                  _buildDetailRow('Min SDK', _selectedApp!.minSdkVersion?.toString() ?? 'N/A'),
                  _buildDetailRow('Process Name', _selectedApp!.processName ?? 'N/A'),
                  _buildDetailRow(
                    'Install Location',
                    _getInstallLocationName(_selectedApp!.installLocation),
                  ),

                  const SizedBox(height: 16),

                  // Action buttons
                  Text('Actions', style: Theme.of(context).textTheme.titleMedium),
                  const SizedBox(height: 8),

                  Wrap(
                    spacing: 8,
                    runSpacing: 8,
                    children: [
                      ElevatedButton.icon(
                        onPressed: () => _openApp(_selectedApp!.packageName!),
                        icon: const Icon(Icons.launch, size: 16),
                        label: const Text('Open'),
                      ),
                      ElevatedButton.icon(
                        onPressed: () => _openAppSettings(_selectedApp!.packageName!),
                        icon: const Icon(Icons.settings, size: 16),
                        label: const Text('Settings'),
                      ),
                      ElevatedButton.icon(
                        onPressed: () => _uninstallApp(_selectedApp!.packageName!),
                        icon: const Icon(Icons.delete, size: 16),
                        label: const Text('Uninstall'),
                        style: ElevatedButton.styleFrom(foregroundColor: Colors.red),
                      ),
                      ElevatedButton.icon(
                        onPressed: () => _getInstallerStore(_selectedApp!.packageName!),
                        icon: const Icon(Icons.store, size: 16),
                        label: const Text('Installer'),
                      ),
                    ],
                  ),

                  // Permissions section
                  if (_selectedAppPermissions != null) ...[
                    const SizedBox(height: 16),
                    Row(
                      children: [
                        Flexible(
                          child: Text(
                            'Requested Permissions (${_selectedAppPermissions!.length})',
                            style: Theme.of(context).textTheme.titleMedium,
                          ),
                        ),
                        const SizedBox(width: 8),
                        IconButton(
                          icon: const Icon(Icons.refresh, size: 18),
                          onPressed: () => _getRequestedPermissions(_selectedApp!.packageName!),
                          tooltip: 'Refresh permissions',
                          padding: EdgeInsets.zero,
                          constraints: const BoxConstraints(),
                        ),
                      ],
                    ),
                    const SizedBox(height: 8),
                    if (_selectedAppPermissions!.isEmpty)
                      const Text(
                        'No permissions requested',
                        style: TextStyle(fontStyle: FontStyle.italic),
                      )
                    else
                      Container(
                        constraints: const BoxConstraints(maxHeight: 400),
                        decoration: BoxDecoration(
                          border: Border.all(color: Colors.grey.shade300),
                          borderRadius: BorderRadius.circular(8),
                        ),
                        child: ListView.builder(
                          shrinkWrap: true,
                          itemCount: _selectedAppPermissions!.length,
                          itemBuilder: (context, index) {
                            final permission = _selectedAppPermissions![index];
                            final shortName = permission.split('.').last;
                            return ListTile(
                              contentPadding: EdgeInsets.all(2),
                              dense: true,
                              title: Text(
                                shortName,
                                style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
                              ),
                              subtitle: Text(
                                permission,
                                style: const TextStyle(fontSize: 11),
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis,
                              ),
                            );
                          },
                        ),
                      ),
                  ],
                ],
              ),
            ),
    );
  }

  Widget _buildDetailRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4.0),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 100,
            child: Text('$label:', style: const TextStyle(fontWeight: FontWeight.bold)),
          ),
          Expanded(child: Text(value)),
        ],
      ),
    );
  }

  Widget _buildCompactCheckbox(String label, bool value, ValueChanged<bool?> onChanged) {
    return InkWell(
      onTap: () => onChanged(!value),
      borderRadius: BorderRadius.circular(4),
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            SizedBox(
              height: 24,
              width: 24,
              child: Checkbox(
                value: value,
                onChanged: onChanged,
                materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
              ),
            ),
            const SizedBox(width: 8),
            Text(label, style: Theme.of(context).textTheme.bodyMedium),
          ],
        ),
      ),
    );
  }
}