@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.
- package/CHANGELOG.json +15 -0
- package/CHANGELOG.md +9 -1
- package/esm/bu.d.ts +14 -14
- package/esm/bu.js +13 -13
- package/esm/bug-report.d.ts +37 -37
- package/esm/bug-report.d.ts.map +1 -1
- package/esm/bug-report.js +136 -135
- package/esm/bug-report.js.map +1 -1
- package/esm/cmd.d.ts +15 -0
- package/esm/cmd.d.ts.map +1 -0
- package/esm/cmd.js +54 -0
- package/esm/cmd.js.map +1 -0
- package/esm/demo-mode.d.ts +43 -42
- package/esm/demo-mode.d.ts.map +1 -1
- package/esm/demo-mode.js +179 -178
- package/esm/demo-mode.js.map +1 -1
- package/esm/index.d.ts +6 -4
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +7 -5
- package/esm/index.js.map +1 -1
- package/esm/logcat.d.ts +91 -89
- package/esm/logcat.d.ts.map +1 -1
- package/esm/logcat.js +288 -200
- package/esm/logcat.js.map +1 -1
- package/esm/overlay-display.d.ts +21 -0
- package/esm/overlay-display.d.ts.map +1 -0
- package/esm/overlay-display.js +78 -0
- package/esm/overlay-display.js.map +1 -0
- package/esm/pm.d.ts +139 -0
- package/esm/pm.d.ts.map +1 -0
- package/esm/pm.js +125 -0
- package/esm/pm.js.map +1 -0
- package/esm/settings.d.ts +12 -12
- package/esm/settings.d.ts.map +1 -1
- package/esm/settings.js +29 -24
- package/esm/settings.js.map +1 -1
- package/package.json +10 -9
- package/src/bug-report.ts +1 -1
- package/src/cmd.ts +67 -0
- package/src/demo-mode.ts +4 -3
- package/src/index.ts +6 -4
- package/src/logcat.ts +231 -19
- package/src/overlay-display.ts +115 -0
- package/src/pm.ts +281 -0
- package/src/settings.ts +45 -16
- 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
|
|
13
|
-
|
|
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
|
-
|
|
71
|
+
microseconds?: boolean;
|
|
72
|
+
nanoseconds?: boolean;
|
|
73
73
|
printable?: boolean;
|
|
74
74
|
year?: boolean;
|
|
75
|
-
|
|
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("
|
|
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.
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
}/${
|
|
131
|
-
.
|
|
132
|
-
|
|
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
|
+
}
|