detect_screenshot 1.0.1 copy "detect_screenshot: ^1.0.1" to clipboard
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.

detect_screenshot #

A Flutter plugin for detecting screenshots and screen recordings on Android and iOS. It can block screenshots and screen recording on Android, display a customizable privacy overlay on iOS, and protect sensitive content from appearing in the app switcher on both platforms.

Features #

  • Detect screenshots on Android, using two approaches:
    • Activity.ScreenCaptureCallback on Android 14+.
    • Filtered MediaStore observation on Android 13 and below.
  • Check if the underlying platform supports each feature.
  • Put your own overlay into screenshots and recordings on iOS, without changing the live screen.
  • Detect screenshots and recording on iOS and Android 15+, as a separate stream.
  • Real-time events over an EventChannel, starting and stopping with your listeners.
  • Report whether a device can detect at all, so a silent stream is never ambiguous.
  • Block screenshots, recording and casting on Android with FLAG_SECURE.
  • Hide the app in the task switcher on both platforms.

Support #

Feature iOS Android 15+ Android 14 Android 13 Android 7–12L
Screenshot detection grant needed grant needed
Screen recording detection
Block capture
Capture shield (overlay)
App switcher privacy

Under the hood: userDidTakeScreenshotNotification and the scene's sceneCaptureState on iOS; Activity.ScreenCaptureCallback (14+), WindowManager.addScreenRecordingCallback (15+), FLAG_SECURE, and setRecentsScreenshotEnabled (13+) on Android. Below Android 14 screenshots are inferred from MediaStore, since no screenshot API existed before then.

Install #

dependencies:
  detect_screenshot: ^1.0.0

Init #

final ScreenshotDetector _screenshotDetector = ScreenshotDetector.instance;

Check Compatibility #

Anything that works on both sits directly on the screenshotDetector main class. Anything that only exists on one platform lives under .android or .ios, so it is obvious at the call site which is which:

await _screenshotDetector.screenshotSupport(); // both
await _screenshotDetector.android.setScreenshotBlocked(blocked: true); // Android only
await _screenshotDetector.ios.setCaptureShield(enabled: true); // iOS only

Calling one on the wrong platform is a harmless no-op, so no Platform.isIOS guards are needed. Use the matching *Support() method when you need to know whether it will actually do something.

A quiet stream means either "nothing was captured" or "this device can't tell you". Those are very different numbers, and on Android the second is common enough to skew them.

Every feature has a matching check, all returning DetectionSupport:

switch (await _screenshotDetector.screenshotSupport()) {
  case DetectionSupport.available:
    break;
  case DetectionSupport.permissionRequired:
    await _screenshotDetector.requestMediaPermission();
  case DetectionSupport.unsupported:
    break;
}
Method Answers
screenshotSupport() Can screenshots be detected here?
screenRecordingSupport() Can screen recording be detected here?
appSwitcherPrivacySupport() Can the task switcher be covered?
android.blockingSupport() Does FLAG_SECURE apply here?
ios.captureShieldSupport() Can captures be given an overlay?

DetectionSupport has three values, and the middle one is the reason this exists:

  • available — it will work
  • permissionRequired — the OS can do it, the app is missing a runtime grant
  • unsupported — the OS has no API for it, and no permission will change that

requestMediaPermission() returns true without prompting wherever the permission is irrelevant, so you don't need to branch on platform first.

Use these to drive the UI too. A "block screenshots" toggle should be disabled when android.blockingSupport() reports unsupported, rather than shown and silently doing nothing.

Detect #

Each detector is independent and runs only while something is listening.

final _screenshotDetector = ScreenshotDetector.instance;

DetectionListener<ScreenshotEvent>? _listener;

@override
void initState() {
  super.initState();
  _listener = _screenshotDetector.listenScreenshots((event) {
    analytics.log('screenshot', {'source': event.source.name});
  });
}

@override
void dispose() {
  _listener?.dispose();
  super.dispose();
}
  • listenScreenshots(...) and listenScreenRecording(...) return a handle with dispose().
  • screenshotDetector.screenshots and screenshotDetector.screenRecording expose the raw streams if you want where, take and friends.
  • Recording reports state changes, not one-shot events. The first event describes the state at subscription time, so a listener attached mid-recording still learns about it.
  • screenshotDetector.isScreenRecordingActive() asks once, without subscribing.

Log the source #

Every ScreenshotEvent carries the source that produced it. Coverage varies by OS version and permission state.

Block #

await _screenshotDetector.android.setScreenshotBlocked(blocked: true);

Sets FLAG_SECURE, which stops screenshots, screen recording, the recents thumbnail, and casting to non-secure displays in one go. Enforced by the system, not by your app.

  • Android only. iOS exposes nothing equivalent, and android.blockingSupport() returns unsupported there.
  • Whole window. You can't protect a single widget, so toggle it as routes come and go.
  • Careful with platform views. FLAG_SECURE is known to black out webviews and some SurfaceView players. Test on a device before enabling it on a screen with a video player or embedded browser.
  • android.isScreenshotBlocked() reads the window flag back, so it stays truthful if your app sets the flag itself.

Capture shield #

iOS has no FLAG_SECURE. What it does have is a way to keep the app's own pixels out of a capture and leave something else in their place — the approach messaging apps use for protected media.

final data = await rootBundle.load('assets/blocked.png');

await _screenshotDetector.ios.setCaptureShield(
  enabled: true,
  placeholderPng: data.buffer.asUint8List(),
);

The live screen never changes. Users see the real UI throughout; only the saved screenshot or recording contains your overlay. That is what makes it feel seamless rather than like a blackout.

  • iOS only. ios.captureShieldSupport() returns unsupported on Android, where android.setScreenshotBlocked() is the right tool.
  • Covers screenshots and recordings, and needs no detection at all.
  • backgroundColor fills whatever the image doesn't cover. Defaults to black.

Building the overlay from a widget #

captureShieldOverlay renders a widget to PNG bytes, so the overlay can be authored in Flutter with your own fonts and theme. This is the example app's version, an asset above a line of text:

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,
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

await _screenshotDetector.ios.setCaptureShield(
  enabled: true,
  placeholderPng: await _renderOverlay(),
);

Note the RawImage. The overlay renders outside the widget tree in one synchronous pass, so anything resolving asynchronously won't have arrived when the frame paints — an Image comes out blank. So make sure to decode the asset up front and hand over the decoded ui.Image.

Render once and keep the bytes; re-rendering on every toggle is wasted work.

The one caveat that matters #

This is the only feature here not built on a public API. Apple offers no supported way to keep content out of a capture, so the shield relies on the internals of a secure UITextField — the app window's layer is moved inside the private canvas that the render server omits, and a second window below shows through in its place.

A future iOS that reshapes that view hierarchy will silently stop it working. It fails open: captures would show the real content again rather than the app breaking. Don't present it to users as a guarantee, and check ios.captureShieldSupport() before offering it as a setting.

App switcher privacy #

await _screenshotDetector.setAppSwitcherPrivacy(enabled: true);

await _screenshotDetector.setAppSwitcherPrivacy(
  enabled: true,
  style: AppSwitcherStyle.color,
  color: Colors.black,
);

Covers the UI in the multitasking view — usually the leak that matters most, and supported on both platforms.

  • iOS places a native overlay before the system takes its snapshot. That runs in UIKit deliberately: Flutter stops scheduling frames as the app backgrounds, so a widget swap loses the race.
  • Android uses setRecentsScreenshotEnabled and draws its own placeholder, so style and color are ignored. Needs Android 13+.
  • On Android 12L and below, android.setScreenshotBlocked(blocked: true) also hides the recents card, at the cost of blocking screenshots entirely.

Hide content while recording #

Note: this was developed as a solution before the iOS overlay override implementation. Just kept it here if someone needs it.

Blanks one part of the tree while the screen is being recorded, leaving the rest of the UI alone.

HideWhileCaptured(
  replacement: const Text('Hidden while recording'),
  child: CardNumber(),
)

Read the next paragraph before reaching for this. It is the narrowest tool here, and on most screens one of the other two is a better fit.

When to use it #

Only two situations really call for it:

  • Protecting a single widget on Android 15+. FLAG_SECURE covers the entire window and the capture shield is iOS-only, so this is the one way to keep a card number out of a recording while the rest of the screen records normally.
  • When the user should notice. The capture shield is invisible — the person recording sees the real UI and only finds the overlay afterwards. This changes the live screen, so it signals "this bit is protected" while they are recording. Different intent, occasionally the one you want.

Everywhere else, prefer android.setScreenshotBlocked() or ios.setCaptureShield().

What it cannot do #

  • It depends on recording detection, and inherits every weakness of it. On iOS that detection is unreliable on recent versions (see Limitations), so treat this as best-effort there rather than something to rely on.
  • Android 14 and below have no recording detection at all, so the child simply stays visible.
  • Screenshots are never caught. They are instantaneous; nothing reacts in time.
  • There is an exposure window between a recording starting and the first blanked frame.

Options: any widget works as replacement. Left out, the child is hidden but keeps the space it occupied, so the surrounding layout does not shift. enabled: false keeps the child visible without moving the widget in and out of the tree.

API reference #

Everything hangs off ScreenshotDetector.instance.

Streams and listeners #

Member Returns Notes
screenshots Stream<ScreenshotEvent> Broadcast. One event per screenshot.
screenRecording Stream<ScreenRecordingEvent> Broadcast. Emits on change, plus once at subscription.
listenScreenshots(cb) DetectionListener<ScreenshotEvent> Same stream, with a dispose() handle.
listenScreenRecording(cb) DetectionListener<ScreenRecordingEvent> Same stream, with a dispose() handle.
DetectionListener.dispose() Future<void> Cancels. The native detector stops once nothing is listening.

Queries #

Method Returns Meaning of the result
screenshotSupport() Future<DetectionSupport> available, permissionRequired, or unsupported.
screenRecordingSupport() Future<DetectionSupport> unsupported below Android 15.
isScreenRecordingActive() Future<bool> Capture state right now. False where unsupported.
requestMediaPermission() Future<bool> True if the grant is now held. True without prompting where irrelevant.
appSwitcherPrivacySupport() Future<DetectionSupport> unsupported below Android 13.
android.isScreenshotBlocked() Future<bool> Reads FLAG_SECURE off the window. False on iOS.
android.blockingSupport() Future<DetectionSupport> unsupported on iOS.
ios.captureShieldSupport() Future<DetectionSupport> unsupported on Android.

Actions #

All return Future<void> and complete once the platform has applied the change. None throw on the wrong platform — they no-op, and the matching *Support() tells you which case you are in.

Method Parameters
setAppSwitcherPrivacy(...) enabled, style (blur | color), color
android.setScreenshotBlocked(...) blocked
ios.setCaptureShield(...) enabled, placeholderPng, backgroundColor

Event models #

Type Fields
ScreenshotEvent source (ScreenshotSource), timestampMs (int), mediaUri (String?)
ScreenRecordingEvent isRecording (bool), timestampMs (int)

mediaUri is only populated for ScreenshotSource.mediaStore. The system callbacks deliberately withhold any reference to the captured image, so it is null on iOS and on Android 14+.

Enums #

Type Values
ScreenshotSource systemCallback (Android 14+), mediaStore (Android 13 and below), systemNotification (iOS)
DetectionSupport available, permissionRequired, unsupported
AppSwitcherStyle blur, color

Helpers #

Member Returns Notes
captureShieldOverlay(widget, {size, pixelRatio, textDirection}) Future<Uint8List> PNG bytes. Throws StateError on an empty size or a failed encode.
HideWhileCaptured widget child, replacement, enabled.

Permissions #

Declared by the plugin, nothing for you to add:

  • DETECT_SCREEN_CAPTURE and DETECT_SCREEN_RECORDING — install-time, no prompt, no Play Console declaration.
  • READ_EXTERNAL_STORAGE capped at maxSdkVersion="32" for the MediaStore fallback. It's a runtime permission, so declaring it isn't holding it — call requestMediaPermission().

Android 13 is left to you. That band needs READ_MEDIA_IMAGES, which falls under Google Play's Photo and Video Permissions policy and needs a declaration naming a qualifying core use case. Screenshot analytics does not fall under this. If your app already declares it for a real photo feature, Android 13 detection starts working with no further change:

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

To drop the storage permission and keep only the Android 14+ path:

<uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    tools:node="remove" />

iOS needs nothing.

How MediaStore events are filtered (below Android 14) #

Below Android 14, ContentObserver fires for every image written to shared storage — downloads, camera captures, cloud sync. Four rules turn that into a usable signal, each fixing a way the naive version goes wrong:

  • Directory, not file name. A row counts when it lands in a Screenshots directory. Display names get localised (Capture d'écran, スクリーンショット); directory names on disk don't, so name matching silently under-reports every non-English device.
  • Recency. A media rescan or a restored backup re-notifies every image on the device. Without a DATE_ADDED window that's one phantom event per stored screenshot.
  • Row identity. Observers fire more than once per insert. Events are keyed on row id, so one screenshot reports once while two taken back to back still report twice.
  • Foreground only. Nothing is reported while your app is backgrounded, where a write to shared storage has nothing to do with your UI.

What's different in this library #

  • Uses the real platform callbacks where they exist, instead of applying one MediaStore strategy to every OS version.
  • Filters MediaStore events rather than forwarding raw observer notifications.
  • Classifies by directory, so it behaves the same in every locale.
  • Reports what a device can't do, instead of going quiet and letting you assume.
  • Tags every event with its detection tier, so counts stay comparable.
  • Covers screen recording on Android, not just iOS.
  • Can use custom widget as the iOS screen recording blocker overlay.
  • Puts a real overlay into iOS captures, rather than only detecting them after the fact.
  • Ships no permission that carries Play review obligations.

Limitations #

  • The Android 14+ callback misses adb shell screencap and some OEM assistant gestures.
  • On iOS, capture state is also true for AirPlay mirroring and wired capture; iOS doesn't distinguish those from the built-in recorder.
  • Android has no screen recording detection before 15, and no permission changes that.
  • Below Android 14, detection depends on a runtime grant your app may never have been given.
  • The capture shield relies on undocumented UIKit internals and can stop working on a future iOS. It fails open, so captures would show real content rather than the app breaking.
  • iOS screen recording detection is unreliable on recent versions. Both push mechanisms Apple offers were measured not firing on iOS 26.6 even though the state itself reads correctly, so the plugin also polls every 500ms while a listener is attached and emits only on change. If you care about what lands in the file rather than reacting live, the capture shield is the sturdier choice.

Example #

You can find a full working example in the example/ directory of the repository.

Contribution #

Feel free to fork this repository and submit pull requests. Please follow best coding practices and include tests for any new features or fixes.

License #

This plugin is licensed under the MIT License. See the LICENSE file for more details.

0
likes
0
points
191
downloads

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

unknown (license)

Dependencies

flutter, meta

More

Packages that depend on detect_screenshot

Packages that implement detect_screenshot