Skip to content

Commit 47f2bbe

Browse files
committed
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.
1 parent 512c390 commit 47f2bbe

6 files changed

Lines changed: 614 additions & 841 deletions

File tree

Lines changed: 24 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import { test } from 'vitest';
22
import assert from 'node:assert/strict';
3-
import {
4-
androidPriorGrantState,
5-
parseAndroidRuntimePermissionGrants,
6-
} from '../permission-grant-state.ts';
3+
import { parseAndroidRuntimePermissionGrants } from '../permission-grant-state.ts';
74

85
// Captured from `adb shell dumpsys package com.callstack.agentdevicelab` on a Pixel 7 / API 36
96
// emulator, trimmed to the sections that decide the answer. The indentation is load-bearing:
@@ -12,9 +9,6 @@ import {
129
const DUMPSYS = [
1310
'Packages:',
1411
' Package [com.example.app] (5f3a1c2):',
15-
' userId=10234',
16-
' declared permissions:',
17-
' com.example.app.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION: prot=signature',
1812
' install permissions:',
1913
' android.permission.MODIFY_AUDIO_SETTINGS: granted=true',
2014
' android.permission.RECORD_AUDIO: granted=true',
@@ -36,45 +30,29 @@ const DUMPSYS = [
3630
' path: /data/app/~~abc==/com.example.app-def==',
3731
].join('\n');
3832

39-
test('runtime grants are read from the requested user only', () => {
40-
const user0 = parseAndroidRuntimePermissionGrants(DUMPSYS, 0);
41-
assert.equal(androidPriorGrantState(user0, 'android.permission.RECORD_AUDIO'), 'not_granted');
42-
assert.equal(androidPriorGrantState(user0, 'android.permission.CAMERA'), 'granted');
43-
44-
// The same permission, granted for another profile: `pm revoke` acts on the acting user, so
45-
// reporting user 10's grant for a user 0 revoke would claim a kill that never happened.
46-
const user10 = parseAndroidRuntimePermissionGrants(DUMPSYS, 10);
47-
assert.equal(androidPriorGrantState(user10, 'android.permission.RECORD_AUDIO'), 'granted');
48-
assert.equal(androidPriorGrantState(user10, 'android.permission.CAMERA'), 'unknown');
33+
// `undefined` is the answer for anything the dump does not place inside the requested user's
34+
// runtime block: an install-permission grant (`pm revoke` cannot touch it), another profile's
35+
// grant, or a permission that user never declared. The caller reports that as `unknown`.
36+
test.each([
37+
[0, 'android.permission.RECORD_AUDIO', 'not_granted', 'acting user overrides the install grant'],
38+
[0, 'android.permission.CAMERA', 'granted', 'acting user'],
39+
[10, 'android.permission.RECORD_AUDIO', 'granted', 'other profile, only when asked for'],
40+
[10, 'android.permission.CAMERA', undefined, 'absent for that user'],
41+
[0, 'android.permission.MODIFY_AUDIO_SETTINGS', undefined, 'install-only'],
42+
[0, 'android.permission.INTERNET', undefined, 'never mentioned'],
43+
] as const)('user %s reads %s as %s (%s)', (userId, permission, expected, _why) => {
44+
assert.equal(parseAndroidRuntimePermissionGrants(DUMPSYS, userId)?.get(permission), expected);
4945
});
5046

51-
test('install permissions never answer for a runtime permission', () => {
52-
// RECORD_AUDIO appears as `granted=true` in the install-permission section and `granted=false`
53-
// in user 0's runtime block. A scan that matched `granted=true` anywhere read the wrong one.
54-
const grants = parseAndroidRuntimePermissionGrants(DUMPSYS, 0);
55-
assert.equal(androidPriorGrantState(grants, 'android.permission.RECORD_AUDIO'), 'not_granted');
56-
assert.equal(
57-
androidPriorGrantState(grants, 'android.permission.MODIFY_AUDIO_SETTINGS'),
58-
'unknown',
59-
);
60-
assert.equal(androidPriorGrantState(grants, 'android.permission.INTERNET'), 'unknown');
61-
});
62-
63-
test('a user with no runtime-permission block is unknown, not empty', () => {
64-
assert.equal(parseAndroidRuntimePermissionGrants(DUMPSYS, 11), undefined);
65-
assert.equal(
66-
androidPriorGrantState(
67-
parseAndroidRuntimePermissionGrants(DUMPSYS, 11),
68-
'android.permission.RECORD_AUDIO',
69-
),
70-
'unknown',
71-
);
72-
});
73-
74-
test('unparseable output is unknown rather than not-granted', () => {
75-
for (const output of ['', 'Packages:\n <none>', 'Error: package not found']) {
76-
assert.equal(parseAndroidRuntimePermissionGrants(output, 0), undefined, output);
77-
}
47+
// A missing block is not an empty one. Every input here means "the device did not tell us",
48+
// which must stay distinguishable from "the app holds nothing".
49+
test.each([
50+
['no runtime block for that user', DUMPSYS, 11],
51+
['empty output', '', 0],
52+
['no Packages section', 'Activity Resolver Table:\n User 0:\n runtime permissions:', 0],
53+
['a package section without the user', 'Packages:\n Package [com.example.app] (abc):', 0],
54+
] as const)('%s reads as unknown', (_label, output, userId) => {
55+
assert.equal(parseAndroidRuntimePermissionGrants(output, userId), undefined);
7856
});
7957

8058
test('an empty runtime block answers not_granted for everything it could have listed', () => {
@@ -94,23 +72,6 @@ test('an empty runtime block answers not_granted for everything it could have li
9472
assert.equal(grants?.size, 0);
9573
});
9674

97-
test('a dump without a Packages section is unknown', () => {
98-
// Every `dumpsys package <pkg>` carries one (it sits after the resolver tables and Key Set
99-
// Manager). Its absence means the output is not the dump we can read, so the honest answer
100-
// is unknown — never the not_granted that would claim the app was left alone.
101-
const grants = parseAndroidRuntimePermissionGrants(
102-
[
103-
'Activity Resolver Table:',
104-
' Non-Data Actions:',
105-
' User 0: installed=true',
106-
' runtime permissions:',
107-
' android.permission.RECORD_AUDIO: granted=true, flags=[ USER_SET]',
108-
].join('\n'),
109-
0,
110-
);
111-
assert.equal(grants, undefined);
112-
});
113-
11475
test('sections after Packages: cannot reopen the scan', () => {
11576
// `Queries:` repeats `User 0:` with no grants, and `Shared users:` repeats runtime grant lines
11677
// for the shared uid. Merging either would flip RECORD_AUDIO to granted for a user whose own
@@ -127,6 +88,6 @@ test('sections after Packages: cannot reopen the scan', () => {
12788
0,
12889
);
12990

130-
assert.equal(androidPriorGrantState(grants, 'android.permission.RECORD_AUDIO'), 'not_granted');
131-
assert.equal(androidPriorGrantState(grants, 'android.permission.CAMERA'), 'granted');
91+
assert.equal(grants?.get('android.permission.RECORD_AUDIO'), 'not_granted');
92+
assert.equal(grants?.get('android.permission.CAMERA'), 'granted');
13293
});

0 commit comments

Comments
 (0)