@yume-chan/android-bin 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.json +36 -0
  2. package/CHANGELOG.md +19 -1
  3. package/LICENSE +21 -0
  4. package/esm/bu.d.ts +20 -7
  5. package/esm/bu.d.ts.map +1 -1
  6. package/esm/bu.js +47 -8
  7. package/esm/bu.js.map +1 -1
  8. package/esm/bug-report.d.ts +82 -21
  9. package/esm/bug-report.d.ts.map +1 -1
  10. package/esm/bug-report.js +189 -55
  11. package/esm/bug-report.js.map +1 -1
  12. package/esm/cmd.d.ts +4 -6
  13. package/esm/cmd.d.ts.map +1 -1
  14. package/esm/cmd.js +39 -24
  15. package/esm/cmd.js.map +1 -1
  16. package/esm/demo-mode.d.ts +3 -3
  17. package/esm/demo-mode.d.ts.map +1 -1
  18. package/esm/demo-mode.js +14 -15
  19. package/esm/demo-mode.js.map +1 -1
  20. package/esm/dumpsys.d.ts +21 -0
  21. package/esm/dumpsys.d.ts.map +1 -0
  22. package/esm/dumpsys.js +82 -0
  23. package/esm/dumpsys.js.map +1 -0
  24. package/esm/index.d.ts +3 -0
  25. package/esm/index.d.ts.map +1 -1
  26. package/esm/index.js +3 -0
  27. package/esm/index.js.map +1 -1
  28. package/esm/logcat.d.ts +3 -2
  29. package/esm/logcat.d.ts.map +1 -1
  30. package/esm/logcat.js +34 -24
  31. package/esm/logcat.js.map +1 -1
  32. package/esm/overlay-display.d.ts +20 -3
  33. package/esm/overlay-display.d.ts.map +1 -1
  34. package/esm/overlay-display.js +34 -61
  35. package/esm/overlay-display.js.map +1 -1
  36. package/esm/pm.d.ts +25 -2
  37. package/esm/pm.d.ts.map +1 -1
  38. package/esm/pm.js +88 -17
  39. package/esm/pm.js.map +1 -1
  40. package/esm/settings.d.ts +21 -8
  41. package/esm/settings.d.ts.map +1 -1
  42. package/esm/settings.js +55 -19
  43. package/esm/settings.js.map +1 -1
  44. package/esm/string-format.d.ts +40 -0
  45. package/esm/string-format.d.ts.map +1 -0
  46. package/esm/string-format.js +153 -0
  47. package/esm/string-format.js.map +1 -0
  48. package/package.json +13 -8
  49. package/src/bu.ts +68 -14
  50. package/src/bug-report.ts +236 -70
  51. package/src/cmd.ts +67 -33
  52. package/src/demo-mode.ts +47 -43
  53. package/src/dumpsys.ts +91 -0
  54. package/src/index.ts +3 -0
  55. package/src/logcat.ts +63 -74
  56. package/src/overlay-display.ts +82 -85
  57. package/src/pm.ts +149 -27
  58. package/src/settings.ts +99 -50
  59. package/src/string-format.ts +199 -0
  60. package/tsconfig.build.json +11 -0
  61. package/tsconfig.build.tsbuildinfo +1 -1
@@ -2,6 +2,7 @@ import type { Adb } from "@yume-chan/adb";
2
2
  import { AdbCommandBase } from "@yume-chan/adb";
3
3
 
4
4
  import { Settings } from "./settings.js";
5
+ import { p } from "./string-format.js";
5
6
 
6
7
  export interface OverlayDisplayDeviceMode {
7
8
  width: number;
@@ -17,99 +18,95 @@ export interface OverlayDisplayDevice {
17
18
  }
18
19
 
19
20
  export class OverlayDisplay extends AdbCommandBase {
20
- private settings: Settings;
21
-
22
- public static readonly OVERLAY_DISPLAY_DEVICES_KEY =
23
- "overlay_display_devices";
21
+ #settings: Settings;
22
+
23
+ static readonly SETTING_KEY = "overlay_display_devices";
24
+
25
+ static readonly SETTING_FORMAT = p.separated(
26
+ ";",
27
+ p.sequence(
28
+ {
29
+ name: "modes",
30
+ format: p.separated(
31
+ "|",
32
+ p.sequence(
33
+ { name: "width", format: p.digits() },
34
+ p.literal("x"),
35
+ { name: "height", format: p.digits() },
36
+ p.literal("/"),
37
+ { name: "density", format: p.digits() },
38
+ ),
39
+ 1,
40
+ ),
41
+ },
42
+ {
43
+ name: "flags",
44
+ format: p.map(
45
+ p.repeated(
46
+ p.sequence(p.literal(","), {
47
+ name: "flag",
48
+ format: p.union(
49
+ p.literal("secure"),
50
+ p.literal("own_content_only"),
51
+ p.literal("show_system_decorations"),
52
+ ),
53
+ }),
54
+ ),
55
+ (value) => value.map((item) => item.flag),
56
+ (value) => value.map((item) => ({ flag: item })),
57
+ ),
58
+ },
59
+ ),
60
+ );
24
61
 
25
62
  constructor(adb: Adb) {
26
63
  super(adb);
27
- this.settings = new Settings(adb);
64
+ this.#settings = new Settings(adb);
28
65
  }
29
66
 
30
- public async get() {
31
- const devices: OverlayDisplayDevice[] = [];
32
-
33
- const settingString = await this.settings.get(
34
- "global",
35
- OverlayDisplay.OVERLAY_DISPLAY_DEVICES_KEY
36
- );
37
-
38
- for (const displayString of settingString.split(";")) {
39
- const [modesString, ...flagStrings] = displayString.split(",");
40
-
41
- if (!modesString) {
42
- continue;
43
- }
44
-
45
- const device: OverlayDisplayDevice = {
46
- modes: [],
47
- secure: false,
48
- ownContentOnly: false,
49
- showSystemDecorations: false,
50
- };
51
- for (const modeString of modesString.split("|")) {
52
- const match = modeString.match(/(\d+)x(\d+)\/(\d+)/);
53
- if (!match) {
54
- continue;
55
- }
56
- device.modes.push({
57
- width: parseInt(match[1]!, 10),
58
- height: parseInt(match[2]!, 10),
59
- density: parseInt(match[3]!, 10),
60
- });
61
- }
62
- if (device.modes.length === 0) {
63
- continue;
64
- }
65
-
66
- for (const flagString of flagStrings) {
67
- switch (flagString) {
68
- case "secure":
69
- device.secure = true;
70
- break;
71
- case "own_content_only":
72
- device.ownContentOnly = true;
73
- break;
74
- case "show_system_decorations":
75
- device.showSystemDecorations = true;
76
- break;
77
- }
78
- }
79
- devices.push(device);
80
- }
81
-
82
- return devices;
67
+ async get() {
68
+ return OverlayDisplay.SETTING_FORMAT.parse({
69
+ value: await this.#settings.get(
70
+ "global",
71
+ OverlayDisplay.SETTING_KEY,
72
+ ),
73
+ position: 0,
74
+ }).map((device) => ({
75
+ modes: device.modes,
76
+ secure: device.flags.includes("secure"),
77
+ ownContentOnly: device.flags.includes("own_content_only"),
78
+ showSystemDecorations: device.flags.includes(
79
+ "show_system_decorations",
80
+ ),
81
+ }));
83
82
  }
84
83
 
85
- public async set(devices: OverlayDisplayDevice[]) {
86
- let settingString = "";
87
- for (const device of devices) {
88
- if (settingString) {
89
- settingString += ";";
90
- }
91
-
92
- settingString += device.modes
93
- .map((mode) => `${mode.width}x${mode.height}/${mode.density}`)
94
- .join("|");
95
-
96
- if (device.secure) {
97
- settingString += ",secure";
98
- }
99
-
100
- if (device.ownContentOnly) {
101
- settingString += ",own_content_only";
102
- }
103
-
104
- if (device.showSystemDecorations) {
105
- settingString += ",show_system_decorations";
106
- }
107
- }
108
-
109
- await this.settings.put(
84
+ async set(devices: OverlayDisplayDevice[]) {
85
+ await this.#settings.put(
110
86
  "global",
111
- OverlayDisplay.OVERLAY_DISPLAY_DEVICES_KEY,
112
- settingString
87
+ OverlayDisplay.SETTING_KEY,
88
+ OverlayDisplay.SETTING_FORMAT.stringify(
89
+ devices.map((device) => {
90
+ const flags: (
91
+ | "secure"
92
+ | "own_content_only"
93
+ | "show_system_decorations"
94
+ )[] = [];
95
+ if (device.secure) {
96
+ flags.push("secure");
97
+ }
98
+ if (device.ownContentOnly) {
99
+ flags.push("own_content_only");
100
+ }
101
+ if (device.showSystemDecorations) {
102
+ flags.push("show_system_decorations");
103
+ }
104
+ return {
105
+ modes: device.modes,
106
+ flags,
107
+ };
108
+ }),
109
+ ),
113
110
  );
114
111
  }
115
112
  }
package/src/pm.ts CHANGED
@@ -177,25 +177,65 @@ export const PACKAGE_MANAGER_INSTALL_OPTIONS_MAP: Record<
177
177
  bypassLowTargetSdkBlock: "--bypass-low-target-sdk-block",
178
178
  };
179
179
 
180
+ export interface PackageManagerListPackagesOptions {
181
+ listDisabled: boolean;
182
+ listEnabled: boolean;
183
+ showSourceDir: boolean;
184
+ showInstaller: boolean;
185
+ listSystem: boolean;
186
+ showUid: boolean;
187
+ listThirdParty: boolean;
188
+ showVersionCode: boolean;
189
+ listApexOnly: boolean;
190
+ user: "all" | "current" | number;
191
+ uid: number;
192
+ filter: string;
193
+ }
194
+
195
+ export const PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP: Record<
196
+ keyof PackageManagerListPackagesOptions,
197
+ string
198
+ > = {
199
+ listDisabled: "-d",
200
+ listEnabled: "-e",
201
+ showSourceDir: "-f",
202
+ showInstaller: "-i",
203
+ listSystem: "-s",
204
+ showUid: "-U",
205
+ listThirdParty: "-3",
206
+ showVersionCode: "--show-versioncode",
207
+ listApexOnly: "--apex-only",
208
+ user: "--user",
209
+ uid: "--uid",
210
+ filter: "",
211
+ };
212
+
213
+ export interface PackageManagerListPackagesResult {
214
+ packageName: string;
215
+ sourceDir?: string | undefined;
216
+ versionCode?: number | undefined;
217
+ installer?: string | undefined;
218
+ uid?: number | undefined;
219
+ }
220
+
180
221
  export class PackageManager extends AdbCommandBase {
181
- private _cmd: Cmd;
222
+ #cmd: Cmd;
182
223
 
183
- public constructor(adb: Adb) {
224
+ constructor(adb: Adb) {
184
225
  super(adb);
185
- this._cmd = new Cmd(adb);
226
+ this.#cmd = new Cmd(adb);
186
227
  }
187
228
 
188
- private buildInstallArgs(
189
- options?: Partial<PackageManagerInstallOptions>
229
+ #buildArguments<T>(
230
+ commands: string[],
231
+ options: Partial<T> | undefined,
232
+ map: Record<keyof T, string>,
190
233
  ): string[] {
191
- const args = ["pm", "install"];
234
+ const args = ["pm", ...commands];
192
235
  if (options) {
193
236
  for (const [key, value] of Object.entries(options)) {
194
237
  if (value) {
195
- const option =
196
- PACKAGE_MANAGER_INSTALL_OPTIONS_MAP[
197
- key as keyof PackageManagerInstallOptions
198
- ];
238
+ const option = map[key as keyof T];
199
239
  if (option) {
200
240
  args.push(option);
201
241
  switch (typeof value) {
@@ -213,19 +253,29 @@ export class PackageManager extends AdbCommandBase {
213
253
  return args;
214
254
  }
215
255
 
216
- public async install(
256
+ #buildInstallArguments(
257
+ options: Partial<PackageManagerInstallOptions> | undefined,
258
+ ): string[] {
259
+ return this.#buildArguments(
260
+ ["install"],
261
+ options,
262
+ PACKAGE_MANAGER_INSTALL_OPTIONS_MAP,
263
+ );
264
+ }
265
+
266
+ async install(
217
267
  apks: string[],
218
- options?: Partial<PackageManagerInstallOptions>
268
+ options?: Partial<PackageManagerInstallOptions>,
219
269
  ): Promise<string> {
220
- const args = this.buildInstallArgs(options);
270
+ const args = this.#buildInstallArguments(options);
221
271
  // WIP: old version of pm doesn't support multiple apks
222
272
  args.push(...apks);
223
273
  return await this.adb.subprocess.spawnAndWaitLegacy(args);
224
274
  }
225
275
 
226
- public async pushAndInstallStream(
276
+ async pushAndInstallStream(
227
277
  stream: ReadableStream<Consumable<Uint8Array>>,
228
- options?: Partial<PackageManagerInstallOptions>
278
+ options?: Partial<PackageManagerInstallOptions>,
229
279
  ): Promise<ReadableStream<string>> {
230
280
  const sync = await this.adb.sync();
231
281
 
@@ -241,11 +291,15 @@ export class PackageManager extends AdbCommandBase {
241
291
  await sync.dispose();
242
292
  }
243
293
 
244
- const args = this.buildInstallArgs(options);
294
+ // Starting from Android 7, `pm` is a only wrapper for `cmd package`,
295
+ // and `cmd package` launches faster than `pm`.
296
+ // But `cmd package` can't read `/data/local/tmp` folder due to SELinux policy,
297
+ // so installing a file must use `pm`.
298
+ const args = this.#buildInstallArguments(options);
245
299
  args.push(filePath);
246
300
  const process = await AdbSubprocessNoneProtocol.raw(
247
301
  this.adb,
248
- args.map(escapeArg).join(" ")
302
+ args.map(escapeArg).join(" "),
249
303
  );
250
304
  return new WrapReadableStream({
251
305
  start: () => process.stdout.pipeThrough(new DecodeUtf8Stream()),
@@ -255,27 +309,95 @@ export class PackageManager extends AdbCommandBase {
255
309
  });
256
310
  }
257
311
 
258
- public async installStream(
312
+ async installStream(
259
313
  size: number,
260
314
  stream: ReadableStream<Consumable<Uint8Array>>,
261
- options?: Partial<PackageManagerInstallOptions>
315
+ options?: Partial<PackageManagerInstallOptions>,
262
316
  ): Promise<ReadableStream<string>> {
263
- if (!this._cmd.supportsCmd) {
317
+ // Android 7 added both `cmd` command and streaming install support,
318
+ // we can't detect whether `pm` supports streaming install,
319
+ // so we detect `cmd` command support instead.
320
+ if (!this.#cmd.supportsCmd) {
264
321
  return this.pushAndInstallStream(stream, options);
265
322
  }
266
323
 
267
- // Android 7 added `cmd package` and piping apk to stdin,
268
- // the source code suggests using `cmd package` over `pm`, but didn't say why.
269
- // However, `cmd package install` can't read `/data/local/tmp` folder due to SELinux policy,
270
- // so even ADB today is still using `pm install` for non-streaming installs.
271
- const args = this.buildInstallArgs(options);
272
- // Remove `pm` from args, final command will be `cmd package install`
324
+ const args = this.#buildInstallArguments(options);
325
+ // Remove `pm` from args, final command will starts with `cmd package install`
273
326
  args.shift();
274
327
  args.push("-S", size.toString());
275
- const process = await this._cmd.spawn(false, "package", ...args);
328
+ const process = await this.#cmd.spawn(false, "package", ...args);
276
329
  await stream.pipeTo(process.stdin);
277
330
  return process.stdout.pipeThrough(new DecodeUtf8Stream());
278
331
  }
279
332
 
333
+ static parsePackageListItem(
334
+ line: string,
335
+ ): PackageManagerListPackagesResult {
336
+ line = line.substring("package:".length);
337
+
338
+ let packageName: string;
339
+ let sourceDir: string | undefined;
340
+ let versionCode: number | undefined;
341
+ let installer: string | undefined;
342
+ let uid: number | undefined;
343
+
344
+ // Parse backwards
345
+ let index = line.indexOf(" uid:");
346
+ if (index !== -1) {
347
+ uid = Number.parseInt(line.substring(index + " uid:".length), 10);
348
+ line = line.substring(0, index);
349
+ }
350
+
351
+ index = line.indexOf(" installer=");
352
+ if (index !== -1) {
353
+ installer = line.substring(index + " installer=".length);
354
+ line = line.substring(0, index);
355
+ }
356
+
357
+ index = line.indexOf(" versionCode:");
358
+ if (index !== -1) {
359
+ versionCode = Number.parseInt(
360
+ line.substring(index + " versionCode:".length),
361
+ 10,
362
+ );
363
+ line = line.substring(0, index);
364
+ }
365
+
366
+ // `sourceDir` may contain `=` so use `lastIndexOf`
367
+ index = line.lastIndexOf("=");
368
+ if (index !== -1) {
369
+ sourceDir = line.substring(0, index);
370
+ packageName = line.substring(index + "=".length);
371
+ } else {
372
+ packageName = line;
373
+ }
374
+
375
+ return {
376
+ packageName,
377
+ sourceDir,
378
+ versionCode,
379
+ installer,
380
+ uid,
381
+ };
382
+ }
383
+
384
+ async listPackages(
385
+ options?: Partial<PackageManagerListPackagesOptions>,
386
+ ): Promise<PackageManagerListPackagesResult[]> {
387
+ const args = this.#buildArguments(
388
+ ["list", "packages"],
389
+ options,
390
+ PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP,
391
+ );
392
+ if (options?.filter) {
393
+ args.push(options.filter);
394
+ }
395
+ const output = await this.adb.subprocess.spawnAndWaitLegacy(args);
396
+ return output
397
+ .split("\n")
398
+ .filter((line) => !!line)
399
+ .map((line) => PackageManager.parsePackageListItem(line));
400
+ }
401
+
280
402
  // TODO: install: support split apk formats (`adb install-multiple`)
281
403
  }
package/src/settings.ts CHANGED
@@ -1,78 +1,127 @@
1
+ import type { Adb, AdbSubprocessWaitResult } from "@yume-chan/adb";
1
2
  import { AdbCommandBase } from "@yume-chan/adb";
2
3
 
4
+ import { Cmd } from "./cmd.js";
5
+
3
6
  export type SettingsNamespace = "system" | "secure" | "global";
4
7
 
5
- export type SettingsResetMode =
6
- | "untrusted_defaults"
7
- | "untrusted_clear"
8
- | "trusted_defaults";
8
+ export enum SettingsResetMode {
9
+ UntrustedDefaults = "untrusted_defaults",
10
+ UntrustedClear = "untrusted_clear",
11
+ TrustedDefaults = "trusted_defaults",
12
+ }
13
+
14
+ export interface SettingsOptions {
15
+ user?: number | "current";
16
+ }
17
+
18
+ export interface SettingsPutOptions extends SettingsOptions {
19
+ tag?: string;
20
+ makeDefault?: boolean;
21
+ }
9
22
 
10
23
  // frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/SettingsService.java
11
24
  export class Settings extends AdbCommandBase {
12
- // TODO: `--user <user>` argument
25
+ #cmd: Cmd;
13
26
 
14
- public base(
15
- command: string,
27
+ constructor(adb: Adb) {
28
+ super(adb);
29
+ this.#cmd = new Cmd(adb);
30
+ }
31
+
32
+ async base(
33
+ verb: string,
16
34
  namespace: SettingsNamespace,
35
+ options: SettingsOptions | undefined,
17
36
  ...args: string[]
18
- ) {
19
- return this.adb.subprocess.spawnAndWaitLegacy([
20
- "settings",
21
- command,
22
- namespace,
23
- ...args,
24
- ]);
37
+ ): Promise<string> {
38
+ let command = ["settings"];
39
+
40
+ if (options?.user !== undefined) {
41
+ command.push("--user", options.user.toString());
42
+ }
43
+
44
+ command.push(verb, namespace);
45
+ command = command.concat(args);
46
+
47
+ let output: AdbSubprocessWaitResult;
48
+ if (this.#cmd.supportsCmd) {
49
+ output = await this.#cmd.spawnAndWait(
50
+ command[0]!,
51
+ ...command.slice(1),
52
+ );
53
+ } else {
54
+ output = await this.adb.subprocess.spawnAndWait(command);
55
+ }
56
+
57
+ if (output.stderr) {
58
+ throw new Error(output.stderr);
59
+ }
60
+
61
+ return output.stdout;
25
62
  }
26
63
 
27
- public get(namespace: SettingsNamespace, key: string) {
28
- return this.base("get", namespace, key);
64
+ async get(
65
+ namespace: SettingsNamespace,
66
+ key: string,
67
+ options?: SettingsOptions,
68
+ ) {
69
+ const output = await this.base("get", namespace, options, key);
70
+ // Remove last \n
71
+ return output.substring(0, output.length - 1);
29
72
  }
30
73
 
31
- public delete(namespace: SettingsNamespace, key: string) {
32
- return this.base("delete", namespace, key);
74
+ async delete(
75
+ namespace: SettingsNamespace,
76
+ key: string,
77
+ options?: SettingsOptions,
78
+ ): Promise<void> {
79
+ await this.base("delete", namespace, options, key);
33
80
  }
34
81
 
35
- public put(
82
+ async put(
36
83
  namespace: SettingsNamespace,
37
84
  key: string,
38
85
  value: string,
39
- tag?: string,
40
- makeDefault?: boolean
41
- ) {
42
- return this.base(
43
- "put",
44
- namespace,
45
- key,
46
- value,
47
- ...(tag ? [tag] : []),
48
- ...(makeDefault ? ["default"] : [])
49
- );
86
+ options?: SettingsPutOptions,
87
+ ): Promise<void> {
88
+ const args = [key, value];
89
+ if (options?.tag) {
90
+ args.push(options.tag);
91
+ }
92
+ if (options?.makeDefault) {
93
+ args.push("default");
94
+ }
95
+ await this.base("put", namespace, options, ...args);
50
96
  }
51
97
 
52
- public reset(
98
+ reset(
53
99
  namespace: SettingsNamespace,
54
- mode: SettingsResetMode
55
- ): Promise<string>;
56
- public reset(
100
+ mode: SettingsResetMode,
101
+ options?: SettingsOptions,
102
+ ): Promise<void>;
103
+ reset(
57
104
  namespace: SettingsNamespace,
58
105
  packageName: string,
59
- tag?: string
60
- ): Promise<string>;
61
- public reset(
106
+ tag?: string,
107
+ options?: SettingsOptions,
108
+ ): Promise<void>;
109
+ async reset(
62
110
  namespace: SettingsNamespace,
63
111
  modeOrPackageName: string,
64
- tag?: string
65
- ): Promise<string> {
66
- return this.base(
67
- "reset",
68
- namespace,
69
- modeOrPackageName,
70
- ...(tag ? [tag] : [])
71
- );
72
- }
73
-
74
- public async list(namespace: SettingsNamespace): Promise<string[]> {
75
- const output = await this.base("list", namespace);
76
- return output.split("\n");
112
+ tagOrOptions?: string | SettingsOptions,
113
+ options?: SettingsOptions,
114
+ ): Promise<void> {
115
+ const args = [modeOrPackageName];
116
+ if (
117
+ modeOrPackageName === SettingsResetMode.UntrustedDefaults ||
118
+ modeOrPackageName === SettingsResetMode.UntrustedClear ||
119
+ modeOrPackageName === SettingsResetMode.TrustedDefaults
120
+ ) {
121
+ options = tagOrOptions as SettingsOptions;
122
+ } else if (typeof tagOrOptions === "string") {
123
+ args.push(tagOrOptions);
124
+ }
125
+ await this.base("reset", namespace, options, ...args);
77
126
  }
78
127
  }