Skip to content

Commit b2ad407

Browse files
committed
fix(android): fall back to per-user package listing when output is empty
`executeShellCommand` never rejects on a non-zero exit code, so catching an error around `pm list packages` could not detect the SecurityException some Samsung devices raise for the Secure Folder user (150). Detect the failure from the empty package output instead, list packages per user, deduplicate the result, and keep using the per-user path for the rest of the session since the installed-apps check is polled frequently.
1 parent c4560b9 commit b2ad407

2 files changed

Lines changed: 169 additions & 84 deletions

File tree

lib/common/mobile/android/android-application-manager.ts

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
export class AndroidApplicationManager extends ApplicationManagerBase {
2424
public PID_CHECK_INTERVAL = 100;
2525
public PID_CHECK_TIMEOUT = 10000; // 10 secs
26+
private listPackagesPerUser = false;
2627

2728
constructor(
2829
private adb: Mobile.IDeviceAndroidDebugBridge,
@@ -42,61 +43,55 @@ export class AndroidApplicationManager extends ApplicationManagerBase {
4243
}
4344

4445
public async getInstalledApplications(): Promise<string[]> {
45-
let result = "";
46-
try {
47-
result = await this.adb.executeShellCommand(["pm", "list", "packages"]);
48-
} catch (err) {
49-
/**
50-
* on some devices (Samsung) listing packages is prevented by a permission error
51-
* notably, some system apps (bloatware) is installed under user 150
52-
* and listing these packages results in a permission error.
53-
* if this happens, we have to first list all the users, and then loop through
54-
* all the users and trying to list packages for that specific user, ignoring
55-
* any errors. These are all then concatenated together and parsed normally.
56-
* This is a slower operation, so we only do it in case listing failed in the first place.
57-
*/
58-
const userIDs: string[] = [];
59-
const users = await this.adb.executeShellCommand(["pm", "list", "users"]);
60-
/**
61-
* Users:
62-
* UserInfo{0:Owner:c13} running
63-
*/
64-
65-
const userIDRegex = /UserInfo{(\d+)[:}]/;
66-
users.split(EOL).forEach((line: string) => {
67-
const [, userID] = line.match(userIDRegex) ?? [];
68-
69-
if (userID) {
70-
userIDs.push(userID);
71-
}
72-
});
73-
74-
for (let id of userIDs) {
75-
try {
76-
result +=
77-
EOL +
78-
(await this.adb.executeShellCommand([
79-
"pm",
80-
"list",
81-
"packages",
82-
"--user",
83-
id,
84-
]));
85-
} catch (err) {
86-
// ignore - likely permission denied.
87-
}
46+
if (!this.listPackagesPerUser) {
47+
const packages = this.parsePackageList(
48+
await this.adb.executeShellCommand(["pm", "list", "packages"]),
49+
);
50+
if (packages.length) {
51+
return packages;
8852
}
8953
}
54+
55+
// Listing without `--user` walks every user and prints nothing when shell
56+
// is denied access to one of them (e.g. Samsung's Secure Folder, user 150)
57+
// without rejecting. Listing per user only loses the inaccessible ones.
58+
this.listPackagesPerUser = true;
59+
const packages: string[] = [];
60+
for (const userId of await this.getUserIds()) {
61+
packages.push(
62+
...this.parsePackageList(
63+
await this.adb.executeShellCommand([
64+
"pm",
65+
"list",
66+
"packages",
67+
"--user",
68+
userId,
69+
]),
70+
),
71+
);
72+
}
73+
74+
return _.uniq(packages);
75+
}
76+
77+
private parsePackageList(output: string): string[] {
9078
const regex = /package:(.+)/;
91-
return result
79+
return (output || "")
9280
.split(EOL)
93-
.map((packageString: string) => {
94-
const match = packageString.match(regex);
81+
.map((line: string) => {
82+
const match = line.match(regex);
9583
return match ? match[1] : null;
9684
})
9785
.filter((parsedPackage: string) => parsedPackage !== null);
9886
}
9987

88+
private async getUserIds(): Promise<string[]> {
89+
const output: string =
90+
(await this.adb.executeShellCommand(["pm", "list", "users"])) || "";
91+
const regex = /UserInfo\{(\d+):/g;
92+
return Array.from(output.matchAll(regex), (match) => match[1]);
93+
}
94+
10095
@hook("install")
10196
public async installApplication(
10297
packageFilePath: string,

0 commit comments

Comments
 (0)