apk_inspector 0.1.0
apk_inspector: ^0.1.0 copied to clipboard
Inspect installed Android apps and APK files - signing certificates, permissions, exported components, flags and installer - with a permission catalog.
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]'}'),
],
);
},
),
);
}
}