@yume-chan/android-bin 0.0.21 → 0.0.23

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 (52) hide show
  1. package/CHANGELOG.json +36 -0
  2. package/CHANGELOG.md +19 -1
  3. package/README.md +40 -14
  4. package/esm/am.d.ts +17 -0
  5. package/esm/am.d.ts.map +1 -0
  6. package/esm/am.js +42 -0
  7. package/esm/am.js.map +1 -0
  8. package/esm/bu.js.map +1 -1
  9. package/esm/bug-report.js.map +1 -1
  10. package/esm/cmd.js +4 -4
  11. package/esm/cmd.js.map +1 -1
  12. package/esm/demo-mode.js.map +1 -1
  13. package/esm/dumpsys.d.ts +22 -0
  14. package/esm/dumpsys.d.ts.map +1 -1
  15. package/esm/dumpsys.js +34 -1
  16. package/esm/dumpsys.js.map +1 -1
  17. package/esm/index.d.ts +3 -0
  18. package/esm/index.d.ts.map +1 -1
  19. package/esm/index.js +3 -0
  20. package/esm/index.js.map +1 -1
  21. package/esm/intent.d.ts +10 -0
  22. package/esm/intent.d.ts.map +1 -0
  23. package/esm/intent.js +51 -0
  24. package/esm/intent.js.map +1 -0
  25. package/esm/logcat.d.ts.map +1 -1
  26. package/esm/logcat.js +5 -5
  27. package/esm/logcat.js.map +1 -1
  28. package/esm/overlay-display.js.map +1 -1
  29. package/esm/pm.d.ts +54 -5
  30. package/esm/pm.d.ts.map +1 -1
  31. package/esm/pm.js +209 -46
  32. package/esm/pm.js.map +1 -1
  33. package/esm/settings.d.ts +2 -1
  34. package/esm/settings.d.ts.map +1 -1
  35. package/esm/settings.js.map +1 -1
  36. package/esm/string-format.d.ts.map +1 -1
  37. package/esm/string-format.js.map +1 -1
  38. package/esm/utils.d.ts +4 -0
  39. package/esm/utils.d.ts.map +1 -0
  40. package/esm/utils.js +23 -0
  41. package/esm/utils.js.map +1 -0
  42. package/package.json +12 -12
  43. package/src/am.ts +69 -0
  44. package/src/cmd.ts +4 -4
  45. package/src/dumpsys.ts +39 -1
  46. package/src/index.ts +3 -0
  47. package/src/intent.ts +63 -0
  48. package/src/logcat.ts +7 -6
  49. package/src/pm.ts +309 -67
  50. package/src/settings.ts +2 -1
  51. package/src/utils.ts +29 -0
  52. package/tsconfig.build.tsbuildinfo +1 -1
package/src/pm.ts CHANGED
@@ -1,17 +1,21 @@
1
1
  // cspell:ignore dont
2
2
  // cspell:ignore instantapp
3
3
  // cspell:ignore apks
4
+ // cspell:ignore versioncode
4
5
 
5
6
  import type { Adb } from "@yume-chan/adb";
6
- import {
7
- AdbCommandBase,
8
- AdbSubprocessNoneProtocol,
9
- escapeArg,
10
- } from "@yume-chan/adb";
7
+ import { AdbCommandBase, escapeArg } from "@yume-chan/adb";
11
8
  import type { Consumable, ReadableStream } from "@yume-chan/stream-extra";
12
- import { DecodeUtf8Stream, WrapReadableStream } from "@yume-chan/stream-extra";
9
+ import {
10
+ ConcatStringStream,
11
+ DecodeUtf8Stream,
12
+ SplitStringStream,
13
+ } from "@yume-chan/stream-extra";
13
14
 
14
15
  import { Cmd } from "./cmd.js";
16
+ import type { IntentBuilder } from "./intent.js";
17
+ import type { SingleUserOrAll } from "./utils.js";
18
+ import { buildArguments } from "./utils.js";
15
19
 
16
20
  export enum PackageManagerInstallLocation {
17
21
  Auto,
@@ -96,7 +100,7 @@ export interface PackageManagerInstallOptions {
96
100
  /**
97
101
  * `--user`
98
102
  */
99
- userId: number;
103
+ user: SingleUserOrAll;
100
104
  /**
101
105
  * `--install-location`
102
106
  */
@@ -163,7 +167,7 @@ export const PACKAGE_MANAGER_INSTALL_OPTIONS_MAP: Record<
163
167
  instantApp: "--instant",
164
168
  full: "--full",
165
169
  preload: "--preload",
166
- userId: "--user",
170
+ user: "--user",
167
171
  installLocation: "--install-location",
168
172
  installReason: "--install-reason",
169
173
  forceUuid: "--force-uuid",
@@ -187,7 +191,7 @@ export interface PackageManagerListPackagesOptions {
187
191
  listThirdParty: boolean;
188
192
  showVersionCode: boolean;
189
193
  listApexOnly: boolean;
190
- user: "all" | "current" | number;
194
+ user: SingleUserOrAll;
191
195
  uid: number;
192
196
  filter: string;
193
197
  }
@@ -218,6 +222,39 @@ export interface PackageManagerListPackagesResult {
218
222
  uid?: number | undefined;
219
223
  }
220
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: string[];
235
+ }
236
+
237
+ const PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP: Record<
238
+ keyof PackageManagerUninstallOptions,
239
+ string
240
+ > = {
241
+ keepData: "-k",
242
+ user: "--user",
243
+ versionCode: "--versionCode",
244
+ splitNames: "",
245
+ };
246
+
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>
254
+ > = {
255
+ user: "--user",
256
+ };
257
+
221
258
  export class PackageManager extends AdbCommandBase {
222
259
  #cmd: Cmd;
223
260
 
@@ -226,48 +263,32 @@ export class PackageManager extends AdbCommandBase {
226
263
  this.#cmd = new Cmd(adb);
227
264
  }
228
265
 
229
- #buildArguments<T>(
230
- commands: string[],
231
- options: Partial<T> | undefined,
232
- map: Record<keyof T, string>,
233
- ): string[] {
234
- const args = ["pm", ...commands];
235
- if (options) {
236
- for (const [key, value] of Object.entries(options)) {
237
- if (value) {
238
- const option = map[key as keyof T];
239
- if (option) {
240
- args.push(option);
241
- switch (typeof value) {
242
- case "number":
243
- args.push(value.toString());
244
- break;
245
- case "string":
246
- args.push(value);
247
- break;
248
- }
249
- }
250
- }
251
- }
252
- }
253
- return args;
254
- }
255
-
256
266
  #buildInstallArguments(
267
+ command: string,
257
268
  options: Partial<PackageManagerInstallOptions> | undefined,
258
269
  ): string[] {
259
- return this.#buildArguments(
260
- ["install"],
270
+ const args = buildArguments(
271
+ ["pm", command],
261
272
  options,
262
273
  PACKAGE_MANAGER_INSTALL_OPTIONS_MAP,
263
274
  );
275
+ if (!options?.skipExisting) {
276
+ // Compatibility with old versions of pm
277
+ args.push("-r");
278
+ }
279
+ return args;
264
280
  }
265
281
 
282
+ /**
283
+ * Install the apk file.
284
+ *
285
+ * @param apks Path to the apk file. It must exist on the device. On Android 10 and lower, only one apk can be specified.
286
+ */
266
287
  async install(
267
288
  apks: string[],
268
289
  options?: Partial<PackageManagerInstallOptions>,
269
290
  ): Promise<string> {
270
- const args = this.#buildInstallArguments(options);
291
+ const args = this.#buildInstallArguments("install", options);
271
292
  // WIP: old version of pm doesn't support multiple apks
272
293
  args.push(...apks);
273
294
  return await this.adb.subprocess.spawnAndWaitLegacy(args);
@@ -276,7 +297,7 @@ export class PackageManager extends AdbCommandBase {
276
297
  async pushAndInstallStream(
277
298
  stream: ReadableStream<Consumable<Uint8Array>>,
278
299
  options?: Partial<PackageManagerInstallOptions>,
279
- ): Promise<ReadableStream<string>> {
300
+ ): Promise<void> {
280
301
  const sync = await this.adb.sync();
281
302
 
282
303
  const fileName = Math.random().toString().substring(2);
@@ -291,43 +312,58 @@ export class PackageManager extends AdbCommandBase {
291
312
  await sync.dispose();
292
313
  }
293
314
 
294
- // Starting from Android 7, `pm` is a only wrapper for `cmd package`,
315
+ // Starting from Android 7, `pm` is only a wrapper for `cmd package`,
295
316
  // and `cmd package` launches faster than `pm`.
296
317
  // But `cmd package` can't read `/data/local/tmp` folder due to SELinux policy,
297
318
  // so installing a file must use `pm`.
298
- const args = this.#buildInstallArguments(options);
319
+ const args = this.#buildInstallArguments("install", options);
299
320
  args.push(filePath);
300
- const process = await AdbSubprocessNoneProtocol.raw(
301
- this.adb,
302
- args.map(escapeArg).join(" "),
303
- );
304
- return new WrapReadableStream({
305
- start: () => process.stdout.pipeThrough(new DecodeUtf8Stream()),
306
- close: async () => {
307
- await this.adb.rm(filePath);
308
- },
309
- });
321
+
322
+ try {
323
+ const output = await this.adb.subprocess
324
+ .spawnAndWaitLegacy(args.map(escapeArg))
325
+ .then((output) => output.trim());
326
+
327
+ if (output !== "Success") {
328
+ throw new Error(output);
329
+ }
330
+ } finally {
331
+ await this.adb.rm(filePath);
332
+ }
310
333
  }
311
334
 
312
335
  async installStream(
313
336
  size: number,
314
337
  stream: ReadableStream<Consumable<Uint8Array>>,
315
338
  options?: Partial<PackageManagerInstallOptions>,
316
- ): Promise<ReadableStream<string>> {
339
+ ): Promise<void> {
317
340
  // Android 7 added both `cmd` command and streaming install support,
318
341
  // we can't detect whether `pm` supports streaming install,
319
342
  // so we detect `cmd` command support instead.
320
343
  if (!this.#cmd.supportsCmd) {
321
- return this.pushAndInstallStream(stream, options);
344
+ await this.pushAndInstallStream(stream, options);
345
+ return;
322
346
  }
323
347
 
324
- const args = this.#buildInstallArguments(options);
348
+ const args = this.#buildInstallArguments("install", options);
325
349
  // Remove `pm` from args, final command will starts with `cmd package install`
326
350
  args.shift();
327
351
  args.push("-S", size.toString());
328
352
  const process = await this.#cmd.spawn(false, "package", ...args);
329
- await stream.pipeTo(process.stdin);
330
- return process.stdout.pipeThrough(new DecodeUtf8Stream());
353
+
354
+ const output = process.stdout
355
+ .pipeThrough(new DecodeUtf8Stream())
356
+ .pipeThrough(new ConcatStringStream())
357
+ .then((output) => output.trim());
358
+
359
+ await Promise.all([
360
+ stream.pipeTo(process.stdin),
361
+ output.then((output) => {
362
+ if (output !== "Success") {
363
+ throw new Error(output);
364
+ }
365
+ }),
366
+ ]);
331
367
  }
332
368
 
333
369
  static parsePackageListItem(
@@ -381,23 +417,229 @@ export class PackageManager extends AdbCommandBase {
381
417
  };
382
418
  }
383
419
 
384
- async listPackages(
420
+ async #cmdOrSubprocess(args: string[]) {
421
+ if (this.#cmd.supportsCmd) {
422
+ args.shift();
423
+ return await this.#cmd.spawn(false, "package", ...args);
424
+ }
425
+
426
+ return this.adb.subprocess.spawn(args);
427
+ }
428
+
429
+ async *listPackages(
385
430
  options?: Partial<PackageManagerListPackagesOptions>,
386
- ): Promise<PackageManagerListPackagesResult[]> {
387
- const args = this.#buildArguments(
388
- ["list", "packages"],
431
+ ): AsyncGenerator<PackageManagerListPackagesResult, void, void> {
432
+ const args = buildArguments(
433
+ ["pm", "list", "packages"],
389
434
  options,
390
435
  PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP,
391
436
  );
392
437
  if (options?.filter) {
393
438
  args.push(options.filter);
394
439
  }
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));
440
+
441
+ const process = await this.#cmdOrSubprocess(args);
442
+ const reader = process.stdout
443
+ .pipeThrough(new DecodeUtf8Stream())
444
+ .pipeThrough(new SplitStringStream("\n"))
445
+ .getReader();
446
+ while (true) {
447
+ const { done, value } = await reader.read();
448
+ if (done) {
449
+ break;
450
+ }
451
+ yield PackageManager.parsePackageListItem(value);
452
+ }
453
+ }
454
+
455
+ async uninstall(
456
+ packageName: string,
457
+ options?: Partial<PackageManagerUninstallOptions>,
458
+ ): Promise<void> {
459
+ const args = buildArguments(
460
+ ["pm", "uninstall"],
461
+ options,
462
+ PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP,
463
+ );
464
+ args.push(packageName);
465
+ if (options?.splitNames) {
466
+ args.push(...options.splitNames);
467
+ }
468
+
469
+ const process = await this.#cmdOrSubprocess(args);
470
+ const output = await process.stdout
471
+ .pipeThrough(new DecodeUtf8Stream())
472
+ .pipeThrough(new ConcatStringStream())
473
+ .then((output) => output.trim());
474
+ if (output !== "Success") {
475
+ throw new Error(output);
476
+ }
400
477
  }
401
478
 
402
- // TODO: install: support split apk formats (`adb install-multiple`)
479
+ async resolveActivity(
480
+ options: PackageManagerResolveActivityOptions,
481
+ ): Promise<string | undefined> {
482
+ let args = buildArguments(
483
+ ["pm", "resolve-activity", "--components"],
484
+ options,
485
+ PACKAGE_MANAGER_RESOLVE_ACTIVITY_OPTIONS_MAP,
486
+ );
487
+
488
+ args = args.concat(options.intent.build());
489
+
490
+ const process = await this.#cmdOrSubprocess(args);
491
+ const output = await process.stdout
492
+ .pipeThrough(new DecodeUtf8Stream())
493
+ .pipeThrough(new ConcatStringStream())
494
+ .then((output) => output.trim());
495
+
496
+ if (output === "No activity found") {
497
+ return undefined;
498
+ }
499
+
500
+ return output;
501
+ }
502
+
503
+ /**
504
+ * Creates a new install session.
505
+ *
506
+ * Install sessions are used to install apps with multiple splits, but it can also be used to install a single apk.
507
+ *
508
+ * Install sessions was added in Android 5.0 (API level 21).
509
+ *
510
+ * @param options Options for the install session
511
+ * @returns ID of the new install session
512
+ */
513
+ async sessionCreate(options?: Partial<PackageManagerInstallOptions>) {
514
+ const args = this.#buildInstallArguments("install-create", options);
515
+
516
+ const process = await this.#cmdOrSubprocess(args);
517
+ const output = await process.stdout
518
+ .pipeThrough(new DecodeUtf8Stream())
519
+ .pipeThrough(new ConcatStringStream())
520
+ .then((output) => output.trim());
521
+
522
+ const sessionIdString = output.match(/.*\[(\d+)\].*/);
523
+ if (!sessionIdString) {
524
+ throw new Error("Failed to create install session");
525
+ }
526
+
527
+ return Number.parseInt(sessionIdString[1]!, 10);
528
+ }
529
+
530
+ async sessionAddSplit(sessionId: number, splitName: string, path: string) {
531
+ const args: string[] = [
532
+ "pm",
533
+ "install-write",
534
+ sessionId.toString(),
535
+ splitName,
536
+ path,
537
+ ];
538
+
539
+ const output = await this.adb.subprocess
540
+ .spawnAndWaitLegacy(args)
541
+ .then((output) => output.trim());
542
+ if (!output.startsWith("Success")) {
543
+ throw new Error(output);
544
+ }
545
+ }
546
+
547
+ async sessionAddSplitStream(
548
+ sessionId: number,
549
+ splitName: string,
550
+ size: number,
551
+ stream: ReadableStream<Consumable<Uint8Array>>,
552
+ ) {
553
+ const args: string[] = [
554
+ "pm",
555
+ "install-write",
556
+ "-S",
557
+ size.toString(),
558
+ sessionId.toString(),
559
+ splitName,
560
+ "-",
561
+ ];
562
+
563
+ const process = await this.#cmdOrSubprocess(args);
564
+ const output = process.stdout
565
+ .pipeThrough(new DecodeUtf8Stream())
566
+ .pipeThrough(new ConcatStringStream())
567
+ .then((output) => output.trim());
568
+
569
+ await Promise.all([
570
+ stream.pipeTo(process.stdin),
571
+ output.then((output) => {
572
+ if (!output.startsWith("Success")) {
573
+ throw new Error(output);
574
+ }
575
+ }),
576
+ ]);
577
+ }
578
+
579
+ async sessionCommit(sessionId: number) {
580
+ const args: string[] = ["pm", "install-commit", sessionId.toString()];
581
+ const output = await this.adb.subprocess
582
+ .spawnAndWaitLegacy(args)
583
+ .then((output) => output.trim());
584
+ if (output !== "Success") {
585
+ throw new Error(output);
586
+ }
587
+ }
588
+
589
+ async sessionAbandon(sessionId: number) {
590
+ const args: string[] = ["pm", "install-abandon", sessionId.toString()];
591
+ const output = await this.adb.subprocess
592
+ .spawnAndWaitLegacy(args)
593
+ .then((output) => output.trim());
594
+ if (output !== "Success") {
595
+ throw new Error(output);
596
+ }
597
+ }
598
+ }
599
+
600
+ export class PackageManagerInstallSession {
601
+ static async create(
602
+ packageManager: PackageManager,
603
+ options?: Partial<PackageManagerInstallOptions>,
604
+ ) {
605
+ const id = await packageManager.sessionCreate(options);
606
+ return new PackageManagerInstallSession(packageManager, id);
607
+ }
608
+
609
+ #packageManager: PackageManager;
610
+
611
+ #id: number;
612
+ get id() {
613
+ return this.#id;
614
+ }
615
+
616
+ constructor(packageManager: PackageManager, id: number) {
617
+ this.#packageManager = packageManager;
618
+ this.#id = id;
619
+ }
620
+
621
+ addSplit(splitName: string, path: string) {
622
+ return this.#packageManager.sessionAddSplit(this.#id, splitName, path);
623
+ }
624
+
625
+ addSplitStream(
626
+ splitName: string,
627
+ size: number,
628
+ stream: ReadableStream<Consumable<Uint8Array>>,
629
+ ) {
630
+ return this.#packageManager.sessionAddSplitStream(
631
+ this.#id,
632
+ splitName,
633
+ size,
634
+ stream,
635
+ );
636
+ }
637
+
638
+ commit() {
639
+ return this.#packageManager.sessionCommit(this.#id);
640
+ }
641
+
642
+ abandon() {
643
+ return this.#packageManager.sessionAbandon(this.#id);
644
+ }
403
645
  }
package/src/settings.ts CHANGED
@@ -2,6 +2,7 @@ import type { Adb, AdbSubprocessWaitResult } from "@yume-chan/adb";
2
2
  import { AdbCommandBase } from "@yume-chan/adb";
3
3
 
4
4
  import { Cmd } from "./cmd.js";
5
+ import type { SingleUser } from "./utils.js";
5
6
 
6
7
  export type SettingsNamespace = "system" | "secure" | "global";
7
8
 
@@ -12,7 +13,7 @@ export enum SettingsResetMode {
12
13
  }
13
14
 
14
15
  export interface SettingsOptions {
15
- user?: number | "current";
16
+ user?: SingleUser;
16
17
  }
17
18
 
18
19
  export interface SettingsPutOptions extends SettingsOptions {
package/src/utils.ts ADDED
@@ -0,0 +1,29 @@
1
+ export function buildArguments<T>(
2
+ commands: string[],
3
+ options: Partial<T> | undefined,
4
+ map: Partial<Record<keyof T, string>>,
5
+ ): string[] {
6
+ const args = commands;
7
+ if (options) {
8
+ for (const [key, value] of Object.entries(options)) {
9
+ if (value) {
10
+ const option = map[key as keyof T];
11
+ if (option) {
12
+ args.push(option);
13
+ switch (typeof value) {
14
+ case "number":
15
+ args.push(value.toString());
16
+ break;
17
+ case "string":
18
+ args.push(value);
19
+ break;
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+ return args;
26
+ }
27
+
28
+ export type SingleUser = number | "current";
29
+ export type SingleUserOrAll = SingleUser | "all";