Repository navigation
Android settings permission all: classify pm refusals on typed reasons, not stderr substrings #2700
Description
Purpose
settings permission with target all on Android decides whether a failing pm call is a skip or an abort by matching lowercase substrings of adb stderr, and the photos path re-derives the same decision by re-running that sniff over another error's details.attempts. AGENTS.md requires behavior to key on typed reasons and details, never error text. This issue also folds in the per-unit accounting that is currently re-spelled in four functions.
Current shape
packages/platform-android/src/settings-permission.ts:
isSkippablePmStderr(~L338) — fivetext.includes(...)branches overnot a changeable permission,has not requested permission,is not a runtime permission,unknown permission,managed by role.tryPmUnit(~L307) — skippable stderr becomes{ ok: false, reason: firstStderrLine(stderr) }, i.e. the reason is the raw first stderr line, and the skip decision is already lost by the time a caller sees it.isSkippablePhotosError(~L364) — re-applies the same text sniff toerror.details.attempts.AllUnitContext/applyAllPermissionUnit/applyAllNotificationsUnit/applyAllPhotosUnit/applyAllPmUnit/finishAllUnit(~L197-L267) — every unit path destructures the same mutable context and repeats theapplied.push+warnIfRevokedaccounting, with skip warnings spelled separately in two of them.
Required behavior
pm exposes no machine-readable status code, so this is not a request to stop reading adb output. The requirement is one classification point:
-
A single owning function turns an adb result into a typed outcome. Suggested shape:
type AndroidPmUnitOutcome = | { kind: 'applied' } | { kind: 'skipped'; reason: AndroidPmSkipReason; detail: string } | { kind: 'aborted'; error: AppError }; type AndroidPmSkipReason = | 'not-changeable' | 'not-requested' | 'not-runtime-permission' | 'unknown-permission' | 'role-managed';
-
warningsstrings are rendered from the typed reason plus the detail line, so operator-facing text is unchanged while no consumer re-reads stderr. -
The photos path consumes the same typed outcome.
isSkippablePhotosErrorand the secondisSkippablePmStderrcall go away; a photos skip carries the per-candidate reasons. -
Operational failures (offline device, dropped transport, denied op) must keep aborting the fan-out — that is what stops
launchAppcontinuing half-applied. Keep it that way. -
Each declared unit owns its own mutation and its own
applied/warningsaccounting, or the code states why it cannot.allPermissionUnits(~L275) keeps the collapse it performs today: one photos probe, one appops path, directpmotherwise.
Observable completion
- No
.includes(over stderr anywhere insettings-permission.ts. pnpm vitest run packages/platform-android/src/__tests__/settings-permission.test.tsgreen, including the existing pins thatallskips install permissions, special ids such asMANAGE_EXTERNAL_STORAGE, custom ids, and role-managed ids likeWRITE_SETTINGSon API 36.- New tests assert the abort side per reason: an operational
pmfailure surfaces asCOMMAND_FAILEDand stops the sequence, with the earlier applied ids named. pnpm check:quickandpnpm formatclean.
Dependencies
No blocker. Independent of #2363, which deliberately left this out to avoid widening a maestro/ios change into the Android backend.
Activity
refactor(platform-android): classify pm refusals on typed reasons (#2700