@yume-chan/android-bin 0.0.18 → 0.0.19

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 (46) hide show
  1. package/CHANGELOG.json +15 -0
  2. package/CHANGELOG.md +9 -1
  3. package/esm/bu.d.ts +14 -14
  4. package/esm/bu.js +13 -13
  5. package/esm/bug-report.d.ts +37 -37
  6. package/esm/bug-report.d.ts.map +1 -1
  7. package/esm/bug-report.js +136 -135
  8. package/esm/bug-report.js.map +1 -1
  9. package/esm/cmd.d.ts +15 -0
  10. package/esm/cmd.d.ts.map +1 -0
  11. package/esm/cmd.js +54 -0
  12. package/esm/cmd.js.map +1 -0
  13. package/esm/demo-mode.d.ts +43 -42
  14. package/esm/demo-mode.d.ts.map +1 -1
  15. package/esm/demo-mode.js +179 -178
  16. package/esm/demo-mode.js.map +1 -1
  17. package/esm/index.d.ts +6 -4
  18. package/esm/index.d.ts.map +1 -1
  19. package/esm/index.js +7 -5
  20. package/esm/index.js.map +1 -1
  21. package/esm/logcat.d.ts +91 -89
  22. package/esm/logcat.d.ts.map +1 -1
  23. package/esm/logcat.js +288 -200
  24. package/esm/logcat.js.map +1 -1
  25. package/esm/overlay-display.d.ts +21 -0
  26. package/esm/overlay-display.d.ts.map +1 -0
  27. package/esm/overlay-display.js +78 -0
  28. package/esm/overlay-display.js.map +1 -0
  29. package/esm/pm.d.ts +139 -0
  30. package/esm/pm.d.ts.map +1 -0
  31. package/esm/pm.js +125 -0
  32. package/esm/pm.js.map +1 -0
  33. package/esm/settings.d.ts +12 -12
  34. package/esm/settings.d.ts.map +1 -1
  35. package/esm/settings.js +29 -24
  36. package/esm/settings.js.map +1 -1
  37. package/package.json +10 -9
  38. package/src/bug-report.ts +1 -1
  39. package/src/cmd.ts +67 -0
  40. package/src/demo-mode.ts +4 -3
  41. package/src/index.ts +6 -4
  42. package/src/logcat.ts +231 -19
  43. package/src/overlay-display.ts +115 -0
  44. package/src/pm.ts +281 -0
  45. package/src/settings.ts +45 -16
  46. package/tsconfig.build.tsbuildinfo +1 -1
package/src/logcat.ts CHANGED
@@ -1,18 +1,17 @@
1
1
  // cspell: ignore logcat
2
+ // cspell: ignore usec
2
3
 
3
4
  import { AdbCommandBase, AdbSubprocessNoneProtocol } from "@yume-chan/adb";
5
+ import type { ReadableStream } from "@yume-chan/stream-extra";
4
6
  import {
5
7
  BufferedTransformStream,
6
8
  DecodeUtf8Stream,
7
9
  SplitStringStream,
8
10
  WrapReadableStream,
9
11
  WritableStream,
10
- type ReadableStream,
11
12
  } from "@yume-chan/stream-extra";
12
- import Struct, {
13
- decodeUtf8,
14
- type StructAsyncDeserializeStream,
15
- } from "@yume-chan/struct";
13
+ import type { StructAsyncDeserializeStream } from "@yume-chan/struct";
14
+ import Struct, { decodeUtf8 } from "@yume-chan/struct";
16
15
 
17
16
  // `adb logcat` is an alias to `adb shell logcat`
18
17
  // so instead of adding to core library, it's implemented here
@@ -69,10 +68,11 @@ export enum LogcatFormat {
69
68
  }
70
69
 
71
70
  export interface LogcatFormatModifiers {
72
- usec?: boolean;
71
+ microseconds?: boolean;
72
+ nanoseconds?: boolean;
73
73
  printable?: boolean;
74
74
  year?: boolean;
75
- zone?: boolean;
75
+ timezone?: boolean;
76
76
  epoch?: boolean;
77
77
  monotonic?: boolean;
78
78
  uid?: boolean;
@@ -92,44 +92,254 @@ export const LoggerEntry = new Struct({ littleEndian: true })
92
92
  .uint16("headerSize")
93
93
  .int32("pid")
94
94
  .uint32("tid")
95
- .uint32("second")
95
+ .uint32("seconds")
96
96
  .uint32("nanoseconds")
97
97
  .uint32("logId")
98
98
  .uint32("uid")
99
99
  .extra({
100
100
  get timestamp() {
101
101
  return (
102
- BigInt(this.second) * NANOSECONDS_PER_SECOND +
102
+ BigInt(this.seconds) * NANOSECONDS_PER_SECOND +
103
103
  BigInt(this.nanoseconds)
104
104
  );
105
105
  },
106
106
  });
107
107
 
108
- export type LoggerEntry = typeof LoggerEntry["TDeserializeResult"];
108
+ export type LoggerEntry = (typeof LoggerEntry)["TDeserializeResult"];
109
109
 
110
110
  // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;drc=bbe77d66e7bee8bd1f0bc7e5492b5376b0207ef6;bpv=0
111
111
  export interface AndroidLogEntry extends LoggerEntry {
112
112
  priority: AndroidLogPriority;
113
113
  tag: string;
114
114
  message: string;
115
+
116
+ toString(format?: LogcatFormat, modifiers?: LogcatFormatModifiers): string;
115
117
  }
116
118
 
117
- // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;l=1415;drc=8dbf3b2bb6b6d1652d9797e477b9abd03278bb79
118
- export function formatAndroidLogEntry(
119
- entry: AndroidLogEntry,
120
- format: LogcatFormat = LogcatFormat.Brief,
121
- modifier?: LogcatFormatModifiers
119
+ function formatSeconds(seconds: number, modifiers: LogcatFormatModifiers) {
120
+ if (modifiers.monotonic) {
121
+ return seconds.toString().padStart(6);
122
+ }
123
+
124
+ if (modifiers.epoch) {
125
+ return seconds.toString().padStart(19);
126
+ }
127
+
128
+ const date = new Date(seconds * 1000);
129
+
130
+ if (modifiers.year) {
131
+ // prettier-ignore
132
+ return `${
133
+ date.getFullYear().toString().padStart(4, "0")
134
+ }-${
135
+ (date.getMonth() + 1).toString().padStart(2, "0")
136
+ }-${
137
+ date.getDate().toString().padStart(2, "0")
138
+ } ${
139
+ date.getHours().toString().padStart(2, "0")
140
+ }:${
141
+ date.getMinutes().toString().padStart(2, "0")
142
+ }:${
143
+ date.getSeconds().toString().padStart(2, "0")
144
+ }`;
145
+ }
146
+
147
+ // prettier-ignore
148
+ return `${
149
+ (date.getMonth() + 1).toString().padStart(2, "0")
150
+ }-${
151
+ date.getDate().toString().padStart(2, "0")
152
+ } ${
153
+ date.getHours().toString().padStart(2, "0")
154
+ }:${
155
+ date.getMinutes().toString().padStart(2, "0")
156
+ }:${
157
+ date.getSeconds().toString().padStart(2, "0")
158
+ }`;
159
+ }
160
+
161
+ function formatNanoseconds(
162
+ nanoseconds: number,
163
+ modifiers: LogcatFormatModifiers
164
+ ) {
165
+ if (modifiers.nanoseconds) {
166
+ return nanoseconds.toString().padStart(9, "0");
167
+ }
168
+
169
+ if (modifiers.microseconds) {
170
+ return ((nanoseconds / 1000) | 0).toString().padStart(6, "0");
171
+ }
172
+
173
+ return ((nanoseconds / 1000000) | 0).toString().padStart(3, "0");
174
+ }
175
+
176
+ function formatTimezone(seconds: number, modifiers: LogcatFormatModifiers) {
177
+ if (!modifiers.timezone || modifiers.monotonic || modifiers.epoch) {
178
+ return "";
179
+ }
180
+
181
+ const date = new Date(seconds * 1000);
182
+ const offset = date.getTimezoneOffset();
183
+ const sign = offset <= 0 ? "+" : "-";
184
+ const absolute = Math.abs(offset);
185
+ const hours = (absolute / 60) | 0;
186
+ const minutes = absolute % 60;
187
+
188
+ // prettier-ignore
189
+ return ` ${
190
+ sign
191
+ }${
192
+ hours.toString().padStart(2, "0")
193
+ }:${
194
+ minutes.toString().padStart(2, "0")
195
+ }`;
196
+ }
197
+
198
+ function formatTime(
199
+ seconds: number,
200
+ nanoseconds: number,
201
+ modifiers: LogcatFormatModifiers
202
+ ) {
203
+ const secondsString = formatSeconds(seconds, modifiers);
204
+ const nanosecondsString = formatNanoseconds(nanoseconds, modifiers);
205
+ const zoneString = formatTimezone(seconds, modifiers);
206
+ return `${secondsString}.${nanosecondsString}${zoneString}`;
207
+ }
208
+
209
+ function formatUid(
210
+ uid: number,
211
+ modifiers: LogcatFormatModifiers,
212
+ suffix: string
122
213
  ) {
123
- const uid = modifier?.uid ? `${entry.uid.toString().padStart(5)}:` : "";
214
+ return modifiers.uid ? `${uid.toString().padStart(5)}${suffix}` : "";
215
+ }
124
216
 
217
+ function getFormatPrefix(
218
+ entry: AndroidLogEntry,
219
+ format: LogcatFormat,
220
+ modifiers: LogcatFormatModifiers
221
+ ) {
222
+ // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;l=1415;drc=8dbf3b2bb6b6d1652d9797e477b9abd03278bb79
125
223
  switch (format) {
126
224
  // TODO: implement other formats
225
+ case LogcatFormat.Tag:
226
+ // prettier-ignore
227
+ return `${
228
+ AndroidLogPriorityToCharacter[entry.priority]
229
+ }/${
230
+ entry.tag.padEnd(8)
231
+ }: `;
232
+ case LogcatFormat.Process:
233
+ // prettier-ignore
234
+ return `${
235
+ AndroidLogPriorityToCharacter[entry.priority]
236
+ }(${
237
+ formatUid(entry.uid, modifiers, ":")
238
+ }${
239
+ entry.pid.toString().padStart(5)
240
+ }) `;
241
+ case LogcatFormat.Thread:
242
+ // prettier-ignore
243
+ return `${
244
+ AndroidLogPriorityToCharacter[entry.priority]
245
+ }(${
246
+ formatUid(entry.uid, modifiers, ":")
247
+ }${
248
+ entry.pid.toString().padStart(5)
249
+ }:${
250
+ entry.tid.toString().padStart(5)
251
+ }) `;
252
+ case LogcatFormat.Raw:
253
+ return "";
254
+ case LogcatFormat.Time:
255
+ // prettier-ignore
256
+ return `${
257
+ formatTime(entry.seconds, entry.nanoseconds, modifiers)
258
+ } ${
259
+ AndroidLogPriorityToCharacter[entry.priority]
260
+ }/${
261
+ entry.tag.padEnd(8)
262
+ }(${
263
+ formatUid(entry.uid, modifiers, ":")
264
+ }${
265
+ entry.pid.toString().padStart(5)
266
+ }): `;
267
+ case LogcatFormat.ThreadTime:
268
+ // prettier-ignore
269
+ return `${
270
+ formatTime(entry.seconds, entry.nanoseconds, modifiers)
271
+ } ${
272
+ formatUid(entry.uid, modifiers, " ")
273
+ }${
274
+ entry.pid.toString().padStart(5)
275
+ } ${
276
+ entry.tid.toString().padStart(5)
277
+ } ${
278
+ AndroidLogPriorityToCharacter[entry.priority]
279
+ } ${
280
+ entry.tag.toString().padEnd(8)
281
+ }: `;
282
+ case LogcatFormat.Brief:
127
283
  default:
284
+ // prettier-ignore
128
285
  return `${
129
286
  AndroidLogPriorityToCharacter[entry.priority]
130
- }/${entry.tag.padEnd(8)}(${uid}${entry.pid
131
- .toString()
132
- .padStart(5)}): ${entry.message}`;
287
+ }/${
288
+ entry.tag.padEnd(8)
289
+ }(${
290
+ formatUid(entry.uid, modifiers, ":")
291
+ }${
292
+ entry.pid.toString().padStart(5)
293
+ }): `;
294
+ }
295
+ }
296
+
297
+ function getFormatSuffix(entry: AndroidLogEntry, format: LogcatFormat) {
298
+ switch (format) {
299
+ case LogcatFormat.Process:
300
+ return ` (${entry.tag})`;
301
+ default:
302
+ return "";
303
+ }
304
+ }
305
+
306
+ function formatEntryWrapLine(
307
+ entry: AndroidLogEntry,
308
+ format: LogcatFormat,
309
+ modifiers: LogcatFormatModifiers
310
+ ) {
311
+ const prefix = getFormatPrefix(entry, format, modifiers);
312
+ const suffix = getFormatSuffix(entry, format);
313
+ return (
314
+ prefix + entry.message.replaceAll("\n", suffix + "\n" + prefix) + suffix
315
+ );
316
+ }
317
+
318
+ function AndroidLogEntryToString(
319
+ this: AndroidLogEntry,
320
+ format: LogcatFormat = LogcatFormat.ThreadTime,
321
+ modifiers: LogcatFormatModifiers = {}
322
+ ) {
323
+ switch (format) {
324
+ case LogcatFormat.Long:
325
+ // prettier-ignore
326
+ return `[ ${
327
+ formatTime(this.seconds, this.nanoseconds, modifiers)
328
+ } ${
329
+ formatUid(this.uid, modifiers, ":")
330
+ }${
331
+ this.pid.toString().padStart(5)
332
+ }:${
333
+ this.tid.toString().padStart(5)
334
+ } ${
335
+ AndroidLogPriorityToCharacter[this.priority]
336
+ }/${
337
+ this.tag.padEnd(8)
338
+ } ]\n${
339
+ this.message
340
+ }\n`;
341
+ default:
342
+ return formatEntryWrapLine(this, format, modifiers);
133
343
  }
134
344
  }
135
345
 
@@ -171,6 +381,7 @@ export async function deserializeAndroidLogEntry(
171
381
  tagEnd < payload.length - 1
172
382
  ? decodeUtf8(payload.subarray(tagEnd + 1))
173
383
  : "";
384
+ entry.toString = AndroidLogEntryToString;
174
385
  return entry;
175
386
  }
176
387
 
@@ -245,6 +456,7 @@ export class Logcat extends AdbCommandBase {
245
456
  maxEntrySize: parseInt(match[8]!, 10),
246
457
  maxPayloadSize: parseInt(match[9]!, 10),
247
458
  });
459
+ return;
248
460
  }
249
461
 
250
462
  match = chunk.match(Logcat.LOG_SIZE_REGEX_10);
@@ -0,0 +1,115 @@
1
+ import type { Adb } from "@yume-chan/adb";
2
+ import { AdbCommandBase } from "@yume-chan/adb";
3
+
4
+ import { Settings } from "./settings.js";
5
+
6
+ export interface OverlayDisplayDeviceMode {
7
+ width: number;
8
+ height: number;
9
+ density: number;
10
+ }
11
+
12
+ export interface OverlayDisplayDevice {
13
+ modes: OverlayDisplayDeviceMode[];
14
+ secure: boolean;
15
+ ownContentOnly: boolean;
16
+ showSystemDecorations: boolean;
17
+ }
18
+
19
+ export class OverlayDisplay extends AdbCommandBase {
20
+ private settings: Settings;
21
+
22
+ public static readonly OVERLAY_DISPLAY_DEVICES_KEY =
23
+ "overlay_display_devices";
24
+
25
+ constructor(adb: Adb) {
26
+ super(adb);
27
+ this.settings = new Settings(adb);
28
+ }
29
+
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;
83
+ }
84
+
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(
110
+ "global",
111
+ OverlayDisplay.OVERLAY_DISPLAY_DEVICES_KEY,
112
+ settingString
113
+ );
114
+ }
115
+ }
package/src/pm.ts ADDED
@@ -0,0 +1,281 @@
1
+ // cspell:ignore dont
2
+ // cspell:ignore instantapp
3
+ // cspell:ignore apks
4
+
5
+ import type { Adb } from "@yume-chan/adb";
6
+ import {
7
+ AdbCommandBase,
8
+ AdbSubprocessNoneProtocol,
9
+ escapeArg,
10
+ } from "@yume-chan/adb";
11
+ import type { Consumable, ReadableStream } from "@yume-chan/stream-extra";
12
+ import { DecodeUtf8Stream, WrapReadableStream } from "@yume-chan/stream-extra";
13
+
14
+ import { Cmd } from "./cmd.js";
15
+
16
+ export enum PackageManagerInstallLocation {
17
+ Auto,
18
+ InternalOnly,
19
+ PreferExternal,
20
+ }
21
+
22
+ export enum PackageManagerInstallReason {
23
+ Unknown,
24
+ AdminPolicy,
25
+ DeviceRestore,
26
+ DeviceSetup,
27
+ UserRequest,
28
+ }
29
+
30
+ // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java;l=3046;drc=6d14d35d0241f6fee145f8e54ffd77252e8d29fd
31
+ export interface PackageManagerInstallOptions {
32
+ /**
33
+ * `-R`
34
+ */
35
+ skipExisting: boolean;
36
+ /**
37
+ * `-i`
38
+ */
39
+ installerPackageName: string;
40
+ /**
41
+ * `-t`
42
+ */
43
+ allowTest: boolean;
44
+ /**
45
+ * `-f`
46
+ */
47
+ internalStorage: boolean;
48
+ /**
49
+ * `-d`
50
+ */
51
+ requestDowngrade: boolean;
52
+ /**
53
+ * `-g`
54
+ */
55
+ grantRuntimePermissions: boolean;
56
+ /**
57
+ * `--restrict-permissions`
58
+ */
59
+ restrictPermissions: boolean;
60
+ /**
61
+ * `--dont-kill`
62
+ */
63
+ doNotKill: boolean;
64
+ /**
65
+ * `--originating-uri`
66
+ */
67
+ originatingUri: string;
68
+ /**
69
+ * `--referrer`
70
+ */
71
+ refererUri: string;
72
+ /**
73
+ * `-p`
74
+ */
75
+ inheritFrom: string;
76
+ /**
77
+ * `--pkg`
78
+ */
79
+ packageName: string;
80
+ /**
81
+ * `--abi`
82
+ */
83
+ abi: string;
84
+ /**
85
+ * `--ephemeral`/`--instant`/`--instantapp`
86
+ */
87
+ instantApp: boolean;
88
+ /**
89
+ * `--full`
90
+ */
91
+ full: boolean;
92
+ /**
93
+ * `--preload`
94
+ */
95
+ preload: boolean;
96
+ /**
97
+ * `--user`
98
+ */
99
+ userId: number;
100
+ /**
101
+ * `--install-location`
102
+ */
103
+ installLocation: PackageManagerInstallLocation;
104
+ /**
105
+ * `--install-reason`
106
+ */
107
+ installReason: PackageManagerInstallReason;
108
+ /**
109
+ * `--force-uuid`
110
+ */
111
+ forceUuid: string;
112
+ /**
113
+ * `--apex`
114
+ */
115
+ apex: boolean;
116
+ /**
117
+ * `--force-non-staged`
118
+ */
119
+ forceNonStaged: boolean;
120
+ /**
121
+ * `--staged`
122
+ */
123
+ staged: boolean;
124
+ /**
125
+ * `--force-queryable`
126
+ */
127
+ forceQueryable: boolean;
128
+ /**
129
+ * `--enable-rollback`
130
+ */
131
+ enableRollback: boolean;
132
+ /**
133
+ * `--staged-ready-timeout`
134
+ */
135
+ stagedReadyTimeout: number;
136
+ /**
137
+ * `--skip-verification`
138
+ */
139
+ skipVerification: boolean;
140
+ /**
141
+ * `--bypass-low-target-sdk-block`
142
+ */
143
+ bypassLowTargetSdkBlock: boolean;
144
+ }
145
+
146
+ export const PACKAGE_MANAGER_INSTALL_OPTIONS_MAP: Record<
147
+ keyof PackageManagerInstallOptions,
148
+ string
149
+ > = {
150
+ skipExisting: "-R",
151
+ installerPackageName: "-i",
152
+ allowTest: "-t",
153
+ internalStorage: "-f",
154
+ requestDowngrade: "-d",
155
+ grantRuntimePermissions: "-g",
156
+ restrictPermissions: "--restrict-permissions",
157
+ doNotKill: "--dont-kill",
158
+ originatingUri: "--originating-uri",
159
+ refererUri: "--referrer",
160
+ inheritFrom: "-p",
161
+ packageName: "--pkg",
162
+ abi: "--abi",
163
+ instantApp: "--instant",
164
+ full: "--full",
165
+ preload: "--preload",
166
+ userId: "--user",
167
+ installLocation: "--install-location",
168
+ installReason: "--install-reason",
169
+ forceUuid: "--force-uuid",
170
+ apex: "--apex",
171
+ forceNonStaged: "--force-non-staged",
172
+ staged: "--staged",
173
+ forceQueryable: "--force-queryable",
174
+ enableRollback: "--enable-rollback",
175
+ stagedReadyTimeout: "--staged-ready-timeout",
176
+ skipVerification: "--skip-verification",
177
+ bypassLowTargetSdkBlock: "--bypass-low-target-sdk-block",
178
+ };
179
+
180
+ export class PackageManager extends AdbCommandBase {
181
+ private _cmd: Cmd;
182
+
183
+ public constructor(adb: Adb) {
184
+ super(adb);
185
+ this._cmd = new Cmd(adb);
186
+ }
187
+
188
+ private buildInstallArgs(
189
+ options?: Partial<PackageManagerInstallOptions>
190
+ ): string[] {
191
+ const args = ["pm", "install"];
192
+ if (options) {
193
+ for (const [key, value] of Object.entries(options)) {
194
+ if (value) {
195
+ const option =
196
+ PACKAGE_MANAGER_INSTALL_OPTIONS_MAP[
197
+ key as keyof PackageManagerInstallOptions
198
+ ];
199
+ if (option) {
200
+ args.push(option);
201
+ switch (typeof value) {
202
+ case "number":
203
+ args.push(value.toString());
204
+ break;
205
+ case "string":
206
+ args.push(value);
207
+ break;
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
213
+ return args;
214
+ }
215
+
216
+ public async install(
217
+ apks: string[],
218
+ options?: Partial<PackageManagerInstallOptions>
219
+ ): Promise<string> {
220
+ const args = this.buildInstallArgs(options);
221
+ // WIP: old version of pm doesn't support multiple apks
222
+ args.push(...apks);
223
+ return await this.adb.subprocess.spawnAndWaitLegacy(args);
224
+ }
225
+
226
+ public async pushAndInstallStream(
227
+ stream: ReadableStream<Consumable<Uint8Array>>,
228
+ options?: Partial<PackageManagerInstallOptions>
229
+ ): Promise<ReadableStream<string>> {
230
+ const sync = await this.adb.sync();
231
+
232
+ const fileName = Math.random().toString().substring(2);
233
+ const filePath = `/data/local/tmp/${fileName}.apk`;
234
+
235
+ try {
236
+ await sync.write({
237
+ filename: filePath,
238
+ file: stream,
239
+ });
240
+ } finally {
241
+ await sync.dispose();
242
+ }
243
+
244
+ const args = this.buildInstallArgs(options);
245
+ args.push(filePath);
246
+ const process = await AdbSubprocessNoneProtocol.raw(
247
+ this.adb,
248
+ args.map(escapeArg).join(" ")
249
+ );
250
+ return new WrapReadableStream({
251
+ start: () => process.stdout.pipeThrough(new DecodeUtf8Stream()),
252
+ close: async () => {
253
+ await this.adb.rm(filePath);
254
+ },
255
+ });
256
+ }
257
+
258
+ public async installStream(
259
+ size: number,
260
+ stream: ReadableStream<Consumable<Uint8Array>>,
261
+ options?: Partial<PackageManagerInstallOptions>
262
+ ): Promise<ReadableStream<string>> {
263
+ if (!this._cmd.supportsCmd) {
264
+ return this.pushAndInstallStream(stream, options);
265
+ }
266
+
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`
273
+ args.shift();
274
+ args.push("-S", size.toString());
275
+ const process = await this._cmd.spawn(false, "package", ...args);
276
+ await stream.pipeTo(process.stdin);
277
+ return process.stdout.pipeThrough(new DecodeUtf8Stream());
278
+ }
279
+
280
+ // TODO: install: support split apk formats (`adb install-multiple`)
281
+ }