screenshot_shield 0.2.0
screenshot_shield: ^0.2.0 copied to clipboard
Detect screenshots and screen recording, blank screen captures of a whole screen or a sensitive region, and hide your app in the app switcher.
0.2.0 #
Fixes #
- Android: screenshot detection could crash the host app on Android 7-9: the media
store query asked for
RELATIVE_PATH, which only exists from Android 10, and only permission errors were caught. The query now adapts to the Android version, never throws, runs off the main thread, and reports each screenshot once. - Android:
backgroundBlurleft the app-switcher thumbnail readable with Flutter's default rendering. Android 13+ now disables the thumbnail outright (screenshots and their detection are unaffected, and dialogs no longer flash a blur); older versions use a blur or an opaque cover. - Android:
preventCaptureis re-applied to every activity that attaches, and is remembered when requested before one exists. - iOS:
preventCaptureis re-applied when UIKit undoes it - after a full-screen modal is dismissed, when the app becomes active - and is installed once a window exists when requested earlier. A replaced root view is detected instead of blanking the app. - iOS: the privacy manifest is now bundled (SwiftPM and CocoaPods). Recording state is
read per scene (
sceneCaptureStateon iOS 17+) instead of from the deprecatedUIScreen.main. - Windows: the plugin did not compile and registered under a name the generated
registrant could not find; both are fixed and CI now builds it.
preventCapturenow works, viaSetWindowDisplayAffinityon the top-level window (resolved when used, since the view is not yet parented when the plugin registers). The window is no longer hidden whenever it loses focus;backgroundBlurinstead opts in to icon-only taskbar and Alt+Tab previews. Recording detection samples continuously (it ran once) and no longer reports the resident Xbox Game Bar as a recorder. - Linux: recorders with names longer than 15 characters (such as
gpu-screen-recorder) are detected. - Guards: when one of two active guards went away it switched protection off for both, and changing a guard's settings while it was active could leak a listener or stop detection for another guard. Guards now count their requests, and apply them in order against their latest settings, so a guard removed or reconfigured while a platform call is in flight (or a guarded route covered immediately by another) never leaves the window protected, or unprotected, by mistake.
- Android: attaching to an activity no longer clears a
FLAG_SECUREor recents setting the host app applied itself; the plugin only re-applies what Dart asked for. - iOS: a replaced root view (a new root view controller, add-to-app) is now detected and protected, without putting the old root's content back on screen.
- Linux: the recorder scan reads a process's command line only when its short name is ambiguous, since reading it can block behind a stuck process.
- iOS: screen-recording detection did not update on recent iOS versions (tested on iOS 27): the plugin now observes the per-scene capture state trait directly.
- Region: fixed a crash (
EXC_BAD_ACCESS) when a region engaged after UIKit replaced its secure canvas, and a copy that stopped updating (froze) after the region disengaged and re-engaged - for example when Control Center is opened to start a recording. - Region: a pending throttled refresh made every repaint refresh immediately, so
animating regions refreshed on every frame and
refreshIntervalhad no effect.
Behaviour changes #
ScreenshotShieldSensitiveView.refreshIntervalnow defaults to about 30 refreshes a second (defaultRefreshInterval); passDuration.zerofor the previous every-frame behaviour. Copies are rasterised at no more than 2x (maxCopyPixelRatio), which more than halves the per-refresh cost on 3x devices.backdropColornow defaults to transparent instead of the Material theme's scaffold colour, so the widget no longer depends on aTheme. Set it when the child has see-through parts such as rounded corners.
Other changes #
- Add
ScreenshotShieldSensitiveView.capturePlaceholder: any widget to show captures in place of the region - a shape matching the content, a message, or a blur - instead of a solidcaptureColor. - The example gains an animating region with a refresh-rate switch and a placeholder.
- The README is reorganised around a feature list, a platform support table and a quick start, and documents the Android media permission needed on Android 10-13 and the permissions the plugin merges into the app.
- pub.dev listing: clearer description, topics, issue tracker and a screenshot.
- CI builds the example on Android, iOS, Windows and Linux and runs a publish dry-run; releases publish from a version tag through pub.dev's automated publishing.
- Add
ScreenshotShield.setKeyboardProtection(enabled:)(iOS): the on-screen keyboard is a private window of its own, so it survived both whole-window protection and a sensitive region and was visible in captures. The plugin now nests the keyboard window's content in the same capture-excluded canvas used for regions while this is enabled, re-installing it whenever a keyboard window appears or is rebuilt, and retiring the canvas safely (the same deferred release the app window needs, sinceCALayer.delegateisunowned(unsafe)). Undocumented UIKit behaviour: verify on the iOS versions you support. On Android the keyboard belongs to another application and cannot be excluded, so the call is a no-op there and the README documents the alternatives. - The example gained a keyboard section (a normal and a secure field) and a switch for this, so it can be compared on a device.
0.1.10 #
- Add
SensitiveProtection.whileRecordingtoScreenshotShieldSensitiveView: the region engages only while the screen is being recorded or mirrored, and ignores the app lifecycle, so it stays untouched (and the app-switcher snapshot keeps showing it) when the app goes to the background.whileCapturedremains the default and adds that background case;alwaysis unchanged.
0.1.9 #
- Fix
ScreenshotShieldSensitiveViewstaying unprotected when it appeared while the screen was already being recorded or mirrored. The recording state was only ever delivered as a stream event, and a broadcast stream replays nothing to a listener that arrives later, while the host's "report the current state" only runs when listening starts - which the reference count suppresses when another consumer (a guard, another region, the app's own subscription) is already listening. A region that mounted mid-recording therefore received nothing until the state changed again. - Track the latest screen-recording state centrally and expose it as the plain
getter
ScreenshotShield.isScreenRecording, so anything that starts watching while a recording is in progress can read the current state instead of waiting for a change it missed. Platform implementations feed it throughreportScreenRecordingState; the Pigeon implementation subscribes when listening starts (not in its constructor, where the Flutter binding is not ready yet and the event channel would silently never be listened to), keeps the subscription so the value stays readable after listening stops, and ignores a stream that is never answered rather than surfacing it as an unhandled error.
0.1.8 #
- iOS: fix two
EXC_BAD_ACCESScrashes inScreenshotShieldSensitiveViewreported from a real app. The private canvas layer UIKit builds inside the secure text field was cached on its own, but its owner view is only reachable throughCALayer.delegate, which isunowned(unsafe): when UIKit rebuilt the canvas - which it does while laying the field out, including during a scene snapshot pass, and while Flutter's platform-views controller resets - the cached layer outlived its delegate and the next layer operation retained the dead view. The canvas is now held as its view and re-resolved from the live field on every layout pass, the region no longer moves layers when it leaves the window, and the canvas view is released a runloop turn later. The whole-window protection had the same latent bug - its cached layer was evenweak, which silently stopped re-asserting the nesting - and now follows the same pattern. ScreenshotShieldSensitiveViewno longer shows a copy in normal use: it wraps the subtree in a layout-neutral box that paints nothing, creates no platform view and rasterises nothing, and only engages while protection is needed.protection: SensitiveProtection.whileCaptured(the new default) covers screen recordings and mirroring plus the app-switcher snapshot;protection: SensitiveProtection.alwayskeeps it engaged permanently for apps that also need foreground screenshots blanked.- While engaged, a capture shows
captureColor(black by default) and the user sees the subtree as a copy refreshed whenever it repaints, composited overbackdropColor(the ambient scaffold background by default) inside the capture-excluded canvas.placeholderColoris renamed tocaptureColor. A shield that is transparent on screen and black in a capture cannot exist: an excluded layer is omitted from the capture rather than replaced by black. - The region is laid out by a custom render box instead of a
Stack: the subtree receives the constraints the region received, unchanged, the region sizes itself from the subtree, the overlay is sized to match it exactly, nothing is clipped, and only the subtree is a pointer target or contributes semantics. - The copy crosses the channel as raw RGBA pixels plus the backdrop colour instead of PNG, and a refresh scheduled outside a frame now asks for one, so the last state always reaches the native view.
- Reference count
startListening/stopListeningin the platform implementation, so several consumers no longer cancel each other's detection.
0.1.7 #
- Add
ScreenshotShieldSensitiveView, an experimental iOS-only widget that excludes a single region from screenshots and screen recordings instead of blanking the whole window. The subtree is rasterised into a native platform view whose layer is nested in its own capture-excluded canvas layer, so the rest of the app stays capturable. The region renders from a snapshot, so it is only as fresh as the last refresh;refreshIntervalor the widget's controller can refresh it. Relies on undocumented UIKit behaviour and has to be verified on a device. ScreenshotShieldSensitiveViewstands down while whole-window prevention is active, since the region is blanked by the window anyway: no platform view is created and no snapshot is taken. It activates again when the protection is released.ScreenshotShield.preventCaptureActiveexposes that state.- Add
SecureCanvas, the shared secure-text-field helper used by the whole-window protection and the new region widget. - iOS: fix a crash when a sensitive region was disposed, for example when
popping the screen it lives on. Flutter disposes platform views from inside a
frame submit, so the rasterised layer is no longer moved back out of the
secure canvas from
deinit; the view tree is simply released. The transparent placeholder and the capture exclusion still behave the same while the region is alive. - iOS: keep a sensitive region interactive. The capture placeholder sat above the wrapped subtree and, being opaque, absorbed pointers, so taps, drags, focus and text input never reached the widget inside the region. It is now transparent to pointers, and the wrapped subtree behaves normally.
- iOS: stop covering a sensitive region with the placeholder before the native
view holds a snapshot, which showed an opaque rectangle on screen. The child
stays visible instead (and the region is simply not excluded from captures
yet), the first snapshot is retried until it lands, and it is re-taken when the
region is laid out at a new size.
ScreenshotShieldSensitiveViewControllercaptures after the next frame and coalesces bursts of requests, so refreshing on a change - a text field'sonChanged, for example - rasterises what was just typed. - iOS: fix the region's platform view being rebuilt on every snapshot, which reset the snapshot state and looped. The placeholder now stays in the tree and only changes colour.
0.1.6 #
Not recommended: the sensitive region platform view in this version crashed when it was disposed, did not receive pointer events, and could cover the region with an opaque placeholder on screen. Use 0.1.7, which fixes all three.
0.1.5 #
- iOS: fix
preventCapturenot blanking screenshots. The secure text field was added as a sibling subview of the window, which protects only the (empty) field itself; the app's content layer is now nested inside the field's capture-excluded canvas layer, so screenshots and screen recordings of the guarded screen come out blank. The content layer is restored exactly when protection is released, and the window is now resolved from the foreground-active scene. This relies on undocumented UIKit behaviour and can break on a future iOS release. - Reformat the Dart sources with the current formatter so the analysis and formatting checks pass again.
0.1.4 #
- Add
ScreenshotShield.onScreenRecordingChanged, aStream<bool>that emits the current screen-recording state (and the current value on subscription) whilestartListeningis active. - iOS: report screen recording (and screen mirroring) via
UIScreen.capturedDidChangeNotification/UIScreen.isCaptured. - Android: report screen recording on Android 15 (API 35) and newer via the
DETECT_SCREEN_RECORDINGAPI; older versions never emit. TheDETECT_SCREEN_RECORDINGpermission is declared. - Windows and Linux: best-effort screen-recording detection by sampling the
running process list for well-known recorders (OBS, Bandicam, Camtasia,
Kazam, Kooha,
wf-recorder, and others) every two seconds. This is heuristic and can both miss unlisted recorders and report an idle recorder.
0.1.3 #
- Android: fix
onScreenshotDetectednever firing whilepreventCaptureis enabled. The secure window flag blanks the frame (so the media-store observer never fires) and, on Android 14+, the system withholds the screen-capture callback for secure windows, so prevention and detection are mutually exclusive. The guards now resolve the conflict by letting detection win on Android when both are requested, and the guarded screen is re-rasterized into a shareable image instead. The system still shows a notice when detection fires on Android 14+. - Android: make media-store screenshot detection on Android 13 and below more reliable. The observer now queries the most recently added image (filtered to the last 15 seconds) instead of trusting the URI delivered by the media store, which varies by Android version and OEM, and it no longer crashes when the media query is blocked by permissions.
- Android: declare
READ_EXTERNAL_STORAGE(scoped to API 28 and below) so detection can query the media store on Android 9 and older; the host app must still request it at runtime. - Add
ScreenshotShieldGuard, a non-route guard that activates while the widget is mounted and itsactiveflag istrue, for screens not managed by aNavigatorwith aRouteObserver. - Add
forcePreventCapturetoScreenshotShieldRouteGuardandScreenshotShieldGuard. On Android it makes capture prevention win over screenshot detection when both are requested (blanking the frame and suppressing detection events), instead of the default where detection wins.
0.1.2 #
- Add Windows and Linux platform support. The Dart widgets work on desktop,
but screenshot detection and prevention are unavailable there (no OS APIs).
On Windows,
setProtection(backgroundBlur: true)cloaks the window from alt-tab and the taskbar preview while it is inactive or minimized. - iOS:
preventCapturenow blanks the app-switcher preview via a hidden secure text field. User screenshots themselves cannot be blanked on iOS, but detection and the shareable-image capture still work. - iOS: the background blur is applied on
willResignActiveso it reliably appears in the app switcher. - Android: the background blur now triggers on the user-leave hint (before
onPause) and forces a frame commit so the recents thumbnail includes it. - Add a GitHub Actions workflow that publishes to pub.dev with configurable major/minor/patch version bumps.
0.1.1 #
- Fix iOS builds: correct the Swift Package library product name to
screenshot-shield. - Background blur on Android now only applies the
RenderEffectblur when the FlutterView usesRenderMode.texture(where it actually reaches Flutter content); otherwise a dim overlay is shown. UseRenderMode.textureinMainActivityfor a real blur. - Make the iOS background blur appear reliably in the app switcher by committing it immediately when the app enters the background.
ScreenshotShieldRouteGuardnow appliespreventCapture/detectScreenshotschanges immediately instead of only on route changes.- Add an example app demonstrating detection, captured-image callbacks, and background blur.
0.1.0 #
- Detect user screenshots on Android and iOS.
- Android 14+ uses the system
DETECT_SCREEN_CAPTUREAPI; older Android versions watch the media store for new screenshots. - iOS reports immediately via the
UIApplicationUserDidTakeScreenshotNotificationsystem notification. ScreenshotShieldScopeprovides a sharedScreenshotShieldto the widget tree.ScreenshotShieldRouteGuardscopes protection to a route: capture prevention and screenshot listening are enabled while the route is in view and released when another route covers it.- Optional re-rasterized PNG of the guarded screen passed to
onScreenshotDetected. - Optional native background blur that hides the app content in the app
switcher (
setProtection(backgroundBlur: true)). - Prevent screen capture on Android via the secure window flag
(
setProtection(preventCapture: true)).
