Package selection guidance compiled from public package information, community data, and project usage.
The device_safety_info package is a Flutter plugin designed to enhance app security by detecting jailbroken/rooted devices, emulators/simulators, external storage access, VPN usage, screen lock status, and application update availability. It helps developers safeguard sensitive data and prevent unauthorized access by identifying potentially risky device states. The plugin supports both Android and iOS platforms, offering real-time checks during app execution. Ideal for financial, healthcare, and enterprise applications requiring robust security measures.
Use cases
Securing financial apps
Protecting healthcare data
Preventing emulator-based attacks
Detecting rooted devices
Enforcing screen lock requirements
Checking for app updates
Key features
Jailbreak/Root detection
Emulator/simulator detection
VPN detection
External storage check
Screen lock status verification
Application update checker
Suitable for
High-security applications
Enterprise mobile solutions
Finance and banking apps
Healthcare and compliance apps
Apps requiring anti-tampering
Considerations
Requires platform-specific permissions
May impact performance on low-end devices
Limited to Android and iOS
Behavior may vary across device models
Not a standalone security solution
Original README
Original README snapshot from the linked source repository. Visit the repository for the latest content.
device_safety_info
A device-security toolkit for Flutter apps: root/jailbreak, hooking, and debugger detection;
screenshot and screen-recording protection; clipboard, overlay-attack, and malware defenses; VPN
and app-update checks — all as plain data and streams you compose into your own UI, with zero
required third-party dependencies.
Primitives, not opinions. Every feature is a Future, a Stream, or a small data class —
the only widgets shipped (IdleTimeoutGuard, SecureScreen) are behavior-only wrappers, never
visual chrome. You build the UI; this package supplies the signal.
No required third-party dependencies. Root/hook detection runs through native FFI and
platform channels this package owns end-to-end — not package_info_plus, not http, not a
security-suite wrapper you also have to trust and keep updated.
Honest about platform limits. Where a check is structurally impossible on a platform (e.g.
overlay-attack detection on iOS, due to app sandboxing) it throws a clearly-coded exception
instead of silently returning a value that looks like "checked, all clear." Where a signal is
best-effort (store-version scraping, sideloading detection), the docs say so plainly.
Modular by feature. Internally organized as one vertical slice per feature across Dart,
Android, and iOS — a change to, say, clipboard behavior only touches clipboard files.
Usage
1. Basic Security Checks
These simple getters provide quick boolean checks for common security states.
// Checks whether device JailBroken or Rooted// iOS: Uses IOSSecuritySuite. Android: Uses Native FFI + Root files check.bool isRooted = awaitDeviceSafetyInfo.isRootedDevice;
// Checks whether device is Real or Emulator/Simulatorbool isReal = awaitDeviceSafetyInfo.isRealDevice;
// Checks for hooking frameworks (Frida, Xposed, Cydia Substrate, etc.)// Uses native scan of process memory maps for high reliability.bool isHooked = awaitDeviceSafetyInfo.isHooked;
// Checks whether a debugger is attached to the process// Uses native C checks (TracerPid/P_TRACED) to bypass simple debugger hooks.bool isDebugger = awaitDeviceSafetyInfo.isDebuggerAttached;
// Checks for screen lock (PIN/Pattern/Biometrics)bool isScreenLock = awaitDeviceSafetyInfo.isScreenLock;
// Checks if app is installed from Official Store (Play Store / App Store)bool isStore = awaitDeviceSafetyInfo.isInstalledFromStore;
// (Android Only) Checks if app is installed on external storagebool isExternal = awaitDeviceSafetyInfo.isExternalStorage;
// (Android Only) Checks if Development Options are enabledbool isDeveloperMode = awaitDeviceSafetyInfo.isDeveloperMode;
2. Advanced Security Actions
For Root and Hook detection, you can take immediate action like closing the app.
// Check for hooks and optionally exit or uninstallbool hooked = awaitDeviceSafetyInfo.checkHooked(
exitProcessIfTrue: true, // Closes the app immediately if hooked
uninstallIfTrue: false, // (Android Only) Triggers uninstallation
);
3. Screenshot & Recording Management
Protect your app's sensitive data from being captured.
// --- Detection ---// Check if screen is currently being captured/recorded/mirroredbool isCaptured = awaitDeviceSafetyInfo.isScreenCaptured;
// Listen to real-time screen capture status changesDeviceSafetyInfo.onScreenCapturedChanged.listen((isCaptured) {
print("Screen capture status changed: $isCaptured");
});
// Listen to screenshot events// iOS: Uses UIApplication.userDidTakeScreenshotNotification// Android: Uses API 34 ScreenCaptureCallback or ContentObserverDeviceSafetyInfo.onScreenshotTaken.listen((_) {
print("User took a screenshot!");
});
// --- Prevention ---// Block screenshots and screen recordings// Android: Uses FLAG_SECURE. iOS: Uses a secure UITextField layer trick.awaitDeviceSafetyInfo.blockScreenshots(block: true);
// Convenience query/toggle alongside blockScreenshots.bool isBlocked = awaitDeviceSafetyInfo.isScreenshotBlocked;
awaitDeviceSafetyInfo.toggleScreenshotBlocking();
// Or declaratively: blocks screenshots for as long as this widget (or any other// mounted SecureScreen) is in the tree. Pure Dart, no native code of its own.SecureScreen(child: MySensitiveScreen());
// --- Live overlay modes ---//// FLAG_SECURE / the iOS secure-layer trick make the OS render nothing at all into a// capture, so no overlay content can ever appear *inside* a screenshot or recording —// these show a real, visible overlay over the app's own on-screen content instead,// reactively, only while a capture/recording is actually happening.// Blur the screen whenever a capture/recording is detected.// Android requires API 31+ for a true blur; degrades to a translucent scrim below that.awaitDeviceSafetyInfo.setScreenshotOverlayMode(
mode: ScreenshotOverlayMode.blur,
blurRadius: 16,
);
// Or a solid color / custom image instead:awaitDeviceSafetyInfo.setScreenshotOverlayMode(
mode: ScreenshotOverlayMode.color,
argbColor: 0xFF6200EE,
);
awaitDeviceSafetyInfo.setScreenshotOverlayMode(
mode: ScreenshotOverlayMode.image,
imageBytes: myBrandedPlaceholderBytes,
);
awaitDeviceSafetyInfo.clearScreenshotOverlayMode();
// --- Screen recording detection ---//// Distinct from isScreenCaptured/onScreenCapturedChanged above, which covers screen// mirroring/external-display capture. Android: backed by the real recording-session// callback, API 35+ only — isSupported reports false below that rather than guessing.// iOS has no API distinguishing "recording" from "mirroring/AirPlay" — both surface// through the same signal isScreenCaptured already uses, so isSupported is always true// there and the two streams report identically.bool canDetectRecording = awaitScreenRecordingDetector.isSupported;
ScreenRecordingDetector.onScreenRecordingChanged.listen(
(isRecording) => print("Screen recording: $isRecording"),
onError: (e) => print("Screen recording detection unavailable: $e"), // see caveat below
);
Known limitation: isSupported reflects Android OS version support only (API 35+) — it
cannot know ahead of time whether a given device's manufacturer actually grants the underlying
DETECT_SCREEN_RECORDING permission to third-party apps at runtime, even though it's declared in
the plugin's manifest. On at least one Samsung device, the OS enforces this through an internal
Knox-branded WindowManagerService path that denies it regardless. When that happens, the stream
delivers a permission_denied error via onError instead of values — always attach one, as shown
above.
4. App Switcher (Recents) Security
Control how your app appears in the recent apps / multitasking view.
// Add a solid color overlay when the app is in the background.// This prevents sensitive data from being visible in the app switcher.awaitDeviceSafetyInfo.setRecentsOverlay(argbColor: 0xFF000000); // Opaque Black// Clear the overlayawaitDeviceSafetyInfo.clearRecentsOverlay();
// (Android Only) Completely hide the app from the Recents menuawaitDeviceSafetyInfo.hideMenu(hide: true);
5. VPN Monitoring
Monitor VPN connectivity in real-time.
final vpnCheck = VPNCheck();
vpnCheck.vpnState.listen((state) {
if (state == VPNState.connectedState) {
print("VPN is now connected.");
} else {
print("VPN is now disconnected.");
}
});
6. App Version Checker
Check if there's a new version available on the store.
voidcheckVersion() async {
final newVersion = NewVersionChecker(
iOSId: 'your.bundle.id',
androidId: 'your.package.name',
// Optional: numeric App Store ID, tried as a fallback lookup if the bundle-ID// lookup above returns no results (a real failure mode even for live apps).
iOSAppStoreId: '123456789',
// Optional: a developer-supplied minimum version, e.g. from your own remote// config. Never scraped from the store — see the caveat below.
minAppVersion: '2.0.0',
);
final status = await newVersion.getVersionStatus();
if (status == null) return; // Store unreachable or unparseable — see caveat below.switch (status.urgency) {
caseUpdateUrgency.required:
// localVersion is below minAppVersion — block further use until updated.print("Update required: ${status.storeVersion}");
break;
caseUpdateUrgency.optional:
print("New version available: ${status.storeVersion} (Local: ${status.localVersion})");
break;
caseUpdateUrgency.none:
break; // Already up to date.
}
print("Update Link: ${status.appStoreLink}");
}
Known limitation: both lookups are best-effort parsing of endpoints neither store publishes
as a stable, documented API (the iTunes lookup JSON shape, and the Play Store listing HTML).
Both have changed unannounced in the past. getVersionStatus() fails soft — it returns null
rather than throwing when a response doesn't parse as expected — but a null result (or
UpdateUrgency.none) should never be treated as proof no update exists, only as "couldn't
determine." This package deliberately does not attempt to extract release notes/"what's new"
text, since that field is one of the most fragile and inconsistently formatted parts of both
stores' responses.
7. Overlay Attack Detection (Android only)
Detect or block touches while another app is drawing an overlay on top of yours (tapjacking /
overlay-phishing). Not applicable on iOS — app sandboxing makes cross-app overlays structurally
impossible, so calls throw a PlatformException('UNSUPPORTED_PLATFORM', ...) there rather than
silently doing nothing.
// --- Detection ---// Fires whenever a touch is delivered while the window is obscured by another app's overlay.DeviceSafetyInfo.onOverlayAttackDetected.listen((_) {
print("Touch received while obscured by another app's overlay!");
});
// --- Prevention ---// Drop touches outright while the window is obscured (OS-level protection).awaitDeviceSafetyInfo.blockTouchesWhenObscured(block: true);
8. Clipboard Protection
Protect sensitive copied text (OTPs, card numbers) from other apps reading it, and react to
clipboard changes from any app.
// Copy sensitive text: hides it from the system clipboard preview (Android API 33+, marks the// pasteboard item local-only on iOS) and auto-clears it after the given duration.awaitDeviceSafetyInfo.copyToClipboard(
'123456',
sensitive: true,
autoClear: constDuration(seconds: 30),
);
// Clear the clipboard immediately.awaitDeviceSafetyInfo.clearClipboard();
// Listen for clipboard changes, from any app.DeviceSafetyInfo.onClipboardChanged.listen((_) {
print("Clipboard contents changed.");
});
9. IOC / C2 Domain Blocking
A lightweight domain-reputation lookup you wire into your own HTTP client (e.g. an http/Dio
interceptor) or WebView navigation guard. This is a client-side lookup utility only — it does not
intercept network traffic itself, and it does not ship a maintained threat-intel feed; you supply
or point at your own list.
// Supply your own list. Entries starting with `*.` match subdomains only (like a TLS wildcard// certificate) — list the bare domain too if it should also be blocked.IOCDomainBlocker.updateBlocklist(['evil.com', '*.evil.com']);
// Or load a newline-separated list from a remote feed ('#' lines are treated as comments).awaitIOCDomainBlocker.loadRemoteBlocklist(Uri.parse('https://example.com/ioc-feed.txt'));
// Check a host before making a request or navigating a WebView to it.if (IOCDomainBlocker.isBlocked(uri.host)) {
// refuse the request / navigation
}
10. Malware Package Detection (Android only)
Checks whether a specific package is installed, for matching against a known-malware/stalkerware
list you supply. Requires a manifest declaration — on Android 11+ (API 30+), package visibility
filtering means you must declare every package name you want to check in your own
AndroidManifest.xml, or the check always reports "not installed" even if it actually is (see
Permissions below). This plugin deliberately doesn't request the broader
QUERY_ALL_PACKAGES permission — that permission is subject to Google Play's manual approval
process and would be merged into every app depending on this plugin, most of which wouldn't qualify.
// Add the package name(s) you want to check to your AndroidManifest.xml <queries> block first.final isInstalled = awaitMalwarePackageDetector.isPackageInstalled('com.example.known.malware');
// Or check a list at once — returns only the ones found installed.final found = awaitMalwarePackageDetector.scanKnownMalware([
'com.example.known.malware',
'com.example.known.spyware',
]);
11. Accessibility Abuse Detection (Android only)
Lists currently-enabled Accessibility services. Malware that abuses the Accessibility API (to read
screen content or auto-click on the user's behalf) shows up here the same way a legitimate screen
reader would — matching malicious against legitimate services is left to you.
final services = awaitDeviceSafetyInfo.enabledAccessibilityServices; // raw component namesfinal anyEnabled = awaitDeviceSafetyInfo.isAnyAccessibilityServiceEnabled;
12. Play Protect Status (Android only)
Reads whether Google Play Protect scanning is enabled. There is no public "Play Protect API" —
this reads the underlying OS setting Play Protect's toggle controls directly.
final status = awaitDeviceSafetyInfo.playProtectStatus; // PlayProtectStatus.enabled/disabled/unknown
13. Idle Session Timeout
Fires a callback after a period with no touch activity anywhere in the wrapped widget tree — pure
Dart, no native code, identical behavior on every platform. timeout accepts any Duration you
choose — there's no fixed or default value baked into the plugin.
IdleTimeoutGuard(
timeout: constDuration(seconds: 30), // e.g. for quick testing// timeout: const Duration(minutes: 5), // a typical session length// timeout: const Duration(minutes: 15), // a more lenient session length
onTimeout: () => logOutUser(),
child: constMyApp(),
);
14. Risk Summary
Aggregates several of the checks above (rooted, hooked, debugger, screen capture, VPN, missing
screen lock) into a single list of plain-language risk flags, for showing a consolidated warning
before a sensitive action.
final flags = awaitRiskSummary.evaluate();
for (final flag in flags) {
print('${flag.title}: ${flag.description}');
}
15. Notification Listener Check (Android only)
Lists apps currently granted notification-listener access — the mechanism banking trojans like
TrickMo and Antidot/PhantomCall commonly abuse to intercept OTP and SMS notifications. Same
"surface the raw list, you judge good vs. bad" shape as Accessibility Abuse Detection above.
final listeners = awaitDeviceSafetyInfo.enabledNotificationListeners; // raw component namesfinal anyEnabled = awaitDeviceSafetyInfo.isAnyNotificationListenerEnabled;
Checks whether install-unknown-apps ("sideloading") rights are currently granted. Important
caveat, read before using: on Android 8.0+ (the vast majority of active devices), this can only
answer "has THIS app been granted install rights" — it cannot detect whether some other
(potentially malicious) app has sideloading rights, because that per-app grant isn't readable
across app boundaries. A false result here is not evidence the device is free of sideloaded
malware. Only on Android 7.x does this reflect the old device-wide toggle. Requires
REQUEST_INSTALL_PACKAGES — see Permissions below.
final canSideload = awaitDeviceSafetyInfo.isUnknownSourcesEnabled;
17. Call-Screening Role (Android only, API 29+)
Android's RoleManager.getRoleHolders() — the API that would reveal which app currently holds
the call-screening role — is a privileged system API third-party apps can't call. PhantomCall
abuses this role to silently block a bank's fraud-team calls, so the best available mitigation is
pointing the user at the OS role picker to review it themselves.
final available = awaitDeviceSafetyInfo.isCallScreeningRoleAvailable; // device capabilityfinal heldByMe = awaitDeviceSafetyInfo.isCallScreeningRoleHeldByThisApp; // only meaningful if// your app screens callsawaitDeviceSafetyInfo.openCallScreeningRoleSettings(); // opens the OS picker
18. Call Activity Detection
Detects when any call — a native SIM call or a VoIP call from WhatsApp/Teams/Skype/Meet/imo/etc.
— starts or ends. Like every other stream in this plugin, this is detect-only: no app is
identified (not achievable on either platform) and no lockdown/navigation policy is applied for
you. Native listeners only run while the stream has an active subscriber.
DeviceSafetyInfo.onCallActivityChanged.listen((event) {
print('${event.source}: ${event.state}'); // e.g. CallActivitySource.simCall, .started
});
final isActive = awaitDeviceSafetyInfo.isCallActive; // point-in-time, no subscription needed
Android distinguishes simCall (via TelephonyManager, needs READ_PHONE_STATE) from voipCall
(inferred from system-wide audio routing state — works for any app, generically, with no per-app
cooperation needed). iOS reports callKitObserved (via CallKit's CXCallObserver — only sees
calls the calling app routed through CallKit) or audioInterrupted (a lower-confidence fallback
for VoIP apps that don't integrate CallKit, via AVAudioSession interruption notifications).
Permissions (Android)
Add these to your AndroidManifest.xml if you use the respective features:
<!-- Required for screenshot detection on Android 10-12 --><uses-permissionandroid:name="android.permission.READ_EXTERNAL_STORAGE"android:maxSdkVersion="32" /><!-- Required for screenshot detection on Android 13 --><uses-permissionandroid:name="android.permission.READ_MEDIA_IMAGES" />
Malware Package Detection: not a runtime permission, but a required manifest declaration.
Without it, MalwarePackageDetector.isPackageInstalled() always reports false for that package,
even if it's actually installed (Android 11+ package visibility filtering):
<queries><packageandroid:name="com.example.known.malware" /><!-- one <package> entry per package name you intend to check --></queries>
Unknown Sources Check: a normal-protection permission, auto-granted with no runtime
prompt — but Google Play treats it as a restricted permission requiring justification in Play
Console's Permissions Declaration form, since it's the same permission that gates actually
installing packages (this plugin only ever queries it). The plugin does not declare this
permission for you; add it yourself only if you use isUnknownSourcesEnabled (see
example/android/app/src/main/AndroidManifest.xml for a working example). Without it,
isUnknownSourcesEnabled degrades gracefully to false on API 26+ rather than crashing:
Call Activity Detection (SIM-call signal): a dangerous runtime permission — this plugin
cannot request it for you (no permission_handler-style dependency), so your app must request
runtime grant itself. Without it, onCallActivityChanged/isCallActive silently degrade to
VoIP-only detection rather than crashing:
Distinguish between physical devices and emulators.
Hook Detection
✅
✅
Detect frameworks like Frida, Xposed, or Cydia Substrate.
Debugger Detection
✅
✅
Check if a debugger is attached to the process.
Screen Lock Status
✅
✅
Check if PIN, Pattern, or Biometrics are enabled.
Store Install Check
✅
✅
Verify if installed from Google Play / App Store.
Developer Mode
✅
❌
Check if Developer Options are enabled.
External Storage Check
✅
❌
Check if the app is installed on external storage.
Screenshot & Screen Recording Protection
Feature
Android
iOS
Description
Screenshot Detection
✅
✅
Listen for real-time screenshot events.
Screen Capture Status
✅
✅
Detect if the screen is being recorded or mirrored.
Block Screenshots
✅
✅
Prevent screenshots and screen recordings in-app.
Screenshot Overlay Modes
✅
✅
Live blur/color/image overlay over the active screen whenever a capture/recording is detected — a branded placeholder instead of a plain black rectangle.
Screen Recording Detection
✅
✅
Detect an active screen-recording session, distinct from mirroring/external-display capture. Android needs API 35+; iOS shares the screen-capture signal — see the caveat in §3.
SecureScreen widget
✅
✅
Declarative, ref-counted widget that blocks screenshots while mounted. Pure Dart, no native code.
App Switcher & Privacy
Feature
Android
iOS
Description
Recents Overlay
✅
✅
Add a custom color overlay in the app switcher.
Hide from Recents
✅
❌
Completely hide the app from the recent apps list.
Clipboard Protection
✅
✅
Copy sensitive text with an auto-clearing, preview-hidden clipboard entry; listen for clipboard changes.
Overlay Attack Detection
✅
❌
Detect/block touches while another app draws over yours (tapjacking). Not applicable on iOS — app sandboxing makes cross-app overlays structurally impossible.
Network, Updates & Malware Defenses
Feature
Android
iOS
Description
VPN Detection
✅
✅
Real-time monitoring of VPN connection status.
Version Checker
✅
✅
Check for newer app versions on the store, with optional force/minimum-version enforcement.
IOC / C2 Domain Blocking
✅
✅
Look up a host against a blocklist you supply, for wiring into your own HTTP client or WebView.
Malware Package Detection
✅
❌
Check if a specific package is installed, to match against a known-malware list you supply. Requires a manifest declaration.
Accessibility Abuse Detection
✅
❌
List currently-enabled Accessibility services, a common abuse vector for screen-reading/auto-clicking malware.
Notification Listener Check
✅
❌
List apps with notification-listener access — a common OTP/SMS-theft vector for banking trojans.
Unknown Sources Check
✅
❌
Check whether this app has been granted install-unknown-apps rights. See caveats in §16.
Call-Screening Role
✅
❌
Check the call-screening role's availability/self-held status, and open the OS picker to review the current holder. Android 10+ (API 29+).
Play Protect Status
✅
❌
Read whether Google Play Protect scanning is enabled.
Session & Risk Utilities
Feature
Android
iOS
Description
Idle Session Timeout
✅
✅
Widget wrapper that fires a callback after a period of no touch activity anywhere in the app.
Risk Summary
✅
✅
Aggregates several checks above into a single list of plain-language risk flags.
Call Activity Detection
✅
✅
Detect when any call — native or VoIP (WhatsApp/Teams/Skype/etc.) — starts or ends, without identifying which app.
Platform Support
Android
iOS
Minimum version
API 24 (Android 7.0)
iOS 16.0
Language
Kotlin
Swift
Contributing
Issues and pull requests are welcome at the
issue tracker. Before opening a
PR for a new check or feature, please open an issue first to discuss the approach — this package
deliberately favors primitives over opinionated UI and avoids adding third-party dependencies, so
it helps to align on shape before writing code.