secure_guard
Detects the device conditions under which a banking or other sensitive session must not stay on screen, and gives you a single stream to react to:
- an audio or video call is ringing or in progress, including messenger VoIP calls
- the screen is being recorded, mirrored or shared
- the device is under remote control
What each platform can actually see
| Signal | Android | iOS |
|---|---|---|
| Cellular call | AudioManager.mode |
CallKit CXCallObserver |
| Messenger VoIP call | AudioManager.mode (MODE_IN_COMMUNICATION) |
CallKit, used by all mainstream messengers |
| Incoming call still ringing | AudioManager.mode (MODE_RINGTONE) |
CallKit reports the call on arrival |
| Screen recording / sharing | prevented by FLAG_SECURE, not reported |
UIScreen.isCaptured |
| Screen mirroring | opt-in via detectVirtualDisplay |
UIScreen.isCaptured |
| Remote control | enabled accessibility services | covered by isCaptured |
Two consequences worth knowing before shipping:
- Android does not let one app observe another app's screen capture. The
countermeasure is
FLAG_SECURE, which is on by default here and makes captured frames come out blank.SecureGuardThreat.screenCapturetherefore only fires on Android whendetectVirtualDisplayis enabled. - iOS has no remote control API, so
SecureGuardThreat.remoteAccessnever fires there. Remote sessions show up asscreenCaptureinstead.
No runtime permissions are required on either platform.
Polling
Every signal is callback driven where the OS offers a callback, so detection is immediate. Polling only fills the gaps:
| Platform | Callbacks | Polls at pollInterval |
|---|---|---|
| Android 33+ | audio mode, accessibility services list, displays | no |
| Android 31–32 | audio mode, accessibility on/off, displays | yes — a service enabled while accessibility is already on raises no callback |
| Android < 31 | accessibility on/off, displays | yes — no audio mode callback |
| iOS | CallKit, capturedDidChange |
no |
When nothing needs polling the monitor still ticks every 30 seconds as a safety net against a listener that silently stops firing.
Usage
import 'package:secure_guard/secure_guard.dart';
await SecureGuard.instance.start(
const SecureGuardConfig(
allowedAccessibilityServices: {
'com.google.android.marvin.talkback',
},
),
);
Cover the whole app from MaterialApp.builder so the block sits above every
route and dialog:
MaterialApp(
builder: (context, child) => SecureGuardOverlay(
blockedBuilder: (context, state) => BlockedScreen(state: state),
child: child!,
),
);
Or drive it yourself:
SecureGuard.instance.states.listen((state) {
if (state.has(SecureGuardThreat.remoteAccess)) {
analytics.report('remote_access', state.sources.join(','));
}
});
SecureGuardState.sources carries the accessibility service ids or audio modes
that triggered the state, which is what you want to forward to an anti-fraud
backend.
Accessibility policy
Remote control tools cannot drive Android without an accessibility service, so that list is the signal. By default only packages matching a built-in list of known remote control tools count, which keeps screen readers working.
AccessibilityPolicy.blockAllUntrusted flags every enabled service instead. It
catches tools that are not on the list, but it will lock out users who rely on a
screen reader unless you whitelist theirs through
allowedAccessibilityServices.
Limitations
Everything here runs client side and can be defeated on a rooted or jailbroken device. Treat the signals as input to a server side risk decision, not as the only line of defence.