Files
ledgrab/ANDROID-REVIEW/android-foreground-app-automation-plan.md
T
alexei.dolgolyov 1c1bbe2551 feat(android): foreground-app automation condition
Make the existing Application automation rule (foreground app -> activate
scene) work on the Android-TV build. A Kotlin ForegroundAppBridge reads the
foreground app via UsageStatsManager and lists launchable apps via LauncherApps;
PlatformDetector bridges it in (ahead of the Windows-only ctypes guard) so the
existing AutomationEngine / ApplicationRule / storage / deactivation modes are
unchanged. New /system/installed-apps + /system/info endpoints feed an app picker
that stores package names (vs process names on desktop); on Android the editor
hides the match-type selector since the foreground app is the only obtainable
signal. PACKAGE_USAGE_STATS is granted via an on-device button + a web-UI banner
(no blanket prompt at capture start); detection degrades gracefully until granted.

Zero new Python/Gradle deps (UsageStatsManager + LauncherApps are in-platform;
matching only string-compares the package name, so no QUERY_ALL_PACKAGES).
assembleDebug + 1897 pytest + ruff + tsc + npm build all green; independent final
review (0 blockers) + security review (no critical issues).
2026-06-02 14:57:29 +03:00

5.4 KiB

Android foreground-app automation condition — implementation notes

Status: implemented on feature/android-foreground-app-automation. Last updated 2026-06-02.

What & why

The desktop build has an Application automation rule (ApplicationRule): activate a scene when given apps are running / foreground / fullscreen. It was already wired end-to-end on Android (engine, storage, API, editor) but silently never fired, because the two Windows-only ctypes paths return empty off-Windows:

  1. DetectionPlatformDetector._get_topmost_process_sync() (and the running/fullscreen variants) returned (None, False) / set() on Android.
  2. The app picker — populated from GET /api/v1/system/processesget_running_processes(), also empty on Android, so users couldn't even choose an app.

This feature fills both holes using in-platform Android APIs and the established Kotlin↔Python bridge pattern. Zero new Python or Gradle dependencies.

Design decision: one implicit "foreground" mode on Android

Android exposes exactly one obtainable signal — the current foreground app package. The desktop rule's four match types (running / topmost / fullscreen / topmost_fullscreen) are either unobtainable (runninggetRunningTasks is restricted) or identical (a foreground TV app effectively is fullscreen). So on Android:

  • The editor hides the match-type selector and the collector forces match_type="topmost".
  • _get_topmost_process_sync() returns (package, True); the running/fullscreen detectors return the foreground app as a best-effort single-element set so legacy rules still behave.

This avoided touching the existing plain <select> (forbidden for new UI) and removed a misleading 4-way choice — a simplification surfaced by the pre-implementation plan review.

Detection — ForegroundAppBridge (Kotlin) ↔ platform_detector.py

android/app/src/main/java/com/ledgrab/android/ForegroundAppBridge.kt (an object singleton, mirroring CameraBridge, context bound in LedGrabApp.onCreate):

  • getForegroundPackage()UsageStatsManager.queryEvents(now - 10s, now), returns the package of the most recent MOVE_TO_FOREGROUND / ACTIVITY_RESUMED event (the two constants share a value; the ~10s window absorbs event lag against the ~1s automation tick). queryEvents is the right call — queryUsageStats gives aggregate durations, not "current app".
  • hasUsageAccess()AppOpsManager OPSTR_GET_USAGE_STATS check (unsafeCheckOpNoThrow on API 29+, checkOpNoThrow below).
  • listLaunchableApps()LauncherApps.getActivityList → JSON [{package,label}] for the picker. The sanctioned launchable-app API; no QUERY_ALL_PACKAGES.

server/src/ledgrab/core/automations/platform_detector.py:

  • Module-level guarded wrappers get_foreground_package() / has_usage_access() / list_installed_apps() resolve jclass("com.ledgrab.android.ForegroundAppBridge").INSTANCE lazily (never at import — the module loads on desktop CI). These are the test monkeypatch surface, mirroring android_camera_engine.
  • The is_android() branch is placed ahead of the import-time if not _IS_WINDOWS: early-return in each detector — the critical fix from plan review (a naive wiring would no-op behind the Windows guard yet still pass tests). The Windows ctypes path is unchanged (regression-tested).
  • A one-time logger.warning fires when Usage Access is missing.

App picker — /system/installed-apps + platform signal

  • GET /api/v1/system/installed-apps{apps:[{package,label}], count} (empty off-Android).
  • GET /api/v1/system/info{is_android, app_match_kind, usage_access_granted} — the editor reads it to pick the app source + matching semantics and to show the Usage-Access banner.
  • Frontend: the command-palette picker (core/process-picker.ts) gained label→value support; a new AppPalette shows the human label and inserts the package name. On Android the app-rule editor uses it (attachAppPicker) instead of the process picker, plus a package-name hint and the Usage-Access banner.

Value semantics (no migration)

ApplicationRule.apps are package names on Android (com.netflix.mediaclient) vs process names on Windows (chrome.exe). Same field, same matching code — no storage migration — but rules are not portable across platforms. Documented in the model/schema docstrings and a user-facing editor hint.

Permission UX

PACKAGE_USAGE_STATS is a special access (can't be granted at runtime):

  • Manifest declares it with tools:ignore="ProtectedPermissions".
  • MainActivity shows a passive "Grant usage access" button (opens ACTION_USAGE_ACCESS_SETTINGS, with a generic-Settings fallback) only while access is missing. No blanket prompt at capture start — most users have no foreground-app rule.
  • The web-UI rule editor shows a banner when an Android Application rule lacks access.

Limitations

  • Foreground-app only; no full window-title or arbitrary process enumeration on Android.
  • Detection rides the existing ~1s automation poll; queryEvents can lag a few seconds.
  • Rules authored on desktop don't match on Android and vice-versa (package vs process names).
  • The on-device "Grant usage access" button currently shows whenever access is missing (not gated on whether an Android Application rule exists), to avoid Activity↔server coupling; the web-UI banner provides the contextual guidance.