flutter_device_protection 1.0.1 copy "flutter_device_protection: ^1.0.1" to clipboard
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,
    );
  }
}
2
likes
150
points
122
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

One scan() call blocks screenshots, screen recording and app-switcher previews, and reports root, jailbreak, emulator, debugger, hooking frameworks and VPN.

Repository (GitHub)
View/report issues

Topics

#security #jailbreak #root-detection #screenshot #privacy

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on flutter_device_protection

Packages that implement flutter_device_protection