flutter_device_protection 1.0.1
flutter_device_protection: ^1.0.1 copied to clipboard
One scan() call blocks screenshots, screen recording and app-switcher previews, and reports root, jailbreak, emulator, debugger, hooking frameworks and VPN.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_device_protection/flutter_device_protection.dart';
void main() {
runApp(const DeviceProtectionExampleApp());
}
class DeviceProtectionExampleApp extends StatelessWidget {
const DeviceProtectionExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Device Protection',
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
home: const DeviceProtectionPage(),
);
}
}
class DeviceProtectionPage extends StatefulWidget {
const DeviceProtectionPage({super.key});
@override
State<DeviceProtectionPage> createState() => _DeviceProtectionPageState();
}
class _DeviceProtectionPageState extends State<DeviceProtectionPage> {
// The protections to request on the next scan.
bool _blockScreenshots = true;
bool _blockScreenRecording = true;
bool _blockBackgroundSnapshot = true;
SecurityReport? _report;
String? _error;
bool _scanning = false;
@override
void initState() {
super.initState();
// Scan as soon as the app starts, so sensitive UI is protected before it
// is ever drawn.
WidgetsBinding.instance.addPostFrameCallback((_) => _scan());
}
/// The one call an application needs.
Future<void> _scan() async {
setState(() {
_scanning = true;
_error = null;
});
try {
final report = await FlutterDeviceProtection.scan(
blockScreenshots: _blockScreenshots,
blockScreenRecording: _blockScreenRecording,
blockBackgroundSnapshot: _blockBackgroundSnapshot,
);
if (!mounted) return;
setState(() {
_report = report;
_scanning = false;
});
if (report.isAtLeast(RiskLevel.high)) {
_showBlockingDialog(report);
}
} catch (error) {
if (!mounted) return;
setState(() {
_error = error.toString();
_scanning = false;
});
}
}
void _showBlockingDialog(SecurityReport report) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Device not trusted'),
content: Text(
'Risk ${report.overallRisk.level.name} '
'(${report.overallRisk.score}/100).\n\n'
'Detected: ${report.threats.map((t) => t.name).join(', ')}',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Understood'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final report = _report;
return Scaffold(
appBar: AppBar(title: const Text('Flutter Device Protection')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_SectionTitle('Protection'),
const Text(
'Flags passed to scan(). Screenshots and recording are one switch '
'on Android (FLAG_SECURE) and two on iOS.',
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Block screenshots'),
value: _blockScreenshots,
onChanged: (value) => setState(() => _blockScreenshots = value),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Block screen recording'),
value: _blockScreenRecording,
onChanged: (value) => setState(() => _blockScreenRecording = value),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Block app-switcher preview'),
value: _blockBackgroundSnapshot,
onChanged: (value) =>
setState(() => _blockBackgroundSnapshot = value),
),
const SizedBox(height: 16),
FilledButton.icon(
onPressed: _scanning ? null : _scan,
icon: _scanning
? const SizedBox.square(
dimension: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.security),
label: Text(_scanning ? 'Scanning...' : 'Apply and scan'),
),
const SizedBox(height: 24),
if (_error != null)
_Card(
child: Text(
'Scan failed: $_error',
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
)
else if (report != null) ...[
_SectionTitle('Result'),
_RiskCard(report: report),
const SizedBox(height: 16),
_SectionTitle('Checks'),
for (final entry in report.checks.entries)
_CheckTile(threat: entry.key, result: entry.value),
const SizedBox(height: 16),
_SectionTitle('Report'),
const Text(
'report.toSummary() for logs, report.toMap() to send to a '
'backend.',
),
const SizedBox(height: 8),
_Card(
child: SelectableText(
report.toSummary(),
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
),
],
const SizedBox(height: 24),
_Card(
child: Column(
children: const [
Icon(Icons.lock, size: 40),
SizedBox(height: 12),
Text(
'Sensitive content',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
'With protection on, this should not appear in a '
'screenshot, a screen recording, or the app-switcher '
'preview.',
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 32),
],
),
);
}
}
class _RiskCard extends StatelessWidget {
const _RiskCard({required this.report});
final SecurityReport report;
@override
Widget build(BuildContext context) {
final risk = report.overallRisk;
final colour = switch (risk.level) {
RiskLevel.low => Colors.green,
RiskLevel.medium => Colors.orange,
RiskLevel.high => Colors.deepOrange,
RiskLevel.critical => Colors.red,
};
return _Card(
child: Row(
children: [
Icon(
report.hasThreats ? Icons.warning_amber : Icons.verified_user,
color: colour,
size: 36,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${risk.level.name.toUpperCase()} - ${risk.score}/100',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colour,
),
),
const SizedBox(height: 4),
Text(
report.hasThreats
? 'Triggered: '
'${report.threats.map((t) => t.name).join(', ')}'
: 'No threats detected',
),
const SizedBox(height: 4),
Text(
'Scanned at ${report.scannedAt.toIso8601String()}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
],
),
);
}
}
class _CheckTile extends StatelessWidget {
const _CheckTile({required this.threat, required this.result});
final SecurityThreat threat;
final SecurityCheckResult result;
@override
Widget build(BuildContext context) {
final (icon, colour) = switch (result) {
SecurityCheckResult(supported: false) => (
Icons.remove_circle_outline,
Colors.grey,
),
SecurityCheckResult(detected: true) => (Icons.error_outline, Colors.red),
_ => (Icons.check_circle_outline, Colors.green),
};
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(icon, color: colour),
title: Text(threat.name),
subtitle: Text(result.toString()),
trailing: result.supported && result.detected
? Text('${result.confidence}')
: null,
);
}
}
class _SectionTitle extends StatelessWidget {
const _SectionTitle(this.text);
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(text, style: Theme.of(context).textTheme.titleLarge),
);
}
}
class _Card extends StatelessWidget {
const _Card({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Theme.of(context).dividerColor),
),
child: child,
);
}
}