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.screenCapture therefore only fires on Android when detectVirtualDisplay is enabled.
  • iOS has no remote control API, so SecureGuardThreat.remoteAccess never fires there. Remote sessions show up as screenCapture instead.

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.

Libraries

secure_guard