@yume-chan/android-bin 0.0.0-20240714132542

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 (76) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/LICENSE +21 -0
  3. package/README.md +49 -0
  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.d.ts +28 -0
  9. package/esm/bu.d.ts.map +1 -0
  10. package/esm/bu.js +53 -0
  11. package/esm/bu.js.map +1 -0
  12. package/esm/bug-report.d.ts +99 -0
  13. package/esm/bug-report.d.ts.map +1 -0
  14. package/esm/bug-report.js +271 -0
  15. package/esm/bug-report.js.map +1 -0
  16. package/esm/cmd.d.ts +25 -0
  17. package/esm/cmd.d.ts.map +1 -0
  18. package/esm/cmd.js +81 -0
  19. package/esm/cmd.js.map +1 -0
  20. package/esm/demo-mode.d.ts +44 -0
  21. package/esm/demo-mode.d.ts.map +1 -0
  22. package/esm/demo-mode.js +179 -0
  23. package/esm/demo-mode.js.map +1 -0
  24. package/esm/dumpsys.d.ts +51 -0
  25. package/esm/dumpsys.d.ts.map +1 -0
  26. package/esm/dumpsys.js +114 -0
  27. package/esm/dumpsys.js.map +1 -0
  28. package/esm/index.d.ts +13 -0
  29. package/esm/index.d.ts.map +1 -0
  30. package/esm/index.js +14 -0
  31. package/esm/index.js.map +1 -0
  32. package/esm/intent.d.ts +11 -0
  33. package/esm/intent.d.ts.map +1 -0
  34. package/esm/intent.js +59 -0
  35. package/esm/intent.js.map +1 -0
  36. package/esm/logcat.d.ts +93 -0
  37. package/esm/logcat.d.ts.map +1 -0
  38. package/esm/logcat.js +300 -0
  39. package/esm/logcat.js.map +1 -0
  40. package/esm/overlay-display.d.ts +38 -0
  41. package/esm/overlay-display.d.ts.map +1 -0
  42. package/esm/overlay-display.js +51 -0
  43. package/esm/overlay-display.js.map +1 -0
  44. package/esm/pm.d.ts +211 -0
  45. package/esm/pm.d.ts.map +1 -0
  46. package/esm/pm.js +382 -0
  47. package/esm/pm.js.map +1 -0
  48. package/esm/settings.d.ts +27 -0
  49. package/esm/settings.d.ts.map +1 -0
  50. package/esm/settings.js +66 -0
  51. package/esm/settings.js.map +1 -0
  52. package/esm/string-format.d.ts +43 -0
  53. package/esm/string-format.d.ts.map +1 -0
  54. package/esm/string-format.js +152 -0
  55. package/esm/string-format.js.map +1 -0
  56. package/esm/utils.d.ts +4 -0
  57. package/esm/utils.d.ts.map +1 -0
  58. package/esm/utils.js +23 -0
  59. package/esm/utils.js.map +1 -0
  60. package/package.json +49 -0
  61. package/src/am.ts +71 -0
  62. package/src/bu.ts +80 -0
  63. package/src/bug-report.ts +348 -0
  64. package/src/cmd.ts +113 -0
  65. package/src/demo-mode.ts +232 -0
  66. package/src/dumpsys.ts +153 -0
  67. package/src/index.ts +14 -0
  68. package/src/intent.ts +73 -0
  69. package/src/logcat.ts +509 -0
  70. package/src/overlay-display.ts +112 -0
  71. package/src/pm.ts +674 -0
  72. package/src/settings.ts +128 -0
  73. package/src/string-format.ts +197 -0
  74. package/src/utils.ts +29 -0
  75. package/tsconfig.build.json +14 -0
  76. package/tsconfig.build.tsbuildinfo +1 -0
package/src/logcat.ts ADDED
@@ -0,0 +1,509 @@
1
+ // cspell: ignore logcat
2
+ // cspell: ignore usec
3
+
4
+ import { AdbCommandBase, AdbSubprocessNoneProtocol } from "@yume-chan/adb";
5
+ import type { ReadableStream } from "@yume-chan/stream-extra";
6
+ import {
7
+ BufferedTransformStream,
8
+ SplitStringStream,
9
+ TextDecoderStream,
10
+ WrapReadableStream,
11
+ WritableStream,
12
+ } from "@yume-chan/stream-extra";
13
+ import type { AsyncExactReadable } from "@yume-chan/struct";
14
+ import Struct, { decodeUtf8 } from "@yume-chan/struct";
15
+
16
+ // `adb logcat` is an alias to `adb shell logcat`
17
+ // so instead of adding to core library, it's implemented here
18
+
19
+ // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/include/android/log.h;l=141;drc=82b5738732161dbaafb2e2f25cce19cd26b9157d
20
+ export enum LogId {
21
+ All = -1,
22
+ Main,
23
+ Radio,
24
+ Events,
25
+ System,
26
+ Crash,
27
+ Stats,
28
+ Security,
29
+ Kernel,
30
+ }
31
+
32
+ // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/include/android/log.h;l=73;drc=82b5738732161dbaafb2e2f25cce19cd26b9157d
33
+ export enum AndroidLogPriority {
34
+ Unknown,
35
+ Default,
36
+ Verbose,
37
+ Debug,
38
+ Info,
39
+ Warn,
40
+ Error,
41
+ Fatal,
42
+ Silent,
43
+ }
44
+
45
+ // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;l=140;drc=8dbf3b2bb6b6d1652d9797e477b9abd03278bb79
46
+ export const AndroidLogPriorityToCharacter: Record<AndroidLogPriority, string> =
47
+ {
48
+ [AndroidLogPriority.Unknown]: "?",
49
+ [AndroidLogPriority.Default]: "?",
50
+ [AndroidLogPriority.Verbose]: "V",
51
+ [AndroidLogPriority.Debug]: "D",
52
+ [AndroidLogPriority.Info]: "I",
53
+ [AndroidLogPriority.Warn]: "W",
54
+ [AndroidLogPriority.Error]: "E",
55
+ [AndroidLogPriority.Fatal]: "F",
56
+ [AndroidLogPriority.Silent]: "S",
57
+ };
58
+
59
+ export enum LogcatFormat {
60
+ Brief,
61
+ Process,
62
+ Tag,
63
+ Thread,
64
+ Raw,
65
+ Time,
66
+ ThreadTime,
67
+ Long,
68
+ }
69
+
70
+ export interface LogcatFormatModifiers {
71
+ microseconds?: boolean;
72
+ nanoseconds?: boolean;
73
+ printable?: boolean;
74
+ year?: boolean;
75
+ timezone?: boolean;
76
+ epoch?: boolean;
77
+ monotonic?: boolean;
78
+ uid?: boolean;
79
+ descriptive?: boolean;
80
+ }
81
+
82
+ export interface LogcatOptions {
83
+ dump?: boolean;
84
+ pid?: number;
85
+ ids?: LogId[];
86
+ }
87
+
88
+ const NANOSECONDS_PER_SECOND = BigInt(1e9);
89
+
90
+ // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/include/log/log_read.h;l=39;drc=82b5738732161dbaafb2e2f25cce19cd26b9157d
91
+ export const LoggerEntry = new Struct({ littleEndian: true })
92
+ .uint16("payloadSize")
93
+ .uint16("headerSize")
94
+ .int32("pid")
95
+ .uint32("tid")
96
+ .uint32("seconds")
97
+ .uint32("nanoseconds")
98
+ .uint32("logId")
99
+ .uint32("uid")
100
+ .extra({
101
+ get timestamp() {
102
+ return (
103
+ BigInt(this.seconds) * NANOSECONDS_PER_SECOND +
104
+ BigInt(this.nanoseconds)
105
+ );
106
+ },
107
+ });
108
+
109
+ export type LoggerEntry = (typeof LoggerEntry)["TDeserializeResult"];
110
+
111
+ // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;drc=bbe77d66e7bee8bd1f0bc7e5492b5376b0207ef6;bpv=0
112
+ export interface AndroidLogEntry extends LoggerEntry {
113
+ priority: AndroidLogPriority;
114
+ tag: string;
115
+ message: string;
116
+
117
+ toString(format?: LogcatFormat, modifiers?: LogcatFormatModifiers): string;
118
+ }
119
+
120
+ function padZero(number: number, length: number) {
121
+ return number.toString().padStart(length, "0");
122
+ }
123
+
124
+ function formatSeconds(seconds: number, modifiers: LogcatFormatModifiers) {
125
+ if (modifiers.monotonic) {
126
+ return padZero(seconds, 6);
127
+ }
128
+
129
+ if (modifiers.epoch) {
130
+ return padZero(seconds, 19);
131
+ }
132
+
133
+ const date = new Date(seconds * 1000);
134
+
135
+ const month = padZero(date.getMonth() + 1, 2);
136
+ const day = padZero(date.getDate(), 2);
137
+ const hour = padZero(date.getHours(), 2);
138
+ const minute = padZero(date.getMinutes(), 2);
139
+ const second = padZero(date.getSeconds(), 2);
140
+ const result = `${month}-${day} ${hour}:${minute}:${second}`;
141
+
142
+ if (modifiers.year) {
143
+ const year = padZero(date.getFullYear(), 4);
144
+ return `${year}-${result}`;
145
+ }
146
+
147
+ return result;
148
+ }
149
+
150
+ function formatNanoseconds(
151
+ nanoseconds: number,
152
+ modifiers: LogcatFormatModifiers,
153
+ ) {
154
+ if (modifiers.nanoseconds) {
155
+ return padZero(nanoseconds, 9);
156
+ }
157
+
158
+ if (modifiers.microseconds) {
159
+ return padZero(nanoseconds / 1000, 6);
160
+ }
161
+
162
+ return padZero(nanoseconds / 1000000, 3);
163
+ }
164
+
165
+ function formatTimezone(seconds: number, modifiers: LogcatFormatModifiers) {
166
+ if (!modifiers.timezone || modifiers.monotonic || modifiers.epoch) {
167
+ return "";
168
+ }
169
+
170
+ const date = new Date(seconds * 1000);
171
+ const offset = date.getTimezoneOffset();
172
+ const sign = offset <= 0 ? "+" : "-";
173
+ const absolute = Math.abs(offset);
174
+ const hours = (absolute / 60) | 0;
175
+ const minutes = absolute % 60;
176
+
177
+ // prettier-ignore
178
+ return ` ${
179
+ sign
180
+ }${
181
+ hours.toString().padStart(2, "0")
182
+ }:${
183
+ minutes.toString().padStart(2, "0")
184
+ }`;
185
+ }
186
+
187
+ function formatTime(
188
+ seconds: number,
189
+ nanoseconds: number,
190
+ modifiers: LogcatFormatModifiers,
191
+ ) {
192
+ const secondsString = formatSeconds(seconds, modifiers);
193
+ const nanosecondsString = formatNanoseconds(nanoseconds, modifiers);
194
+ const zoneString = formatTimezone(seconds, modifiers);
195
+ return `${secondsString}.${nanosecondsString}${zoneString}`;
196
+ }
197
+
198
+ function formatUid(
199
+ uid: number,
200
+ modifiers: LogcatFormatModifiers,
201
+ suffix: string,
202
+ ) {
203
+ return modifiers.uid ? `${uid.toString().padStart(5)}${suffix}` : "";
204
+ }
205
+
206
+ function getFormatPrefix(
207
+ entry: AndroidLogEntry,
208
+ format: LogcatFormat,
209
+ modifiers: LogcatFormatModifiers,
210
+ ) {
211
+ // https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;l=1415;drc=8dbf3b2bb6b6d1652d9797e477b9abd03278bb79
212
+ switch (format) {
213
+ // TODO: implement other formats
214
+ case LogcatFormat.Tag:
215
+ // prettier-ignore
216
+ return `${
217
+ AndroidLogPriorityToCharacter[entry.priority]
218
+ }/${
219
+ entry.tag.padEnd(8)
220
+ }: `;
221
+ case LogcatFormat.Process:
222
+ // prettier-ignore
223
+ return `${
224
+ AndroidLogPriorityToCharacter[entry.priority]
225
+ }(${
226
+ formatUid(entry.uid, modifiers, ":")
227
+ }${
228
+ entry.pid.toString().padStart(5)
229
+ }) `;
230
+ case LogcatFormat.Thread:
231
+ // prettier-ignore
232
+ return `${
233
+ AndroidLogPriorityToCharacter[entry.priority]
234
+ }(${
235
+ formatUid(entry.uid, modifiers, ":")
236
+ }${
237
+ entry.pid.toString().padStart(5)
238
+ }:${
239
+ entry.tid.toString().padStart(5)
240
+ }) `;
241
+ case LogcatFormat.Raw:
242
+ return "";
243
+ case LogcatFormat.Time:
244
+ // prettier-ignore
245
+ return `${
246
+ formatTime(entry.seconds, entry.nanoseconds, modifiers)
247
+ } ${
248
+ AndroidLogPriorityToCharacter[entry.priority]
249
+ }/${
250
+ entry.tag.padEnd(8)
251
+ }(${
252
+ formatUid(entry.uid, modifiers, ":")
253
+ }${
254
+ entry.pid.toString().padStart(5)
255
+ }): `;
256
+ case LogcatFormat.ThreadTime:
257
+ // prettier-ignore
258
+ return `${
259
+ formatTime(entry.seconds, entry.nanoseconds, modifiers)
260
+ } ${
261
+ formatUid(entry.uid, modifiers, " ")
262
+ }${
263
+ entry.pid.toString().padStart(5)
264
+ } ${
265
+ entry.tid.toString().padStart(5)
266
+ } ${
267
+ AndroidLogPriorityToCharacter[entry.priority]
268
+ } ${
269
+ entry.tag.toString().padEnd(8)
270
+ }: `;
271
+ case LogcatFormat.Brief:
272
+ default:
273
+ // prettier-ignore
274
+ return `${
275
+ AndroidLogPriorityToCharacter[entry.priority]
276
+ }/${
277
+ entry.tag.padEnd(8)
278
+ }(${
279
+ formatUid(entry.uid, modifiers, ":")
280
+ }${
281
+ entry.pid.toString().padStart(5)
282
+ }): `;
283
+ }
284
+ }
285
+
286
+ function getFormatSuffix(entry: AndroidLogEntry, format: LogcatFormat) {
287
+ switch (format) {
288
+ case LogcatFormat.Process:
289
+ return ` (${entry.tag})`;
290
+ default:
291
+ return "";
292
+ }
293
+ }
294
+
295
+ function formatEntryWrapLine(
296
+ entry: AndroidLogEntry,
297
+ format: LogcatFormat,
298
+ modifiers: LogcatFormatModifiers,
299
+ ) {
300
+ const prefix = getFormatPrefix(entry, format, modifiers);
301
+ const suffix = getFormatSuffix(entry, format);
302
+ return (
303
+ prefix + entry.message.replaceAll("\n", suffix + "\n" + prefix) + suffix
304
+ );
305
+ }
306
+
307
+ function AndroidLogEntryToString(
308
+ this: AndroidLogEntry,
309
+ format: LogcatFormat = LogcatFormat.ThreadTime,
310
+ modifiers: LogcatFormatModifiers = {},
311
+ ) {
312
+ switch (format) {
313
+ case LogcatFormat.Long:
314
+ // prettier-ignore
315
+ return `[ ${
316
+ formatTime(this.seconds, this.nanoseconds, modifiers)
317
+ } ${
318
+ formatUid(this.uid, modifiers, ":")
319
+ }${
320
+ this.pid.toString().padStart(5)
321
+ }:${
322
+ this.tid.toString().padStart(5)
323
+ } ${
324
+ AndroidLogPriorityToCharacter[this.priority]
325
+ }/${
326
+ this.tag.padEnd(8)
327
+ } ]\n${
328
+ this.message
329
+ }\n`;
330
+ default:
331
+ return formatEntryWrapLine(this, format, modifiers);
332
+ }
333
+ }
334
+
335
+ function findTagEnd(payload: Uint8Array) {
336
+ for (const separator of [0, " ".charCodeAt(0), ":".charCodeAt(0)]) {
337
+ const index = payload.indexOf(separator);
338
+ if (index !== -1) {
339
+ return index;
340
+ }
341
+ }
342
+
343
+ const index = payload.findIndex((x) => x >= 0x7f);
344
+ if (index !== -1) {
345
+ return index;
346
+ }
347
+
348
+ return payload.length;
349
+ }
350
+
351
+ export async function deserializeAndroidLogEntry(
352
+ stream: AsyncExactReadable,
353
+ ): Promise<AndroidLogEntry> {
354
+ const entry = (await LoggerEntry.deserialize(stream)) as AndroidLogEntry;
355
+ if (entry.headerSize !== LoggerEntry.size) {
356
+ // Skip unknown fields
357
+ await stream.readExactly(entry.headerSize - LoggerEntry.size);
358
+ }
359
+
360
+ let payload = await stream.readExactly(entry.payloadSize);
361
+
362
+ // https://cs.android.com/android/platform/superproject/+/master:system/logging/logcat/logcat.cpp;l=193-194;drc=bbe77d66e7bee8bd1f0bc7e5492b5376b0207ef6
363
+ // TODO: payload for some log IDs are in binary format.
364
+ entry.priority = payload[0] as AndroidLogPriority;
365
+
366
+ payload = payload.subarray(1);
367
+ const tagEnd = findTagEnd(payload);
368
+ entry.tag = decodeUtf8(payload.subarray(0, tagEnd));
369
+ entry.message =
370
+ tagEnd < payload.length - 1
371
+ ? decodeUtf8(payload.subarray(tagEnd + 1))
372
+ : "";
373
+ entry.toString = AndroidLogEntryToString;
374
+ return entry;
375
+ }
376
+
377
+ export interface LogSize {
378
+ id: LogId;
379
+ size: number;
380
+ readable?: number;
381
+ consumed: number;
382
+ maxEntrySize: number;
383
+ maxPayloadSize: number;
384
+ }
385
+
386
+ export class Logcat extends AdbCommandBase {
387
+ static logIdToName(id: LogId): string {
388
+ return LogId[id];
389
+ }
390
+
391
+ static logNameToId(name: string): LogId {
392
+ const key = name[0]!.toUpperCase() + name.substring(1);
393
+ return LogId[key as keyof typeof LogId];
394
+ }
395
+
396
+ static joinLogId(ids: LogId[]): string {
397
+ return ids.map((id) => Logcat.logIdToName(id)).join(",");
398
+ }
399
+
400
+ static parseSize(value: number, multiplier: string): number {
401
+ const MULTIPLIERS = ["", "Ki", "Mi", "Gi"];
402
+ return value * 1024 ** (MULTIPLIERS.indexOf(multiplier) || 0);
403
+ }
404
+
405
+ // TODO: logcat: Support output format before Android 10
406
+ // ref https://android-review.googlesource.com/c/platform/system/core/+/748128
407
+ static readonly LOG_SIZE_REGEX_10: RegExp =
408
+ /(.*): ring buffer is (.*) (.*)B \((.*) (.*)B consumed\), max entry is (.*) B, max payload is (.*) B/;
409
+
410
+ // Android 11 added `readable` part
411
+ // ref https://android-review.googlesource.com/c/platform/system/core/+/1390940
412
+ static readonly LOG_SIZE_REGEX_11: RegExp =
413
+ /(.*): ring buffer is (.*) (.*)B \((.*) (.*)B consumed, (.*) (.*)B readable\), max entry is (.*) B, max payload is (.*) B/;
414
+
415
+ async getLogSize(ids?: LogId[]): Promise<LogSize[]> {
416
+ const { stdout } = await this.adb.subprocess.spawn([
417
+ "logcat",
418
+ "-g",
419
+ ...(ids ? ["-b", Logcat.joinLogId(ids)] : []),
420
+ ]);
421
+
422
+ const result: LogSize[] = [];
423
+ await stdout
424
+ .pipeThrough(new TextDecoderStream())
425
+ .pipeThrough(new SplitStringStream("\n"))
426
+ .pipeTo(
427
+ new WritableStream({
428
+ write(chunk) {
429
+ let match = chunk.match(Logcat.LOG_SIZE_REGEX_11);
430
+ if (match) {
431
+ result.push({
432
+ id: Logcat.logNameToId(match[1]!),
433
+ size: Logcat.parseSize(
434
+ Number.parseInt(match[2]!, 10),
435
+ match[3]!,
436
+ ),
437
+ readable: Logcat.parseSize(
438
+ Number.parseInt(match[6]!, 10),
439
+ match[7]!,
440
+ ),
441
+ consumed: Logcat.parseSize(
442
+ Number.parseInt(match[4]!, 10),
443
+ match[5]!,
444
+ ),
445
+ maxEntrySize: parseInt(match[8]!, 10),
446
+ maxPayloadSize: parseInt(match[9]!, 10),
447
+ });
448
+ return;
449
+ }
450
+
451
+ match = chunk.match(Logcat.LOG_SIZE_REGEX_10);
452
+ if (match) {
453
+ result.push({
454
+ id: Logcat.logNameToId(match[1]!),
455
+ size: Logcat.parseSize(
456
+ Number.parseInt(match[2]!, 10),
457
+ match[3]!,
458
+ ),
459
+ consumed: Logcat.parseSize(
460
+ Number.parseInt(match[4]!, 10),
461
+ match[5]!,
462
+ ),
463
+ maxEntrySize: parseInt(match[6]!, 10),
464
+ maxPayloadSize: parseInt(match[7]!, 10),
465
+ });
466
+ }
467
+ },
468
+ }),
469
+ );
470
+
471
+ return result;
472
+ }
473
+
474
+ async clear(ids?: LogId[]): Promise<void> {
475
+ const args = ["logcat", "-c"];
476
+ if (ids && ids.length > 0) {
477
+ args.push("-b", Logcat.joinLogId(ids));
478
+ }
479
+
480
+ await this.adb.subprocess.spawnAndWait(args);
481
+ }
482
+
483
+ binary(options?: LogcatOptions): ReadableStream<AndroidLogEntry> {
484
+ return new WrapReadableStream(async () => {
485
+ const args = ["logcat", "-B"];
486
+ if (options?.dump) {
487
+ args.push("-d");
488
+ }
489
+ if (options?.pid) {
490
+ args.push("--pid", options.pid.toString());
491
+ }
492
+ if (options?.ids) {
493
+ args.push("-b", Logcat.joinLogId(options.ids));
494
+ }
495
+
496
+ // TODO: make `spawn` return synchronously with streams pending
497
+ // so it's easier to chain them.
498
+ const { stdout } = await this.adb.subprocess.spawn(args, {
499
+ // PERF: None protocol is 150% faster then Shell protocol
500
+ protocols: [AdbSubprocessNoneProtocol],
501
+ });
502
+ return stdout;
503
+ }).pipeThrough(
504
+ new BufferedTransformStream((stream) => {
505
+ return deserializeAndroidLogEntry(stream);
506
+ }),
507
+ );
508
+ }
509
+ }
@@ -0,0 +1,112 @@
1
+ import type { Adb } from "@yume-chan/adb";
2
+ import { AdbCommandBase } from "@yume-chan/adb";
3
+
4
+ import { Settings } from "./settings.js";
5
+ import { p } from "./string-format.js";
6
+
7
+ export interface OverlayDisplayDeviceMode {
8
+ width: number;
9
+ height: number;
10
+ density: number;
11
+ }
12
+
13
+ export interface OverlayDisplayDevice {
14
+ modes: OverlayDisplayDeviceMode[];
15
+ secure: boolean;
16
+ ownContentOnly: boolean;
17
+ showSystemDecorations: boolean;
18
+ }
19
+
20
+ export class OverlayDisplay extends AdbCommandBase {
21
+ #settings: Settings;
22
+
23
+ static readonly SETTING_KEY = "overlay_display_devices";
24
+
25
+ static readonly SETTING_FORMAT = p.separated(
26
+ ";",
27
+ p.sequence(
28
+ {
29
+ name: "modes",
30
+ format: p.separated(
31
+ "|",
32
+ p.sequence(
33
+ { name: "width", format: p.digits() },
34
+ p.literal("x"),
35
+ { name: "height", format: p.digits() },
36
+ p.literal("/"),
37
+ { name: "density", format: p.digits() },
38
+ ),
39
+ { min: 1 },
40
+ ),
41
+ },
42
+ {
43
+ name: "flags",
44
+ format: p.map(
45
+ p.repeated(
46
+ p.sequence(p.literal(","), {
47
+ name: "flag",
48
+ format: p.union(
49
+ p.literal("secure"),
50
+ p.literal("own_content_only"),
51
+ p.literal("show_system_decorations"),
52
+ ),
53
+ }),
54
+ ),
55
+ (value) => value.map((item) => item.flag),
56
+ (value) => value.map((item) => ({ flag: item })),
57
+ ),
58
+ },
59
+ ),
60
+ );
61
+
62
+ constructor(adb: Adb) {
63
+ super(adb);
64
+ this.#settings = new Settings(adb);
65
+ }
66
+
67
+ async get() {
68
+ return OverlayDisplay.SETTING_FORMAT.parse({
69
+ value: await this.#settings.get(
70
+ "global",
71
+ OverlayDisplay.SETTING_KEY,
72
+ ),
73
+ position: 0,
74
+ }).map((device) => ({
75
+ modes: device.modes,
76
+ secure: device.flags.includes("secure"),
77
+ ownContentOnly: device.flags.includes("own_content_only"),
78
+ showSystemDecorations: device.flags.includes(
79
+ "show_system_decorations",
80
+ ),
81
+ }));
82
+ }
83
+
84
+ async set(devices: OverlayDisplayDevice[]): Promise<void> {
85
+ await this.#settings.put(
86
+ "global",
87
+ OverlayDisplay.SETTING_KEY,
88
+ OverlayDisplay.SETTING_FORMAT.stringify(
89
+ devices.map((device) => {
90
+ const flags: (
91
+ | "secure"
92
+ | "own_content_only"
93
+ | "show_system_decorations"
94
+ )[] = [];
95
+ if (device.secure) {
96
+ flags.push("secure");
97
+ }
98
+ if (device.ownContentOnly) {
99
+ flags.push("own_content_only");
100
+ }
101
+ if (device.showSystemDecorations) {
102
+ flags.push("show_system_decorations");
103
+ }
104
+ return {
105
+ modes: device.modes,
106
+ flags,
107
+ };
108
+ }),
109
+ ),
110
+ );
111
+ }
112
+ }