detect_screenshot 2.0.1 copy "detect_screenshot: ^2.0.1" to clipboard
detect_screenshot: ^2.0.1 copied to clipboard

Detect screenshots and screen recording on Android and iOS, block capture with FLAG_SECURE, and hide sensitive content in the app switcher.

example/lib/main.dart

import 'dart:async';
import 'dart:io' show Platform;
import 'dart:ui' as ui;

import 'package:detect_screenshot/detect_screenshot.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const ExampleApp());
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'detect_screenshot',
      theme: ThemeData(colorSchemeSeed: Colors.indigo),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final ScreenshotDetector _screenshotDetector = ScreenshotDetector.instance;
  final List<String> _log = <String>[];

  DetectionListener<ScreenshotEvent>? _screenshotsListener;
  DetectionListener<ScreenRecordingEvent>? _recordingListener;

  DetectionSupport? _screenshotSupport;
  DetectionSupport? _recordingSupport;
  DetectionSupport? _appSwitcherSupport;
  bool _isRecording = false;
  bool _blocked = false;
  bool _appSwitcherPrivate = false;
  bool _shielded = false;

  @override
  void initState() {
    super.initState();
    _screenshotsListener = _screenshotDetector.listenForScreenshots((
      ScreenshotEvent event,
    ) {
      _append(
        'screenshot via ${event.source.name}'
        '${event.mediaUri == null ? '' : ' — ${event.mediaUri}'}',
      );
    });
    _recordingListener = _screenshotDetector.listenForScreenRecordings((
      ScreenRecordingEvent event,
    ) {
      setState(() => _isRecording = event.isRecording);
      _append('recording ${event.isRecording ? 'started' : 'stopped'}');
    });
    unawaited(_refreshSupport());
  }

  @override
  void dispose() {
    unawaited(_screenshotsListener?.dispose());
    unawaited(_recordingListener?.dispose());
    super.dispose();
  }

  Future<void> _refreshSupport() async {
    final DetectionSupport screenshot = await _screenshotDetector
        .checkScreenshotListenerSupport();
    final DetectionSupport recording = await _screenshotDetector
        .checkScreenRecordListenSupport();
    final DetectionSupport switcher = await _screenshotDetector
        .checkAppSwitcherPrivacySupport();
    final bool active = await _screenshotDetector.isScreenRecordingActive();
    final bool blocked = await _screenshotDetector.android
        .isScreenshotBlocked();
    if (!mounted) return;
    setState(() {
      _screenshotSupport = screenshot;
      _recordingSupport = recording;
      _appSwitcherSupport = switcher;
      _isRecording = active;
      _blocked = blocked;
    });
  }

  Future<void> _toggleShield(bool value) async {
    Uint8List? overlay;
    if (value) {
      overlay = await _renderOverlay();
    }
    await _screenshotDetector.ios.setCaptureShield(
      enabled: value,
      placeholderPng: overlay,
    );
    if (!mounted) return;
    setState(() => _shielded = value);
    _append('capture shield ${value ? 'on' : 'off'}');
  }

  /// Builds the placeholder that captures will show: the asset above the text.
  ///
  /// The image is decoded up front and handed over as a [RawImage]. The overlay
  /// is rendered outside the widget tree, where an [Image] would still be
  /// resolving its asset when the frame is painted, and would come out blank.
  Future<Uint8List> _renderOverlay() async {
    final ByteData data = await rootBundle.load('assets/blocked.png');
    final ui.Codec codec = await ui.instantiateImageCodec(
      data.buffer.asUint8List(),
      targetWidth: 480,
    );
    final ui.FrameInfo frame = await codec.getNextFrame();

    return captureShieldOverlay(
      ColoredBox(
        color: const Color(0xFF1A1A2E),
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              RawImage(image: frame.image, width: 180, fit: BoxFit.contain),
              const SizedBox(height: 28),
              const Text(
                'Screenshots of this app \n is not permitted',
                textAlign: TextAlign.center,
                style: TextStyle(
                  color: Color(0xFFFFFFFF),
                  fontSize: 28,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  /// Polls the platform directly, bypassing the event stream. If this reports
  /// true during a recording while the stream stayed quiet, the state read is
  /// fine and only the change callback is missing.
  Future<void> _recheckCaptureState() async {
    final bool active = await _screenshotDetector.isScreenRecordingActive();
    if (!mounted) return;
    setState(() => _isRecording = active);
    _append('polled isScreenRecordingActive = $active');
  }

  Future<void> _toggleBlocking(bool value) async {
    await _screenshotDetector.android.setScreenshotBlocked(blocked: value);
    final bool blocked = await _screenshotDetector.android
        .isScreenshotBlocked();
    if (!mounted) return;
    setState(() => _blocked = blocked);
    _append('screenshot blocking ${blocked ? 'on' : 'off'}');
  }

  Future<void> _toggleAppSwitcherPrivacy(bool value) async {
    await _screenshotDetector.setAppSwitcherPrivacy(enabled: value);
    if (!mounted) return;
    setState(() => _appSwitcherPrivate = value);
    _append('app switcher privacy ${value ? 'on' : 'off'}');
  }

  Future<void> _requestPermission() async {
    final bool granted = await _screenshotDetector.requestMediaPermission();
    _append('media permission ${granted ? 'granted' : 'denied'}');
    await _refreshSupport();
  }

  void _append(String line) {
    if (!mounted) return;
    final DateTime now = DateTime.now();
    final String stamp =
        '${now.hour.toString().padLeft(2, '0')}:'
        '${now.minute.toString().padLeft(2, '0')}:'
        '${now.second.toString().padLeft(2, '0')}';
    setState(() => _log.insert(0, '$stamp  $line'));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('detect_screenshot')),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          _SupportRow(label: 'Screenshots', support: _screenshotSupport),
          _SupportRow(label: 'Screen recording', support: _recordingSupport),
          ListTile(
            title: const Text('Currently captured'),
            trailing: Text(_isRecording ? 'yes' : 'no'),
          ),
          // Both of these are single-platform features, so a plain platform
          // check is what decides whether the control does anything.
          SwitchListTile(
            title: const Text('Block screenshots'),
            subtitle: const Text('FLAG_SECURE — Android only'),
            value: _blocked,
            onChanged: Platform.isAndroid ? _toggleBlocking : null,
          ),
          SwitchListTile(
            title: const Text('Capture shield'),
            subtitle: const Text('Screenshots show an overlay — iOS only'),
            value: _shielded,
            onChanged: Platform.isIOS ? _toggleShield : null,
          ),
          SwitchListTile(
            title: const Text('Hide in app switcher'),
            subtitle: Text(_appSwitcherSupport?.name ?? '...'),
            value: _appSwitcherPrivate,
            onChanged: _appSwitcherSupport == DetectionSupport.available
                ? _toggleAppSwitcherPrivacy
                : null,
          ),
          const Padding(
            padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            child: HideWhileCaptured(
              replacement: Text('Hidden while the screen is captured'),
              child: Text('Sensitive content — try recording the screen'),
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Row(
              children: <Widget>[
                Expanded(
                  child: FilledButton.tonal(
                    onPressed: _requestPermission,
                    child: const Text('Request permission'),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: FilledButton.tonal(
                    onPressed: _recheckCaptureState,
                    child: const Text('Re-check capture'),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: OutlinedButton(
                    onPressed: () => setState(_log.clear),
                    child: const Text('Clear log'),
                  ),
                ),
              ],
            ),
          ),
          const Divider(height: 32),
          Expanded(
            child: _log.isEmpty
                ? const Center(
                    child: Text('Take a screenshot or start a recording.'),
                  )
                : ListView.builder(
                    itemCount: _log.length,
                    itemBuilder: (BuildContext context, int index) => ListTile(
                      dense: true,
                      title: Text(
                        _log[index],
                        style: const TextStyle(fontFamily: 'monospace'),
                      ),
                    ),
                  ),
          ),
        ],
      ),
    );
  }
}

class _SupportRow extends StatelessWidget {
  const _SupportRow({required this.label, required this.support});

  final String label;
  final DetectionSupport? support;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(label),
      trailing: Text(support?.name ?? 'checking'),
    );
  }
}
0
likes
160
points
191
downloads

Documentation

API reference

Publisher

verified publisherkasuncreations.com

Weekly Downloads

Detect screenshots and screen recording on Android and iOS, block capture with FLAG_SECURE, and hide sensitive content in the app switcher.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, meta

More

Packages that depend on detect_screenshot

Packages that implement detect_screenshot