@yume-chan/android-bin 2.1.0 → 3.0.0-beta.1

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 (96) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +4 -1
  3. package/esm/am.d.ts +20 -15
  4. package/esm/am.d.ts.map +1 -1
  5. package/esm/am.js +60 -13
  6. package/esm/am.js.map +1 -1
  7. package/esm/bu.d.ts +22 -18
  8. package/esm/bu.d.ts.map +1 -1
  9. package/esm/bu.js +10 -4
  10. package/esm/bu.js.map +1 -1
  11. package/esm/bug-report.d.ts +21 -20
  12. package/esm/bug-report.d.ts.map +1 -1
  13. package/esm/bug-report.js +15 -21
  14. package/esm/bug-report.js.map +1 -1
  15. package/esm/cmd/index.d.ts +2 -0
  16. package/esm/cmd/index.d.ts.map +1 -0
  17. package/esm/cmd/index.js +2 -0
  18. package/esm/cmd/index.js.map +1 -0
  19. package/esm/cmd/none.d.ts +5 -0
  20. package/esm/cmd/none.d.ts.map +1 -0
  21. package/esm/cmd/none.js +39 -0
  22. package/esm/cmd/none.js.map +1 -0
  23. package/esm/cmd/service.d.ts +29 -0
  24. package/esm/cmd/service.d.ts.map +1 -0
  25. package/esm/cmd/service.js +24 -0
  26. package/esm/cmd/service.js.map +1 -0
  27. package/esm/cmd/shell.d.ts +5 -0
  28. package/esm/cmd/shell.d.ts.map +1 -0
  29. package/esm/cmd/shell.js +43 -0
  30. package/esm/cmd/shell.js.map +1 -0
  31. package/esm/cmd/utils.d.ts +5 -0
  32. package/esm/cmd/utils.d.ts.map +1 -0
  33. package/esm/cmd/utils.js +24 -0
  34. package/esm/cmd/utils.js.map +1 -0
  35. package/esm/demo-mode.d.ts +3 -4
  36. package/esm/demo-mode.d.ts.map +1 -1
  37. package/esm/demo-mode.js +13 -21
  38. package/esm/demo-mode.js.map +1 -1
  39. package/esm/dumpsys.d.ts +4 -2
  40. package/esm/dumpsys.d.ts.map +1 -1
  41. package/esm/dumpsys.js +13 -10
  42. package/esm/dumpsys.js.map +1 -1
  43. package/esm/index.d.ts +1 -1
  44. package/esm/index.d.ts.map +1 -1
  45. package/esm/index.js +1 -1
  46. package/esm/index.js.map +1 -1
  47. package/esm/intent.d.ts +37 -9
  48. package/esm/intent.d.ts.map +1 -1
  49. package/esm/intent.js +116 -53
  50. package/esm/intent.js.map +1 -1
  51. package/esm/logcat.d.ts +4 -2
  52. package/esm/logcat.d.ts.map +1 -1
  53. package/esm/logcat.js +9 -6
  54. package/esm/logcat.js.map +1 -1
  55. package/esm/overlay-display.d.ts +17 -14
  56. package/esm/overlay-display.d.ts.map +1 -1
  57. package/esm/overlay-display.js +2 -4
  58. package/esm/overlay-display.js.map +1 -1
  59. package/esm/pm.d.ts +131 -168
  60. package/esm/pm.d.ts.map +1 -1
  61. package/esm/pm.js +229 -112
  62. package/esm/pm.js.map +1 -1
  63. package/esm/settings.d.ts +1 -2
  64. package/esm/settings.d.ts.map +1 -1
  65. package/esm/settings.js +9 -9
  66. package/esm/settings.js.map +1 -1
  67. package/esm/utils.d.ts +4 -1
  68. package/esm/utils.d.ts.map +1 -1
  69. package/esm/utils.js +24 -13
  70. package/esm/utils.js.map +1 -1
  71. package/package.json +9 -9
  72. package/src/am.ts +94 -34
  73. package/src/bu.ts +31 -20
  74. package/src/bug-report.ts +42 -42
  75. package/src/cmd/index.ts +1 -0
  76. package/src/cmd/none.ts +62 -0
  77. package/src/cmd/service.ts +54 -0
  78. package/src/cmd/shell.ts +67 -0
  79. package/src/cmd/utils.ts +35 -0
  80. package/src/demo-mode.ts +13 -24
  81. package/src/dumpsys.ts +16 -10
  82. package/src/index.ts +1 -1
  83. package/src/intent.ts +187 -49
  84. package/src/logcat.ts +12 -6
  85. package/src/overlay-display.ts +21 -19
  86. package/src/pm.ts +374 -301
  87. package/src/settings.ts +11 -10
  88. package/src/utils.ts +30 -13
  89. package/tsconfig.build.json +0 -11
  90. package/CHANGELOG.md +0 -181
  91. package/esm/cmd.d.ts +0 -23
  92. package/esm/cmd.d.ts.map +0 -1
  93. package/esm/cmd.js +0 -104
  94. package/esm/cmd.js.map +0 -1
  95. package/src/cmd.ts +0 -166
  96. package/tsconfig.build.tsbuildinfo +0 -1
package/src/pm.ts CHANGED
@@ -2,9 +2,10 @@
2
2
  // cspell:ignore instantapp
3
3
  // cspell:ignore apks
4
4
  // cspell:ignore versioncode
5
+ // cspell:ignore dexopt
5
6
 
6
- import type { Adb } from "@yume-chan/adb";
7
- import { AdbServiceBase } from "@yume-chan/adb";
7
+ import type { Adb, AdbNoneProtocolProcess } from "@yume-chan/adb";
8
+ import { escapeArg } from "@yume-chan/adb";
8
9
  import type { MaybeConsumable, ReadableStream } from "@yume-chan/stream-extra";
9
10
  import {
10
11
  ConcatStringStream,
@@ -12,10 +13,11 @@ import {
12
13
  TextDecoderStream,
13
14
  } from "@yume-chan/stream-extra";
14
15
 
15
- import { CmdNoneProtocolService } from "./cmd.js";
16
- import type { IntentBuilder } from "./intent.js";
17
- import type { SingleUserOrAll } from "./utils.js";
18
- import { buildArguments } from "./utils.js";
16
+ import { Cmd } from "./cmd/index.js";
17
+ import type { Intent } from "./intent.js";
18
+ import { serializeIntent } from "./intent.js";
19
+ import type { Optional, SingleUserOrAll } from "./utils.js";
20
+ import { buildCommand } from "./utils.js";
19
21
 
20
22
  export enum PackageManagerInstallLocation {
21
23
  Auto,
@@ -31,173 +33,83 @@ export enum PackageManagerInstallReason {
31
33
  UserRequest,
32
34
  }
33
35
 
34
- // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java;l=3046;drc=6d14d35d0241f6fee145f8e54ffd77252e8d29fd
35
- export interface PackageManagerInstallOptions {
36
- /**
37
- * `-R`
38
- */
39
- skipExisting: boolean;
40
- /**
41
- * `-i`
42
- */
43
- installerPackageName: string;
44
- /**
45
- * `-t`
46
- */
47
- allowTest: boolean;
48
- /**
49
- * `-f`
50
- */
51
- internalStorage: boolean;
52
- /**
53
- * `-d`
54
- */
55
- requestDowngrade: boolean;
56
- /**
57
- * `-g`
58
- */
59
- grantRuntimePermissions: boolean;
60
- /**
61
- * `--restrict-permissions`
62
- */
63
- restrictPermissions: boolean;
64
- /**
65
- * `--dont-kill`
66
- */
67
- doNotKill: boolean;
68
- /**
69
- * `--originating-uri`
70
- */
71
- originatingUri: string;
72
- /**
73
- * `--referrer`
74
- */
75
- refererUri: string;
76
- /**
77
- * `-p`
78
- */
79
- inheritFrom: string;
80
- /**
81
- * `--pkg`
82
- */
83
- packageName: string;
84
- /**
85
- * `--abi`
86
- */
87
- abi: string;
88
- /**
89
- * `--ephemeral`/`--instant`/`--instantapp`
90
- */
91
- instantApp: boolean;
92
- /**
93
- * `--full`
94
- */
95
- full: boolean;
96
- /**
97
- * `--preload`
98
- */
99
- preload: boolean;
100
- /**
101
- * `--user`
102
- */
103
- user: SingleUserOrAll;
104
- /**
105
- * `--install-location`
106
- */
107
- installLocation: PackageManagerInstallLocation;
108
- /**
109
- * `--install-reason`
110
- */
111
- installReason: PackageManagerInstallReason;
112
- /**
113
- * `--force-uuid`
114
- */
115
- forceUuid: string;
116
- /**
117
- * `--apex`
118
- */
119
- apex: boolean;
120
- /**
121
- * `--force-non-staged`
122
- */
123
- forceNonStaged: boolean;
124
- /**
125
- * `--staged`
126
- */
127
- staged: boolean;
128
- /**
129
- * `--force-queryable`
130
- */
131
- forceQueryable: boolean;
132
- /**
133
- * `--enable-rollback`
134
- */
135
- enableRollback: boolean;
136
- /**
137
- * `--staged-ready-timeout`
138
- */
139
- stagedReadyTimeout: number;
140
- /**
141
- * `--skip-verification`
142
- */
143
- skipVerification: boolean;
144
- /**
145
- * `--bypass-low-target-sdk-block`
146
- */
147
- bypassLowTargetSdkBlock: boolean;
36
+ interface OptionDefinition<T> {
37
+ type: T;
38
+ name: string;
39
+ minApiLevel?: number;
40
+ maxApiLevel?: number;
148
41
  }
149
42
 
150
- export const PACKAGE_MANAGER_INSTALL_OPTIONS_MAP: Record<
151
- keyof PackageManagerInstallOptions,
152
- string
153
- > = {
154
- skipExisting: "-R",
155
- installerPackageName: "-i",
156
- allowTest: "-t",
157
- internalStorage: "-f",
158
- requestDowngrade: "-d",
159
- grantRuntimePermissions: "-g",
160
- restrictPermissions: "--restrict-permissions",
161
- doNotKill: "--dont-kill",
162
- originatingUri: "--originating-uri",
163
- refererUri: "--referrer",
164
- inheritFrom: "-p",
165
- packageName: "--pkg",
166
- abi: "--abi",
167
- instantApp: "--instant",
168
- full: "--full",
169
- preload: "--preload",
170
- user: "--user",
171
- installLocation: "--install-location",
172
- installReason: "--install-reason",
173
- forceUuid: "--force-uuid",
174
- apex: "--apex",
175
- forceNonStaged: "--force-non-staged",
176
- staged: "--staged",
177
- forceQueryable: "--force-queryable",
178
- enableRollback: "--enable-rollback",
179
- stagedReadyTimeout: "--staged-ready-timeout",
180
- skipVerification: "--skip-verification",
181
- bypassLowTargetSdkBlock: "--bypass-low-target-sdk-block",
182
- };
183
-
184
- export interface PackageManagerListPackagesOptions {
185
- listDisabled: boolean;
186
- listEnabled: boolean;
187
- showSourceDir: boolean;
188
- showInstaller: boolean;
189
- listSystem: boolean;
190
- showUid: boolean;
191
- listThirdParty: boolean;
192
- showVersionCode: boolean;
193
- listApexOnly: boolean;
194
- user: SingleUserOrAll;
195
- uid: number;
196
- filter: string;
43
+ function option<T>(
44
+ name: string,
45
+ minApiLevel?: number,
46
+ maxApiLevel?: number,
47
+ ): OptionDefinition<T> {
48
+ return {
49
+ name,
50
+ minApiLevel,
51
+ maxApiLevel,
52
+ } as OptionDefinition<T>;
197
53
  }
198
54
 
199
- export const PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP: Record<
200
- keyof PackageManagerListPackagesOptions,
55
+ // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java;l=3046;drc=6d14d35d0241f6fee145f8e54ffd77252e8d29fd
56
+ const InstallOptionsDefinitions = {
57
+ forwardLock: option<boolean>("-l", undefined, 28),
58
+ replaceExisting: option<boolean>("-r", undefined, 27),
59
+ skipExisting: option<boolean>("-R", 28),
60
+ installerPackageName: option<string>("-i"),
61
+ allowTest: option<boolean>("-t"),
62
+ externalStorage: option<boolean>("-s", undefined, 28),
63
+ internalStorage: option<boolean>("-f"),
64
+ requestDowngrade: option<boolean>("-d"),
65
+ grantRuntimePermissions: option<boolean>("-g", 23),
66
+ restrictPermissions: option<boolean>("--restrict-permissions", 29),
67
+ doNotKill: option<boolean>("--dont-kill"),
68
+ originatingUri: option<string>("--originating-uri"),
69
+ referrerUri: option<string>("--referrer"),
70
+ inheritFrom: option<string>("-p", 24),
71
+ packageName: option<string>("--pkg", 28),
72
+ abi: option<string>("--abi", 21),
73
+ instantApp: option<boolean>("--ephemeral", 24),
74
+ full: option<boolean>("--full", 26),
75
+ preload: option<boolean>("--preload", 28),
76
+ user: option<SingleUserOrAll>("--user", 21),
77
+ installLocation: option<PackageManagerInstallLocation>(
78
+ "--install-location",
79
+ 24,
80
+ ),
81
+ installReason: option<PackageManagerInstallReason>("--install-reason", 29),
82
+ updateOwnership: option<boolean>("--update-ownership", 34),
83
+ forceUuid: option<string>("--force-uuid", 24),
84
+ forceSdk: option<number>("--force-sdk", 24),
85
+ apex: option<boolean>("--apex", 29),
86
+ forceNonStaged: option<boolean>("--force-non-staged", 31),
87
+ multiPackage: option<boolean>("--multi-package", 29),
88
+ staged: option<boolean>("--staged", 29),
89
+ nonStaged: option<boolean>("--non-staged", 35),
90
+ forceQueryable: option<boolean>("--force-queryable", 30),
91
+ enableRollback: option<boolean | number>("--enable-rollback", 29),
92
+ rollbackImpactLevel: option<number>("--rollback-impact-level", 35),
93
+ wait: option<boolean | number>("--wait", 30, 30),
94
+ noWait: option<boolean>("--no-wait", 30, 30),
95
+ stagedReadyTimeout: option<number>("--staged-ready-timeout", 31),
96
+ skipVerification: option<boolean>("--skip-verification", 30),
97
+ skipEnable: option<boolean>("--skip-enable", 34),
98
+ bypassLowTargetSdkBlock: option<boolean>(
99
+ "--bypass-low-target-sdk-block",
100
+ 34,
101
+ ),
102
+ ignoreDexoptProfile: option<boolean>("--ignore-dexopt-profile", 35),
103
+ packageSource: option<number>("--package-source", 35),
104
+ dexoptCompilerFilter: option<string>("--dexopt-compiler-filter", 35),
105
+ disableAutoInstallDependencies: option<boolean>(
106
+ "--disable-auto-install-dependencies",
107
+ 36,
108
+ ),
109
+ } as const;
110
+
111
+ const ListPackagesOptionsMap: Record<
112
+ keyof PackageManager.ListPackagesOptions,
201
113
  string
202
114
  > = {
203
115
  listDisabled: "-d",
@@ -214,28 +126,8 @@ export const PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP: Record<
214
126
  filter: "",
215
127
  };
216
128
 
217
- export interface PackageManagerListPackagesResult {
218
- packageName: string;
219
- sourceDir?: string | undefined;
220
- versionCode?: number | undefined;
221
- installer?: string | undefined;
222
- uid?: number | undefined;
223
- }
224
-
225
- export interface PackageManagerUninstallOptions {
226
- keepData: boolean;
227
- user: SingleUserOrAll;
228
- versionCode: number;
229
- /**
230
- * Only remove the specified splits, not the entire app
231
- *
232
- * On Android 10 and lower, only one split name can be specified.
233
- */
234
- splitNames: readonly string[];
235
- }
236
-
237
- const PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP: Record<
238
- keyof PackageManagerUninstallOptions,
129
+ const UninstallOptionsMap: Record<
130
+ keyof PackageManager.UninstallOptions,
239
131
  string
240
132
  > = {
241
133
  keepData: "-k",
@@ -244,26 +136,19 @@ const PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP: Record<
244
136
  splitNames: "",
245
137
  };
246
138
 
247
- export interface PackageManagerResolveActivityOptions {
248
- user?: SingleUserOrAll;
249
- intent: IntentBuilder;
250
- }
251
-
252
- const PACKAGE_MANAGER_RESOLVE_ACTIVITY_OPTIONS_MAP: Partial<
253
- Record<keyof PackageManagerResolveActivityOptions, string>
139
+ const ResolveActivityOptionsMap: Partial<
140
+ Record<keyof PackageManager.ResolveActivityOptions, string>
254
141
  > = {
255
142
  user: "--user",
256
143
  };
257
144
 
258
- function buildInstallArguments(
145
+ function buildInstallCommand(
259
146
  command: string,
260
- options: Partial<PackageManagerInstallOptions> | undefined,
147
+ options: PackageManager.InstallOptions | undefined,
148
+ apiLevel: number | undefined,
261
149
  ): string[] {
262
- const args = buildArguments(
263
- [PackageManager.ServiceName, command],
264
- options,
265
- PACKAGE_MANAGER_INSTALL_OPTIONS_MAP,
266
- );
150
+ const args = [PackageManager.ServiceName, command];
151
+
267
152
  if (!options?.skipExisting) {
268
153
  /*
269
154
  * | behavior | previous version | modern version |
@@ -278,18 +163,74 @@ function buildInstallArguments(
278
163
  */
279
164
  args.push("-r");
280
165
  }
166
+
167
+ if (!options) {
168
+ return args;
169
+ }
170
+
171
+ for (const [key, value] of Object.entries(options)) {
172
+ if (value === undefined || value === null) {
173
+ continue;
174
+ }
175
+
176
+ const option =
177
+ InstallOptionsDefinitions[
178
+ key as keyof PackageManager.InstallOptions
179
+ ];
180
+
181
+ if (option === undefined) {
182
+ continue;
183
+ }
184
+
185
+ if (apiLevel !== undefined) {
186
+ if (
187
+ option.minApiLevel !== undefined &&
188
+ apiLevel < option.minApiLevel
189
+ ) {
190
+ continue;
191
+ }
192
+ if (
193
+ option.maxApiLevel !== undefined &&
194
+ apiLevel > option.maxApiLevel
195
+ ) {
196
+ continue;
197
+ }
198
+ }
199
+
200
+ switch (typeof value) {
201
+ case "boolean":
202
+ if (value) {
203
+ args.push(option.name);
204
+ }
205
+ break;
206
+ case "number":
207
+ args.push(option.name, value.toString());
208
+ break;
209
+ case "string":
210
+ args.push(option.name, value);
211
+ break;
212
+ default:
213
+ throw new Error(
214
+ `Unsupported type for option ${key}: ${typeof value}`,
215
+ );
216
+ }
217
+ }
218
+
281
219
  return args;
282
220
  }
283
221
 
284
- export class PackageManager extends AdbServiceBase {
285
- static ServiceName = "package";
286
- static CommandName = "pm";
222
+ export class PackageManager {
223
+ static readonly ServiceName = "package";
224
+ static readonly CommandName = "pm";
287
225
 
288
- #cmd: CmdNoneProtocolService;
226
+ readonly #adb: Adb;
227
+ readonly #apiLevel: number | undefined;
228
+ readonly #cmd: Cmd.NoneProtocolService;
289
229
 
290
- constructor(adb: Adb) {
291
- super(adb);
292
- this.#cmd = new CmdNoneProtocolService(adb, PackageManager.CommandName);
230
+ constructor(adb: Adb, apiLevel?: number) {
231
+ this.#adb = adb;
232
+ this.#apiLevel = apiLevel;
233
+ this.#cmd = Cmd.createNoneProtocol(adb, PackageManager.CommandName);
293
234
  }
294
235
 
295
236
  /**
@@ -299,76 +240,74 @@ export class PackageManager extends AdbServiceBase {
299
240
  */
300
241
  async install(
301
242
  apks: readonly string[],
302
- options?: Partial<PackageManagerInstallOptions>,
303
- ): Promise<string> {
304
- const args = buildInstallArguments("install", options);
305
- args[0] = PackageManager.CommandName;
243
+ options?: PackageManager.InstallOptions,
244
+ ): Promise<void> {
245
+ const command = buildInstallCommand("install", options, this.#apiLevel);
246
+
247
+ command[0] = PackageManager.CommandName;
248
+
306
249
  // WIP: old version of pm doesn't support multiple apks
307
- args.push(...apks);
250
+ for (const apk of apks) {
251
+ command.push(apk);
252
+ }
308
253
 
309
254
  // Starting from Android 7, `pm` becomes a wrapper to `cmd package`.
310
255
  // The benefit of `cmd package` is it starts faster than the old `pm`,
311
256
  // because it connects to the already running `system` process,
312
257
  // instead of initializing all system components from scratch.
313
258
  //
314
- // But launching `cmd package` directly causes it to not be able to
315
- // read files in `/data/local/tmp` (and many other places) due to SELinux policies,
316
- // so installing files must still use `pm`.
259
+ // But `cmd` executable can't read files in `/data/local/tmp`
260
+ // (and many other places) due to SELinux policies,
261
+ // so installing from files must still use `pm`.
317
262
  // (the starting executable file decides which SELinux policies to apply)
318
- const output = await this.adb.subprocess.noneProtocol
319
- .spawnWaitText(args)
263
+ const output = await this.#adb.subprocess.noneProtocol
264
+ .spawn(command.map(escapeArg))
265
+ .wait()
266
+ .toString()
320
267
  .then((output) => output.trim());
321
268
 
322
269
  if (output !== "Success") {
323
270
  throw new Error(output);
324
271
  }
325
-
326
- return output;
327
272
  }
328
273
 
329
274
  async pushAndInstallStream(
330
275
  stream: ReadableStream<MaybeConsumable<Uint8Array>>,
331
- options?: Partial<PackageManagerInstallOptions>,
332
- ): Promise<string> {
276
+ options?: PackageManager.InstallOptions,
277
+ ): Promise<void> {
333
278
  const fileName = Math.random().toString().substring(2);
334
279
  const filePath = `/data/local/tmp/${fileName}.apk`;
335
280
 
336
- const sync = await this.adb.sync();
337
-
338
- try {
339
- await sync.write({
340
- filename: filePath,
341
- file: stream,
342
- });
343
- } finally {
344
- await sync.dispose();
345
- }
281
+ await this.#adb.sync.write({
282
+ filename: filePath,
283
+ file: stream,
284
+ });
346
285
 
347
286
  try {
348
- return await this.install([filePath], options);
287
+ await this.install([filePath], options);
349
288
  } finally {
350
- await this.adb.rm(filePath);
289
+ await this.#adb.rm(filePath);
351
290
  }
352
291
  }
353
292
 
354
293
  async installStream(
355
294
  size: number,
356
295
  stream: ReadableStream<MaybeConsumable<Uint8Array>>,
357
- options?: Partial<PackageManagerInstallOptions>,
296
+ options?: PackageManager.InstallOptions,
358
297
  ): Promise<void> {
359
- // Android 7 added both `cmd` command and streaming install support,
360
- // It's hard to detect whether `pm` supports streaming install (unless actually trying),
361
- // so check for whether `cmd` is supported,
362
- // and assume `pm` streaming install support status is same as that.
363
- if (!this.#cmd.isSupported) {
298
+ // Technically, support for `cmd` and streaming install are unrelated,
299
+ // but it's impossible to detect streaming install support without actually trying it.
300
+ // As they are both added in Android 7,
301
+ // assume `cmd` support also means streaming install support (and vice versa).
302
+ if (this.#cmd.mode === Cmd.Mode.Fallback) {
364
303
  // Fall back to push file then install
365
304
  await this.pushAndInstallStream(stream, options);
366
305
  return;
367
306
  }
368
307
 
369
- const args = buildInstallArguments("install", options);
370
- args.push("-S", size.toString());
371
- const process = await this.#cmd.spawn(args);
308
+ const command = buildInstallCommand("install", options, this.#apiLevel);
309
+ command.push("-S", size.toString());
310
+ const process = await this.#cmd.spawn(command);
372
311
 
373
312
  const output = process.output
374
313
  .pipeThrough(new TextDecoderStream())
@@ -385,10 +324,10 @@ export class PackageManager extends AdbServiceBase {
385
324
  ]);
386
325
  }
387
326
 
388
- static parsePackageListItem(
389
- line: string,
390
- ): PackageManagerListPackagesResult {
391
- line = line.substring("package:".length);
327
+ static readonly PackageListItemPrefix = "package:";
328
+
329
+ static parsePackageListItem(line: string): PackageManager.PackageInfo {
330
+ line = line.substring(PackageManager.PackageListItemPrefix.length);
392
331
 
393
332
  let packageName: string;
394
333
  let sourceDir: string | undefined;
@@ -439,41 +378,84 @@ export class PackageManager extends AdbServiceBase {
439
378
  }
440
379
 
441
380
  async *listPackages(
442
- options?: Partial<PackageManagerListPackagesOptions>,
443
- ): AsyncGenerator<PackageManagerListPackagesResult, void, void> {
444
- const args = buildArguments(
381
+ options?: Optional<PackageManager.ListPackagesOptions>,
382
+ ): AsyncGenerator<PackageManager.PackageInfo, void, void> {
383
+ const command = buildCommand(
445
384
  ["package", "list", "packages"],
446
385
  options,
447
- PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP,
386
+ ListPackagesOptionsMap,
448
387
  );
388
+
449
389
  if (options?.filter) {
450
- args.push(options.filter);
390
+ command.push(options.filter);
451
391
  }
452
392
 
453
- const process = await this.#cmd.spawn(args);
454
- const reader = process.output
393
+ const process = await this.#cmd.spawn(command);
394
+
395
+ const output = process.output
455
396
  .pipeThrough(new TextDecoderStream())
456
- .pipeThrough(new SplitStringStream("\n"))
457
- .getReader();
458
- while (true) {
459
- const { done, value } = await reader.read();
460
- if (done) {
461
- break;
397
+ .pipeThrough(new SplitStringStream("\n", { trim: true }));
398
+
399
+ for await (const line of output) {
400
+ if (!line.startsWith(PackageManager.PackageListItemPrefix)) {
401
+ continue;
462
402
  }
463
- yield PackageManager.parsePackageListItem(value);
403
+
404
+ yield PackageManager.parsePackageListItem(line);
464
405
  }
465
406
  }
466
407
 
467
- async getPackageSources(packageName: string): Promise<string[]> {
468
- const args = [PackageManager.ServiceName, "-p", packageName];
469
- const process = await this.#cmd.spawn(args);
470
- const result: string[] = [];
471
- for await (const line of process.output
408
+ /**
409
+ * Gets APK file paths for a package.
410
+ *
411
+ * On supported Android versions, all split APKs are included.
412
+ * @param packageName The package name to query
413
+ * @param options The user ID to query
414
+ * @returns An array of APK file paths
415
+ */
416
+ async getPackageSources(
417
+ packageName: string,
418
+ options?: {
419
+ /**
420
+ * The user ID to query
421
+ */
422
+ user?: number | undefined;
423
+ },
424
+ ): Promise<string[]> {
425
+ // `pm path` and `pm -p` are the same,
426
+ // but `pm path` allows an optional `--user` option.
427
+ const command = [PackageManager.ServiceName, "path"];
428
+
429
+ if (options?.user !== undefined) {
430
+ command.push("--user", options.user.toString());
431
+ }
432
+
433
+ command.push(packageName);
434
+
435
+ // Android 7 and 8 support `cmd package` but not `cmd package path` command
436
+ let process: AdbNoneProtocolProcess;
437
+ if (this.#apiLevel !== undefined && this.#apiLevel <= 27) {
438
+ command[0] = PackageManager.CommandName;
439
+ process = await this.#adb.subprocess.noneProtocol.spawn(
440
+ command.map(escapeArg),
441
+ );
442
+ } else {
443
+ process = await this.#cmd.spawn(command);
444
+ }
445
+
446
+ const lines = process.output
472
447
  .pipeThrough(new TextDecoderStream())
473
- .pipeThrough(new SplitStringStream("\n"))) {
474
- if (line.startsWith("package:")) {
475
- result.push(line.substring("package:".length));
448
+ .pipeThrough(new SplitStringStream("\n", { trim: true }));
449
+
450
+ const result: string[] = [];
451
+ for await (const line of lines) {
452
+ if (!line.startsWith(PackageManager.PackageListItemPrefix)) {
453
+ continue;
476
454
  }
455
+
456
+ result.push(
457
+ line.substring(PackageManager.PackageListItemPrefix.length),
458
+ );
477
459
  }
478
460
 
479
461
  return result;
@@ -481,20 +463,26 @@ export class PackageManager extends AdbServiceBase {
481
463
 
482
464
  async uninstall(
483
465
  packageName: string,
484
- options?: Partial<PackageManagerUninstallOptions>,
466
+ options?: Optional<PackageManager.UninstallOptions>,
485
467
  ): Promise<void> {
486
- const args = buildArguments(
468
+ const command = buildCommand(
487
469
  [PackageManager.ServiceName, "uninstall"],
488
470
  options,
489
- PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP,
471
+ UninstallOptionsMap,
490
472
  );
491
- args.push(packageName);
473
+
474
+ command.push(packageName);
475
+
492
476
  if (options?.splitNames) {
493
- args.push(...options.splitNames);
477
+ for (const splitName of options.splitNames) {
478
+ command.push(splitName);
479
+ }
494
480
  }
495
481
 
496
482
  const output = await this.#cmd
497
- .spawnWaitText(args)
483
+ .spawn(command)
484
+ .wait()
485
+ .toString()
498
486
  .then((output) => output.trim());
499
487
  if (output !== "Success") {
500
488
  throw new Error(output);
@@ -502,18 +490,22 @@ export class PackageManager extends AdbServiceBase {
502
490
  }
503
491
 
504
492
  async resolveActivity(
505
- options: PackageManagerResolveActivityOptions,
493
+ options: PackageManager.ResolveActivityOptions,
506
494
  ): Promise<string | undefined> {
507
- let args = buildArguments(
495
+ const command = buildCommand(
508
496
  [PackageManager.ServiceName, "resolve-activity", "--components"],
509
497
  options,
510
- PACKAGE_MANAGER_RESOLVE_ACTIVITY_OPTIONS_MAP,
498
+ ResolveActivityOptionsMap,
511
499
  );
512
500
 
513
- args = args.concat(options.intent.build());
501
+ for (const arg of serializeIntent(options.intent)) {
502
+ command.push(arg);
503
+ }
514
504
 
515
505
  const output = await this.#cmd
516
- .spawnWaitText(args)
506
+ .spawn(command)
507
+ .wait()
508
+ .toString()
517
509
  .then((output) => output.trim());
518
510
 
519
511
  if (output === "No activity found") {
@@ -534,20 +526,28 @@ export class PackageManager extends AdbServiceBase {
534
526
  * @returns ID of the new install session
535
527
  */
536
528
  async sessionCreate(
537
- options?: Partial<PackageManagerInstallOptions>,
529
+ options?: PackageManager.InstallOptions,
538
530
  ): Promise<number> {
539
- const args = buildInstallArguments("install-create", options);
531
+ const command = buildInstallCommand(
532
+ "install-create",
533
+ options,
534
+ this.#apiLevel,
535
+ );
540
536
 
541
537
  const output = await this.#cmd
542
- .spawnWaitText(args)
538
+ .spawn(command)
539
+ .wait()
540
+ .toString()
543
541
  .then((output) => output.trim());
544
542
 
545
- const sessionIdString = output.match(/.*\[(\d+)\].*/);
546
- if (!sessionIdString) {
547
- throw new Error("Failed to create install session");
543
+ // The output format won't change to make it easier to parse
544
+ // https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java;l=1744;drc=e38fa24e5738513d721ec2d9fd2dd00f32e327c1
545
+ const match = output.match(/\[(\d+)\]/);
546
+ if (!match) {
547
+ throw new Error(output);
548
548
  }
549
549
 
550
- return Number.parseInt(sessionIdString[1]!, 10);
550
+ return Number.parseInt(match[1]!, 10);
551
551
  }
552
552
 
553
553
  async checkResult(stream: ReadableStream<Uint8Array>) {
@@ -566,15 +566,18 @@ export class PackageManager extends AdbServiceBase {
566
566
  splitName: string,
567
567
  path: string,
568
568
  ): Promise<void> {
569
- const args: string[] = [
570
- "pm",
569
+ const command: string[] = [
570
+ PackageManager.CommandName,
571
571
  "install-write",
572
572
  sessionId.toString(),
573
573
  splitName,
574
574
  path,
575
575
  ];
576
576
 
577
- const process = await this.adb.subprocess.noneProtocol.spawn(args);
577
+ // Similar to `install`, must use `adb.subprocess` so it can read `path`
578
+ const process = await this.#adb.subprocess.noneProtocol.spawn(
579
+ command.map(escapeArg),
580
+ );
578
581
  await this.checkResult(process.output);
579
582
  }
580
583
 
@@ -584,7 +587,7 @@ export class PackageManager extends AdbServiceBase {
584
587
  size: number,
585
588
  stream: ReadableStream<MaybeConsumable<Uint8Array>>,
586
589
  ): Promise<void> {
587
- const args: string[] = [
590
+ const command: string[] = [
588
591
  PackageManager.ServiceName,
589
592
  "install-write",
590
593
  "-S",
@@ -594,38 +597,104 @@ export class PackageManager extends AdbServiceBase {
594
597
  "-",
595
598
  ];
596
599
 
597
- const process = await this.#cmd.spawn(args);
600
+ const process = await this.#cmd.spawn(command);
598
601
  await Promise.all([
599
602
  stream.pipeTo(process.stdin),
600
603
  this.checkResult(process.output),
601
604
  ]);
602
605
  }
603
606
 
607
+ /**
608
+ * Commit an install session.
609
+ * @param sessionId ID of install session returned by `createSession`
610
+ * @returns A `Promise` that resolves when the session is committed
611
+ */
604
612
  async sessionCommit(sessionId: number): Promise<void> {
605
- const args: string[] = [
613
+ const command: string[] = [
606
614
  PackageManager.ServiceName,
607
615
  "install-commit",
608
616
  sessionId.toString(),
609
617
  ];
610
- const process = await this.#cmd.spawn(args);
618
+
619
+ // Android 7 did support `cmd package install-commit` command,
620
+ // but it wrote the "Success" message to an incorrect output stream,
621
+ // causing `checkResult` to fail with an empty message
622
+ let process: AdbNoneProtocolProcess;
623
+ if (this.#apiLevel !== undefined && this.#apiLevel <= 25) {
624
+ command[0] = PackageManager.CommandName;
625
+ process = await this.#adb.subprocess.noneProtocol.spawn(
626
+ command.map(escapeArg),
627
+ );
628
+ } else {
629
+ process = await this.#cmd.spawn(command);
630
+ }
631
+
611
632
  await this.checkResult(process.output);
612
633
  }
613
634
 
614
635
  async sessionAbandon(sessionId: number): Promise<void> {
615
- const args: string[] = [
636
+ const command: string[] = [
616
637
  PackageManager.ServiceName,
617
638
  "install-abandon",
618
639
  sessionId.toString(),
619
640
  ];
620
- const process = await this.#cmd.spawn(args);
641
+ const process = await this.#cmd.spawn(command);
621
642
  await this.checkResult(process.output);
622
643
  }
623
644
  }
624
645
 
646
+ export namespace PackageManager {
647
+ export type InstallOptions = {
648
+ [K in keyof typeof InstallOptionsDefinitions]?:
649
+ | (typeof InstallOptionsDefinitions)[K]["type"]
650
+ | undefined;
651
+ };
652
+
653
+ export interface ListPackagesOptions {
654
+ listDisabled: boolean;
655
+ listEnabled: boolean;
656
+ showSourceDir: boolean;
657
+ showInstaller: boolean;
658
+ listSystem: boolean;
659
+ showUid: boolean;
660
+ listThirdParty: boolean;
661
+ showVersionCode: boolean;
662
+ listApexOnly: boolean;
663
+ user: SingleUserOrAll;
664
+ uid: number;
665
+ filter: string;
666
+ }
667
+
668
+ export interface PackageInfo {
669
+ packageName: string;
670
+ sourceDir?: string | undefined;
671
+ versionCode?: number | undefined;
672
+ installer?: string | undefined;
673
+ uid?: number | undefined;
674
+ }
675
+
676
+ export interface UninstallOptions {
677
+ keepData: boolean;
678
+ user: SingleUserOrAll;
679
+ versionCode: number;
680
+ /**
681
+ * Only remove the specified splits, not the entire app
682
+ *
683
+ * On Android 10 and lower, only one split name can be specified.
684
+ */
685
+ splitNames: readonly string[];
686
+ }
687
+
688
+ export interface ResolveActivityOptions {
689
+ user?: SingleUserOrAll;
690
+ intent: Intent;
691
+ }
692
+ }
693
+
625
694
  export class PackageManagerInstallSession {
626
695
  static async create(
627
696
  packageManager: PackageManager,
628
- options?: Partial<PackageManagerInstallOptions>,
697
+ options?: PackageManager.InstallOptions,
629
698
  ): Promise<PackageManagerInstallSession> {
630
699
  const id = await packageManager.sessionCreate(options);
631
700
  return new PackageManagerInstallSession(packageManager, id);
@@ -660,6 +729,10 @@ export class PackageManagerInstallSession {
660
729
  );
661
730
  }
662
731
 
732
+ /**
733
+ * Commit this install session.
734
+ * @returns A `Promise` that resolves when the session is committed
735
+ */
663
736
  commit(): Promise<void> {
664
737
  return this.#packageManager.sessionCommit(this.#id);
665
738
  }