Files
Michał Pierzchała d29dc22861 fix(android): warn when a permission revoke kills the session app (#1856)
* fix(android): warn when a permission revoke kills the session app

settings permission deny|reset maps to pm revoke, and Android kills the
app's process whenever a runtime permission it currently holds is revoked,
so a grant -> deny/reset sequence silently left the session on the launcher
and the next selector failed with no hint. The revoke path now reads the
prior grant state from dumpsys package first and, when it was granted,
returns wasGranted: true plus a warning naming open <app> --relaunch; the
settings CLI output renders response warnings, and commands.md documents
the behavior next to the pm revoke mapping.

Closes #1796

* fix(android): state the revoke-kill consequence conditionally

Review finding: the warning asserted the app had been killed, inferred only
from the prior grant state. dumpsys reports granted=true for any user
profile while pm revoke acts on the current one, and the app need not have
been running, so the claim could be false. State the platform rule and make
the consequence conditional; the relaunch guidance is unchanged.

* fix(android): model the prior grant state as granted/not_granted/unknown

A failed or unparseable dumpsys read as "nothing granted", so the response
asserted the app was untouched when the state was simply unknown, and the
grant scan matched every granted=true line — install permissions and other
users' blocks included — so another profile's grant could claim a kill that
never happened. Both directions of the same defect.

The read now resolves the acting user (am get-current-user) and walks the
dump's nesting (Packages: > User <id>: > runtime permissions:), and reports
priorGrantState: granted | not_granted | unknown. unknown carries the same
relaunch guidance without claiming what the state was; only not_granted is
silent.

* fix(android): address the foreground user in every permission mutation

The tri-state read scoped state to am get-current-user, but the mutations
ran bare pm grant/revoke and clear-permission-flags.
PackageManagerShellCommand defaults those to UserHandle.USER_SYSTEM, so on
a device whose foreground user is nonzero the command read one user's state
and edited user 0 — leaving the running app's permission untouched while
reporting on a user it did not change.

Proven on a Pixel 7 / API 36 emulator with the foreground user switched to
10: a bare pm revoke flipped User 0 to granted=false and left User 10
granted=true.

The foreground user is now resolved once and passed as --user to pm
grant/revoke, pm clear-permission-flags, and appops set, and the state read
takes that same id. When it cannot be resolved the mutation keeps the
platform default and the state is reported unknown rather than guessed.

* test(android): pin the user-scoped permission argv in the provider scenario

The scripted ADB provider answered only the unscoped pm grant/revoke form,
and the Settings contract asserted the unscoped transcript entry, so the
provider lane could not see which user a permission mutation addressed.

* test(android): extract the settings contract out of the lifecycle monolith

The user-scoped argv assertions pushed android-lifecycle.test.ts past its
size ratchet, whose instruction is to extract rather than grow a file over
the tripwire. assertAndroidSettingsContract moves to a sibling module and
the pin drops 1597 -> 1559.

* refactor(android): shrink the permission path to one concept per file

Size/design pass on the #1796 change:
- settings.ts was 505 lines (past the 500 extract-before-adding tripwire);
  the permission family moves to settings-permission.ts and the dispatcher
  drops to 265.
- permission-grant-state.ts loses topLevelSection (a nestedBlock with an
  indent-0 header), its single-use line reader, and androidPriorGrantState
  (one map lookup at its only production call site).
- the grants map narrows to 'granted' | 'not_granted': unknown was never a
  value, absence is what carries it, so the tests read the map directly.
- the permission tests move to settings-permission.test.ts and consolidate
  into argv/tri-state/photos/rejection tables; the parser tests fold seven
  cases into two.

Every red-proof re-run after the consolidation: dropping --user reds 7
argv/photos cases, and the pre-fix state model reds 13 across both files.

* fix(android): refuse permission mutations that cannot name their user

The fallback issued bare pm/appops commands when am get-current-user did
not answer, which is the #1796 defect itself: those default to
UserHandle.USER_SYSTEM, so a session running as user 10 had user 0 edited
while the response reported only priorGrantState: unknown. It was also a
fallback added without approval, and the docs' claim that every mutation
names its user was false on that path.

Resolving the acting user is now a prerequisite: setAndroidSetting
permission fails with COMMAND_FAILED and a recovery hint, issuing no pm,
appops or clear-permission-flags call at all. The test that locked the
fallback in is replaced by one asserting the empty mutation call list for
grant, deny and reset.
2026-08-19 17:35:47 +02:00
..