WIP: feat(auth): automatic logoff after user inactivity (UI API 1.12.0) #30

Draft
james.maes wants to merge 1 commits from feat/session-user-activity into main
pull from: feat/session-user-activity
Owner

Why

Automatic logoff: a session must end after a set time without a user action. The scanner sends a session heartbeat every 15 seconds or less while it is in the foreground, so its requests are not its operator. QQQ core gets a user inactivity timeout in QRun-IO/qqq PR 1361 (UI API 1.12.0); this is the scanner's side of it.

What

  • scanner-host/.../UserActivity.kt: the time of the operator's last action on a clock that counts device sleep (SystemClock.elapsedRealtime). ScannerActivity.onUserInteraction records every touch and key; a hardware scan records one too (ScannerWidget), since no key event reaches the activity for it.
  • Every request of the session states X-QQQ-User-Activity-Age: the UI API repository, the mobile-session heartbeat and record files. The device enrollment API does not (it has no user session). Until the first action of a fresh process the age is the device's uptime: nothing is assumed in the operator's favour, and the server decides from what it recorded.
  • InactivityLogoff composable: when the server publishes userInactivityTimeoutSeconds, warns 60 seconds before ("Are you still there?", "Stay signed in") and calls QViewModel.logOut() when the time runs out. The server ends the session at the next request regardless, which the existing onUnauthorized path already handles (requestLogout).
  • core: OAuth2AuthenticationMetaData.Values.userInactivityTimeoutSeconds (optional).

A server without the value ignores the header and the scanner shows nothing new.

Not done here

  • The automatic logoff is a local QQQ sign-out; it does not open the browser for the identity provider's end-session. The next sign-in asks for credentials only where the server advertises nativeSessionLogin.prompt = login.
  • No message on the sign-in screen says why the operator was signed out.
  • No connected or E2E test: none can run without an emulator. AGENTS.md asks for emulator coverage of authentication changes, so this needs :qqqsampleandroidmobileapp:connectedDebugAndroidTest and a scanner E2E case before it leaves draft.

Verified

  • ./gradlew :qqq:android:core:testDebugUnitTest :qqq:android:mobileapp:testDebugUnitTest :scanner-host:testDebugUnitTest :scanner-host:lintDebug :scanner-host:assembleDebug on d3258bf: exit 0. Unit tests: core 60, mobileapp 65, scanner-host 130, 0 failures (5 new in UserActivityTest, one assertion pair in NativeSessionLoginTest). Debug APK assembled.
  • Gitea build on d3258bf (run 7368: unit tests, lint, APKs and the existing connected tests on an emulator): success. Those connected tests do not exercise the new timer or dialog.
  • NOT verified: on a device or emulator. Nobody has seen the dialog, the sign-out, or a TC22 hardware scan resetting the timer.

Release

After QRun-IO/qqq PR 1361 is released: emulator and TC22 checks, then a signed scanner release (1.0.5) by the usual hand-signed process, then MDM distribution. Scanners that are not updated are signed out one timeout after each sign-in once a server switches the timeout on.

🤖 Generated with Claude Code

## Why Automatic logoff: a session must end after a set time without a **user action**. The scanner sends a session heartbeat every 15 seconds or less while it is in the foreground, so its requests are not its operator. QQQ core gets a user inactivity timeout in QRun-IO/qqq PR 1361 (UI API 1.12.0); this is the scanner's side of it. ## What - `scanner-host/.../UserActivity.kt`: the time of the operator's last action on a clock that counts device sleep (`SystemClock.elapsedRealtime`). `ScannerActivity.onUserInteraction` records every touch and key; a hardware scan records one too (`ScannerWidget`), since no key event reaches the activity for it. - Every request of the session states `X-QQQ-User-Activity-Age`: the UI API repository, the mobile-session heartbeat and record files. The device enrollment API does not (it has no user session). Until the first action of a fresh process the age is the device's uptime: nothing is assumed in the operator's favour, and the server decides from what it recorded. - `InactivityLogoff` composable: when the server publishes `userInactivityTimeoutSeconds`, warns 60 seconds before ("Are you still there?", "Stay signed in") and calls `QViewModel.logOut()` when the time runs out. The server ends the session at the next request regardless, which the existing `onUnauthorized` path already handles (`requestLogout`). - core: `OAuth2AuthenticationMetaData.Values.userInactivityTimeoutSeconds` (optional). A server without the value ignores the header and the scanner shows nothing new. ## Not done here - The automatic logoff is a local QQQ sign-out; it does not open the browser for the identity provider's end-session. The next sign-in asks for credentials only where the server advertises `nativeSessionLogin.prompt = login`. - No message on the sign-in screen says why the operator was signed out. - No connected or E2E test: none can run without an emulator. `AGENTS.md` asks for emulator coverage of authentication changes, so this needs `:qqqsampleandroidmobileapp:connectedDebugAndroidTest` and a scanner E2E case before it leaves draft. ## Verified - `./gradlew :qqq:android:core:testDebugUnitTest :qqq:android:mobileapp:testDebugUnitTest :scanner-host:testDebugUnitTest :scanner-host:lintDebug :scanner-host:assembleDebug` on d3258bf: exit 0. Unit tests: core 60, mobileapp 65, scanner-host 130, 0 failures (5 new in `UserActivityTest`, one assertion pair in `NativeSessionLoginTest`). Debug APK assembled. - Gitea build on d3258bf (run 7368: unit tests, lint, APKs and the existing connected tests on an emulator): success. Those connected tests do not exercise the new timer or dialog. - NOT verified: on a device or emulator. Nobody has seen the dialog, the sign-out, or a TC22 hardware scan resetting the timer. ## Release After QRun-IO/qqq PR 1361 is released: emulator and TC22 checks, then a signed scanner release (1.0.5) by the usual hand-signed process, then MDM distribution. Scanners that are not updated are signed out one timeout after each sign-in once a server switches the timeout on. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
james.maes added 1 commit 2026-11-10 18:45:18 +00:00
feat(auth): automatic logoff after user inactivity (UI API 1.12.0)
android / android (push) Successful in 7m26s
build / android (push) Successful in 7m26s
d3258bf52a
Every request of a signed-in session states the age of the operator's last
touch, key or scan in X-QQQ-User-Activity-Age, so the session heartbeat alone
no longer keeps a session alive on a server with a user inactivity timeout.
When the server publishes userInactivityTimeoutSeconds the scanner warns a
minute before and signs out when it runs out; the server ends the session at
the next request regardless. Servers without the value are unaffected.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
All checks were successful
android / android (push) Successful in 7m26s
build / android (push) Successful in 7m26s
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This pull request is marked as a work in progress.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/session-user-activity:feat/session-user-activity
git checkout feat/session-user-activity
Sign in to join this conversation.