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

PlatformAndroid

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

example/lib/main.dart

import 'dart:async';
import 'dart:typed_data';

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;
  bool _monitoringBusy = false;
  final List<String> _changeEvents = [];
  final TextEditingController _packageNameController = TextEditingController();
  final TextEditingController _packageNamePrefixController = TextEditingController();
  final Map<String, String> _queryResults = {};
  bool _queryLoading = false;
  bool _iconQueried = false;
  Uint8List? _queriedIconBytes;
  bool _installSourceQueried = false;
  AppInstallSourceInfo? _installSourceInfo;
  bool _permissionsQueried = false;
  bool _includeDetailIcon = true;

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

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

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

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

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

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

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

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

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

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

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

  void _clearPackageQueries() {
    _queryResults.clear();
    _iconQueried = false;
    _queriedIconBytes = null;
    _installSourceQueried = false;
    _installSourceInfo = null;
    _permissionsQueried = false;
    _selectedAppPermissions = null;
  }

  String? _queryPackageName() {
    final packageName = _packageNameController.text.trim();
    if (packageName.isNotEmpty) return packageName;
    setState(() => _statusMessage = 'Enter a package name first');
    return null;
  }

  Future<void> _checkAppState(String label, Future<bool?> Function(String) query) async {
    final packageName = _queryPackageName();
    if (packageName == null) return;
    setState(() {
      _queryLoading = true;
      _statusMessage = 'Checking $label for $packageName...';
    });
    try {
      final result = await query(packageName);
      if (!mounted) return;
      setState(() {
        _queryResults[label] = result == null
            ? 'Not found or not visible'
            : result
            ? 'Yes'
            : 'No';
        _statusMessage = '$label: ${_queryResults[label]} ($packageName)';
      });
    } catch (e) {
      if (!mounted) return;
      setState(() => _statusMessage = 'Error checking $label: $e');
    } finally {
      if (mounted) setState(() => _queryLoading = false);
    }
  }

  Future<void> _getAppIcon() async {
    final packageName = _queryPackageName();
    if (packageName == null) return;
    setState(() {
      _queryLoading = true;
      _statusMessage = 'Loading icon for $packageName...';
    });
    try {
      final bytes = await FlutterDeviceApps.getAppIcon(packageName);
      if (!mounted) return;
      setState(() {
        _iconQueried = true;
        _queriedIconBytes = bytes;
        _statusMessage = bytes == null ? 'Icon not available' : 'Loaded ${bytes.length} icon bytes';
      });
    } catch (e) {
      if (!mounted) return;
      setState(() => _statusMessage = 'Error loading icon: $e');
    } finally {
      if (mounted) setState(() => _queryLoading = false);
    }
  }

  Future<void> _getInstallSourceInfo() async {
    final packageName = _queryPackageName();
    if (packageName == null) return;
    setState(() {
      _queryLoading = true;
      _statusMessage = 'Loading install source for $packageName...';
    });
    try {
      final source = await FlutterDeviceApps.getInstallSourceInfo(packageName);
      if (!mounted) return;
      setState(() {
        _installSourceQueried = true;
        _installSourceInfo = source;
        _statusMessage = source == null ? 'App not found or not visible' : 'Install source loaded';
      });
    } catch (e) {
      if (!mounted) return;
      setState(() => _statusMessage = 'Error getting install source: $e');
    } finally {
      if (mounted) setState(() => _queryLoading = false);
    }
  }

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

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

  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 _formatBytes(int? bytes) {
    if (bytes == null) return 'N/A';
    const units = ['B', 'KB', 'MB', 'GB', 'TB'];
    double value = bytes.toDouble();
    int unitIndex = 0;

    while (value >= 1024 && unitIndex < units.length - 1) {
      value /= 1024;
      unitIndex++;
    }

    final display = unitIndex == 0 ? value.toStringAsFixed(0) : value.toStringAsFixed(2);
    return '$display ${units[unitIndex]} ($bytes bytes)';
  }

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

    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 = {0: 'Auto', 1: 'Internal Only', 2: 'Prefer External'};

    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: _monitoringBusy ? null : _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),

            TextField(
              controller: _packageNamePrefixController,
              enabled: !_loading,
              decoration: const InputDecoration(
                labelText: 'Package name prefix',
                hintText: 'com.google.',
                helperText: 'Leave empty for all packages. Apply with Refresh Apps.',
                border: OutlineInputBorder(),
                isDense: true,
              ),
              onSubmitted: (_) => _loadApps(),
            ),
            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: _loading || _queryLoading
                      ? null
                      : () => _getAppDetails(app.packageName ?? ''),
                  dense: true,
                );
              },
            ),
          ),
        ],
      ),
    );
  }

  bool get _canQueryPackage =>
      !_loading && !_queryLoading && _packageNameController.text.trim().isNotEmpty;

  String _formatBool(bool? value) => value == null
      ? 'N/A'
      : value
      ? 'Yes'
      : 'No';

  Widget _buildPackageQueries() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Package Queries', style: Theme.of(context).textTheme.titleMedium),
        const SizedBox(height: 8),
        TextField(
          controller: _packageNameController,
          enabled: !_loading && !_queryLoading,
          decoration: const InputDecoration(
            labelText: 'Package name',
            hintText: 'com.example.app',
            border: OutlineInputBorder(),
            isDense: true,
          ),
          onChanged: (_) => setState(() {
            _clearPackageQueries();
            _selectedApp = null;
            _selectedAppPermissions = null;
          }),
          onSubmitted: (packageName) {
            if (_canQueryPackage) _getAppDetails(packageName.trim());
          },
        ),
        const SizedBox(height: 8),
        _buildCompactCheckbox('Include Detail Icon', _includeDetailIcon, (value) {
          if (_loading || _queryLoading) return;
          setState(() => _includeDetailIcon = value ?? true);
        }),
        Wrap(
          spacing: 8,
          runSpacing: 8,
          children: [
            ElevatedButton(
              onPressed: _canQueryPackage
                  ? () => _getAppDetails(_packageNameController.text.trim())
                  : null,
              child: const Text('Details'),
            ),
            ElevatedButton(
              onPressed: _canQueryPackage
                  ? () => _checkAppState('Installed', FlutterDeviceApps.isAppInstalled)
                  : null,
              child: const Text('Installed?'),
            ),
            ElevatedButton(
              onPressed: _canQueryPackage
                  ? () => _checkAppState('System App', FlutterDeviceApps.isSystemApp)
                  : null,
              child: const Text('System?'),
            ),
            ElevatedButton(
              onPressed: _canQueryPackage
                  ? () => _checkAppState('Enabled', FlutterDeviceApps.isAppEnabled)
                  : null,
              child: const Text('Enabled?'),
            ),
            ElevatedButton(
              onPressed: _canQueryPackage
                  ? () => _checkAppState('Launchable', FlutterDeviceApps.isAppLaunchable)
                  : null,
              child: const Text('Launchable?'),
            ),
            ElevatedButton(
              onPressed: _canQueryPackage ? _getAppIcon : null,
              child: const Text('Load Icon'),
            ),
            ElevatedButton(
              onPressed: _canQueryPackage ? _getInstallSourceInfo : null,
              child: const Text('Install Source'),
            ),
            ElevatedButton(
              onPressed: _canQueryPackage
                  ? () => _getRequestedPermissions(_packageNameController.text.trim())
                  : null,
              child: const Text('Permissions'),
            ),
          ],
        ),
        if (_queryLoading) ...[const SizedBox(height: 8), const LinearProgressIndicator()],
        for (final result in _queryResults.entries) _buildDetailRow(result.key, result.value),
        if (_iconQueried) ...[
          const SizedBox(height: 8),
          if (_queriedIconBytes == null)
            const Text('Icon not available')
          else ...[
            Image.memory(
              _queriedIconBytes!,
              width: 64,
              height: 64,
              errorBuilder: (context, error, stackTrace) => const Icon(Icons.android, size: 64),
            ),
            Text('Icon: ${_queriedIconBytes!.length} bytes'),
          ],
        ],
        if (_installSourceQueried) ...[
          const SizedBox(height: 16),
          Text('Install Source', style: Theme.of(context).textTheme.titleMedium),
          if (_installSourceInfo == null)
            const Text('App not found or not visible')
          else ...[
            _buildDetailRow(
              'Installer',
              _getStoreDisplayName(_installSourceInfo!.installingPackageName),
            ),
            _buildDetailRow(
              'Installing Package',
              _installSourceInfo!.installingPackageName ?? 'N/A',
            ),
            _buildDetailRow(
              'Initiating Package',
              _installSourceInfo!.initiatingPackageName ?? 'N/A',
            ),
            _buildDetailRow(
              'Originating Package',
              _installSourceInfo!.originatingPackageName ?? 'N/A',
            ),
            _buildDetailRow(
              'Package Source',
              _installSourceInfo!.packageSource?.toString() ?? 'N/A',
            ),
            _buildDetailRow('Update Owner', _installSourceInfo!.updateOwnerPackageName ?? 'N/A'),
          ],
        ],
      ],
    );
  }

  Widget _buildAppDetails() {
    return Card(
      margin: const EdgeInsets.only(left: 4.0, right: 8.0, bottom: 8.0),
      child: SingleChildScrollView(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildPackageQueries(),
            const SizedBox(height: 16),
            if (_selectedApp == null)
              const Text('Select an app or enter its package name to view details')
            else
              _buildSelectedAppDetails(),
            if (_permissionsQueried) _buildPermissions(),
          ],
        ),
      ),
    );
  }

  Widget _buildSelectedAppDetails() {
    return 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('UID', _selectedApp!.uid?.toString() ?? 'N/A'),
        _buildDetailRow('First Install', _formatDateTime(_selectedApp!.firstInstallTime)),
        _buildDetailRow('Last Update', _formatDateTime(_selectedApp!.lastUpdateTime)),
        _buildDetailRow('System App', _formatBool(_selectedApp!.isSystem)),
        _buildDetailRow('Enabled', _formatBool(_selectedApp!.enabled)),
        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 (Requested)',
          _getInstallLocationName(_selectedApp!.installLocation),
        ),
        _buildDetailRow(
          'On External Storage (FLAG_EXTERNAL_STORAGE)',
          _selectedApp!.isOnExternalStorage?.toString() ?? 'N/A',
        ),
        _buildDetailRow('APK Path', _selectedApp!.apkPath ?? 'N/A'),
        _buildDetailRow('APK Size', _formatBytes(_selectedApp!.apkSizeBytes)),
        _buildDetailRow('Data Path', _selectedApp!.dataPath ?? 'N/A'),

        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),
            ),
          ],
        ),
      ],
    );
  }

  Widget _buildPermissions() {
    if (_selectedAppPermissions == null) {
      return const Padding(
        padding: EdgeInsets.only(top: 16),
        child: Text('No permissions info available'),
      );
    }
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        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: _canQueryPackage
                  ? () => _getRequestedPermissions(_packageNameController.text.trim())
                  : null,
              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.center,
        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),
            Flexible(child: Text(label, style: Theme.of(context).textTheme.bodyMedium)),
          ],
        ),
      ),
    );
  }
}