Skip to content

Commit 5becc03

Browse files
committed
fix: use grouped permission listing for adb grants
1 parent 2e30e86 commit 5becc03

2 files changed

Lines changed: 145 additions & 21 deletions

File tree

packages/platform-android/src/__tests__/adb.test.ts

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -596,20 +596,54 @@ describe('getStartAppArgs', () => {
596596
expect(spawnSpy).toHaveBeenCalledTimes(2);
597597
});
598598

599-
it('grants permissions to an app', async () => {
599+
it('grants requested dangerous permissions to an app', async () => {
600600
const { grantPermissions } = await import('../adb.js');
601-
const spawnSpy = vi.spyOn(tools, 'spawn');
602-
spawnSpy.mockResolvedValue({
603-
stdout: '',
604-
} as Awaited<ReturnType<typeof tools.spawn>>);
601+
const spawnSpy = vi
602+
.spyOn(tools, 'spawn')
603+
.mockResolvedValueOnce({
604+
stdout: 'package:com.example.app\n',
605+
} as Awaited<ReturnType<typeof tools.spawn>>)
606+
.mockResolvedValueOnce({
607+
stdout: `requested permissions:\n android.permission.CAMERA\n android.permission.INTERNET\n android.permission.ACCESS_FINE_LOCATION\ninstall permissions:\n`,
608+
} as Awaited<ReturnType<typeof tools.spawn>>)
609+
.mockResolvedValueOnce({
610+
stdout: `Dangerous Permissions:\n permission:android.permission.CAMERA\n permission:android.permission.ACCESS_FINE_LOCATION\n`,
611+
} as Awaited<ReturnType<typeof tools.spawn>>)
612+
.mockResolvedValue({
613+
stdout: '',
614+
} as Awaited<ReturnType<typeof tools.spawn>>);
605615

606-
await grantPermissions('emulator-5554', 'com.example.app', [
607-
'android.permission.CAMERA',
608-
'android.permission.RECORD_AUDIO',
609-
]);
616+
await grantPermissions('emulator-5554', 'com.example.app');
610617

611-
expect(spawnSpy).toHaveBeenCalledTimes(2);
618+
expect(spawnSpy).toHaveBeenCalledTimes(5);
612619
expect(spawnSpy).toHaveBeenNthCalledWith(1, expect.any(String), [
620+
'-s',
621+
'emulator-5554',
622+
'shell',
623+
'pm',
624+
'list',
625+
'packages',
626+
'com.example.app',
627+
]);
628+
expect(spawnSpy).toHaveBeenNthCalledWith(2, expect.any(String), [
629+
'-s',
630+
'emulator-5554',
631+
'shell',
632+
'dumpsys',
633+
'package',
634+
'com.example.app',
635+
]);
636+
expect(spawnSpy).toHaveBeenNthCalledWith(3, expect.any(String), [
637+
'-s',
638+
'emulator-5554',
639+
'shell',
640+
'pm',
641+
'list',
642+
'permissions',
643+
'-g',
644+
'-d',
645+
]);
646+
expect(spawnSpy).toHaveBeenNthCalledWith(4, expect.any(String), [
613647
'-s',
614648
'emulator-5554',
615649
'shell',
@@ -618,23 +652,33 @@ describe('getStartAppArgs', () => {
618652
'com.example.app',
619653
'android.permission.CAMERA',
620654
]);
621-
expect(spawnSpy).toHaveBeenNthCalledWith(2, expect.any(String), [
655+
expect(spawnSpy).toHaveBeenNthCalledWith(5, expect.any(String), [
622656
'-s',
623657
'emulator-5554',
624658
'shell',
625659
'pm',
626660
'grant',
627661
'com.example.app',
628-
'android.permission.RECORD_AUDIO',
662+
'android.permission.ACCESS_FINE_LOCATION',
629663
]);
630664
});
631665

632-
it('handles empty permission list when granting permissions', async () => {
666+
it('does nothing when the app has no grantable dangerous permissions', async () => {
633667
const { grantPermissions } = await import('../adb.js');
634-
const spawnSpy = vi.spyOn(tools, 'spawn');
668+
const spawnSpy = vi
669+
.spyOn(tools, 'spawn')
670+
.mockResolvedValueOnce({
671+
stdout: 'package:com.example.app\n',
672+
} as Awaited<ReturnType<typeof tools.spawn>>)
673+
.mockResolvedValueOnce({
674+
stdout: `requested permissions:\n android.permission.INTERNET\ninstall permissions:\n`,
675+
} as Awaited<ReturnType<typeof tools.spawn>>)
676+
.mockResolvedValueOnce({
677+
stdout: `Dangerous Permissions:\n permission:android.permission.CAMERA\n`,
678+
} as Awaited<ReturnType<typeof tools.spawn>>);
635679

636-
await grantPermissions('emulator-5554', 'com.example.app', []);
680+
await grantPermissions('emulator-5554', 'com.example.app');
637681

638-
expect(spawnSpy).not.toHaveBeenCalled();
682+
expect(spawnSpy).toHaveBeenCalledTimes(3);
639683
});
640684
});

packages/platform-android/src/adb.ts

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,79 @@ export const getDeviceInfo = async (
378378
return { manufacturer, model };
379379
};
380380

381+
const getRequestedPermissions = async (
382+
adbId: string,
383+
bundleId: string,
384+
): Promise<string[]> => {
385+
const { stdout } = await spawn(getAdbBinaryPath(), [
386+
'-s',
387+
adbId,
388+
'shell',
389+
'dumpsys',
390+
'package',
391+
bundleId,
392+
]);
393+
394+
const requestedPermissions = new Set<string>();
395+
const lines = stdout.split('\n');
396+
let inRequestedPermissionsSection = false;
397+
398+
for (const line of lines) {
399+
const trimmedLine = line.trim();
400+
401+
if (trimmedLine === 'requested permissions:') {
402+
inRequestedPermissionsSection = true;
403+
continue;
404+
}
405+
406+
if (!inRequestedPermissionsSection) {
407+
continue;
408+
}
409+
410+
if (trimmedLine === '') {
411+
continue;
412+
}
413+
414+
if (trimmedLine.endsWith(':')) {
415+
break;
416+
}
417+
418+
if (/^[a-zA-Z0-9_.]+$/.test(trimmedLine)) {
419+
requestedPermissions.add(trimmedLine);
420+
continue;
421+
}
422+
423+
break;
424+
}
425+
426+
return [...requestedPermissions];
427+
};
428+
429+
const getDangerousPermissions = async (adbId: string): Promise<Set<string>> => {
430+
const { stdout } = await spawn(getAdbBinaryPath(), [
431+
'-s',
432+
adbId,
433+
'shell',
434+
'pm',
435+
'list',
436+
'permissions',
437+
'-g',
438+
'-d',
439+
]);
440+
441+
const dangerousPermissions = new Set<string>();
442+
443+
for (const match of stdout.matchAll(/permission:([a-zA-Z0-9_.]+)/g)) {
444+
const permission = match[1]?.trim();
445+
446+
if (permission) {
447+
dangerousPermissions.add(permission);
448+
}
449+
}
450+
451+
return dangerousPermissions;
452+
};
453+
381454
export const isBootCompleted = async (adbId: string): Promise<boolean> => {
382455
try {
383456
const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
@@ -738,17 +811,24 @@ export const getConnectedDevices = async (): Promise<AdbDevice[]> => {
738811
export const grantPermissions = async (
739812
adbId: string,
740813
bundleId: string,
741-
permissions: string[],
742814
): Promise<void> => {
743-
if (permissions.length === 0) {
744-
return;
745-
}
746-
747815
const isInstalled = await isAppInstalled(adbId, bundleId);
748816
if (!isInstalled) {
749817
throw new AdbAppNotInstalledError(bundleId, adbId);
750818
}
751819

820+
const [requestedPermissions, dangerousPermissions] = await Promise.all([
821+
getRequestedPermissions(adbId, bundleId),
822+
getDangerousPermissions(adbId),
823+
]);
824+
const permissions = requestedPermissions.filter((permission) =>
825+
dangerousPermissions.has(permission),
826+
);
827+
828+
if (permissions.length === 0) {
829+
return;
830+
}
831+
752832
const grantCommands = permissions.map((permission) => [
753833
'-s',
754834
adbId,

0 commit comments

Comments
 (0)