detect_screenshot 1.0.1
detect_screenshot: ^1.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: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? _blockingSupport;
DetectionSupport? _appSwitcherSupport;
DetectionSupport? _shieldSupport;
bool _isRecording = false;
bool _blocked = false;
bool _appSwitcherPrivate = false;
bool _shielded = false;
@override
void initState() {
super.initState();
_screenshotsListener = _screenshotDetector.listenScreenshots((
ScreenshotEvent event,
) {
_append(
'screenshot via ${event.source.name}'
'${event.mediaUri == null ? '' : ' — ${event.mediaUri}'}',
);
});
_recordingListener = _screenshotDetector.listenScreenRecording((
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
.screenshotSupport();
final DetectionSupport recording = await _screenshotDetector
.screenRecordingSupport();
final DetectionSupport blocking = await _screenshotDetector.android
.blockingSupport();
final DetectionSupport switcher = await _screenshotDetector
.appSwitcherPrivacySupport();
final DetectionSupport shield = await _screenshotDetector.ios
.captureShieldSupport();
final bool active = await _screenshotDetector.isScreenRecordingActive();
final bool blocked = await _screenshotDetector.android
.isScreenshotBlocked();
if (!mounted) return;
setState(() {
_screenshotSupport = screenshot;
_recordingSupport = recording;
_blockingSupport = blocking;
_appSwitcherSupport = switcher;
_shieldSupport = shield;
_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'),
),
SwitchListTile(
title: const Text('Block screenshots'),
subtitle: Text('FLAG_SECURE — ${_blockingSupport?.name ?? '...'}'),
value: _blocked,
onChanged: _blockingSupport == DetectionSupport.available
? _toggleBlocking
: null,
),
SwitchListTile(
title: const Text('Capture shield'),
subtitle: Text(
'Screenshots show an overlay — ${_shieldSupport?.name ?? '...'}',
),
value: _shielded,
onChanged: _shieldSupport == DetectionSupport.available
? _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'),
);
}
}