Skip to main content

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

PlatformAndroid

Inspect installed Android apps and APK files - signing certificates, permissions, exported components, flags and installer - with a permission catalog.

example/lib/main.dart

import 'dart:typed_data';

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

void main() => runApp(const ExampleApp());

final _inspector = ApkInspector();

/// Lists installed apps with lazy icons and opens a details screen.
class ExampleApp extends StatelessWidget {
  /// Creates the example app.
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'apk_inspector',
        theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),
        home: const AppListPage(),
      );
}

/// Pages through installed apps.
class AppListPage extends StatefulWidget {
  /// Creates the list page.
  const AppListPage({super.key});

  @override
  State<AppListPage> createState() => _AppListPageState();
}

class _AppListPageState extends State<AppListPage> {
  final _apps = <AppSummary>[];
  bool _includeSystem = false;
  bool _loading = false;
  int? _next = 0;
  String? _error;

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

  Future<void> _reload() async {
    setState(() {
      _apps.clear();
      _next = 0;
    });
    await _loadMore();
  }

  Future<void> _loadMore() async {
    final offset = _next;
    if (_loading || offset == null) return;
    setState(() => _loading = true);
    try {
      final page = await _inspector.installedAppsPage(
        offset: offset,
        limit: 40,
        includeSystem: _includeSystem,
      );
      setState(() {
        _apps.addAll(page.apps);
        _next = page.nextOffset;
        _error = null;
      });
    } on ApkInspectorException catch (e) {
      setState(() => _error = e.toString());
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  Future<void> _openApkFile() async {
    final controller = TextEditingController();
    final path = await showDialog<String>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Inspect APK file'),
        content: TextField(
          controller: controller,
          decoration:
              const InputDecoration(hintText: '/sdcard/Download/app.apk'),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, controller.text),
            child: const Text('Inspect'),
          ),
        ],
      ),
    );
    if (path == null || path.isEmpty || !mounted) return;
    await Navigator.push(
      context,
      MaterialPageRoute<void>(
        builder: (_) =>
            DetailsPage(load: () => _inspector.inspectApkFile(path)),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Apps (${_apps.length})'),
        actions: [
          IconButton(
            tooltip: 'Inspect APK file',
            icon: const Icon(Icons.file_open),
            onPressed: _openApkFile,
          ),
          IconButton(
            tooltip: 'Toggle system apps',
            icon: Icon(_includeSystem ? Icons.android : Icons.apps),
            onPressed: () {
              _includeSystem = !_includeSystem;
              _reload();
            },
          ),
        ],
      ),
      body: _error != null
          ? Center(child: Text(_error!))
          : NotificationListener<ScrollNotification>(
              onNotification: (n) {
                if (n.metrics.extentAfter < 400) _loadMore();
                return false;
              },
              child: ListView.builder(
                itemCount: _apps.length + (_next != null ? 1 : 0),
                itemBuilder: (context, i) {
                  if (i >= _apps.length) {
                    return const Padding(
                      padding: EdgeInsets.all(16),
                      child: Center(child: CircularProgressIndicator()),
                    );
                  }
                  final app = _apps[i];
                  return ListTile(
                    leading: _LazyIcon(app.packageName),
                    title: Text(app.label),
                    subtitle: Text(app.packageName),
                    trailing: app.isSystem ? const Text('system') : null,
                    onTap: () => Navigator.push(
                      context,
                      MaterialPageRoute<void>(
                        builder: (_) => DetailsPage(
                          load: () => _inspector.appDetails(app.packageName),
                        ),
                      ),
                    ),
                  );
                },
              ),
            ),
    );
  }
}

class _LazyIcon extends StatefulWidget {
  const _LazyIcon(this.packageName);
  final String packageName;

  @override
  State<_LazyIcon> createState() => _LazyIconState();
}

class _LazyIconState extends State<_LazyIcon> {
  late final Future<Uint8List?> _icon = _inspector.iconOf(widget.packageName);

  @override
  Widget build(BuildContext context) => SizedBox.square(
        dimension: 40,
        child: FutureBuilder<Uint8List?>(
          future: _icon,
          builder: (context, s) => s.data == null
              ? const Icon(Icons.android)
              : Image.memory(s.data!, gaplessPlayback: true),
        ),
      );
}

/// Shows [AppDetails] for an installed app or an APK file.
class DetailsPage extends StatelessWidget {
  /// Creates the page; [load] fetches the details.
  const DetailsPage({super.key, required this.load});

  /// Loads the details to show.
  final Future<AppDetails> Function() load;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Details')),
      body: FutureBuilder<AppDetails>(
        future: load(),
        builder: (context, s) {
          if (s.hasError) return Center(child: Text('${s.error}'));
          final d = s.data;
          if (d == null) {
            return const Center(child: CircularProgressIndicator());
          }
          final risk = d.riskScore;
          final cert = d.signing?.currentSigners.firstOrNull;
          return ListView(
            padding: const EdgeInsets.all(16),
            children: [
              Text(d.label, style: Theme.of(context).textTheme.headlineSmall),
              Text(
                  '${d.packageName}  ${d.versionName ?? ''} (${d.versionCode})'),
              Text('SDK min ${d.minSdk} / target ${d.targetSdk} / '
                  'compile ${d.compileSdk}'),
              Text('Installer: ${d.installSource?.installer ?? 'unknown'}'),
              if (d.signingSchemes != null)
                Text(
                    'Schemes: ${d.signingSchemes!.map((s) => s.name).join(', ')}'),
              const Divider(),
              Text('Risk score ${risk.score}/100'),
              for (final f in risk.factors) Text('  $f'),
              const Divider(),
              if (cert != null) ...[
                Text('Signer: ${cert.subject}'),
                SelectableText('SHA-256: ${cert.formattedSha256}'),
                Text('Past signers: ${d.signing!.pastSigners.length}'),
              ],
              const Divider(),
              Text('Permissions (${d.permissions.length})'),
              for (final p in d.permissions)
                ListTile(
                  dense: true,
                  title: Text(p.name),
                  subtitle: Text(
                    PermissionCatalog.describe(p.name,
                                protectionLevel: p.protectionLevel?.raw)
                            .description ??
                        '',
                  ),
                  trailing: Text(p.risk.name),
                ),
              const Divider(),
              Text('Exported components (${d.exportedComponents.length})'),
              for (final c in d.exportedComponents)
                Text('${c.kind.name}: ${c.name}'
                    '${c.isProtected ? '' : '  [no permission]'}'),
            ],
          );
        },
      ),
    );
  }
}
0
likes
160
points
390
downloads

Documentation

API reference

Publisher

verified publisherabdeldjalilchougui.biz

Weekly Downloads

Inspect installed Android apps and APK files - signing certificates, permissions, exported components, flags and installer - with a permission catalog.

Homepage
Repository (GitHub)
View/report issues

Topics

#android #apk #permissions #security #plugin

License

MIT (license)

Dependencies

flutter

More

Packages that depend on apk_inspector

Packages that implement apk_inspector