@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
@@ -0,0 +1,348 @@
1
+ // cspell: ignore bugreport
2
+ // cspell: ignore bugreportz
3
+
4
+ import type { Adb, AdbSync } from "@yume-chan/adb";
5
+ import { AdbCommandBase, AdbSubprocessShellProtocol } from "@yume-chan/adb";
6
+ import type { AbortSignal, ReadableStream } from "@yume-chan/stream-extra";
7
+ import {
8
+ AbortController,
9
+ PushReadableStream,
10
+ SplitStringStream,
11
+ TextDecoderStream,
12
+ WrapReadableStream,
13
+ WritableStream,
14
+ } from "@yume-chan/stream-extra";
15
+
16
+ export interface BugReportCapabilities {
17
+ supportsBugReport: boolean;
18
+ bugReportZVersion?: string | undefined;
19
+ supportsBugReportZ: boolean;
20
+ supportsBugReportZProgress: boolean;
21
+ supportsBugReportZStream: boolean;
22
+ }
23
+
24
+ export interface BugReportZOptions {
25
+ signal?: AbortSignal;
26
+ /**
27
+ * A callback that will be called when progress is updated.
28
+ *
29
+ * Specify `onProgress` when `supportsBugReportZProgress` is `false` will throw an error.
30
+ */
31
+ onProgress?: ((completed: string, total: string) => void) | undefined;
32
+ }
33
+
34
+ export class BugReport extends AdbCommandBase {
35
+ static VERSION_REGEX: RegExp = /(\d+)\.(\d+)/;
36
+
37
+ static BEGIN_REGEX: RegExp = /BEGIN:(.*)/;
38
+
39
+ static PROGRESS_REGEX: RegExp = /PROGRESS:(.*)\/(.*)/;
40
+
41
+ static OK_REGEX: RegExp = /OK:(.*)/;
42
+
43
+ static FAIL_REGEX: RegExp = /FAIL:(.*)/;
44
+
45
+ /**
46
+ * Queries the device's bugreport capabilities.
47
+ */
48
+ static async queryCapabilities(adb: Adb): Promise<BugReport> {
49
+ // bugreportz requires shell protocol
50
+ if (!AdbSubprocessShellProtocol.isSupported(adb)) {
51
+ return new BugReport(adb, {
52
+ supportsBugReport: true,
53
+ bugReportZVersion: undefined,
54
+ supportsBugReportZ: false,
55
+ supportsBugReportZProgress: false,
56
+ supportsBugReportZStream: false,
57
+ });
58
+ }
59
+
60
+ const { stderr, exitCode } = await adb.subprocess.spawnAndWait([
61
+ "bugreportz",
62
+ "-v",
63
+ ]);
64
+ if (exitCode !== 0 || stderr === "") {
65
+ return new BugReport(adb, {
66
+ supportsBugReport: true,
67
+ bugReportZVersion: undefined,
68
+ supportsBugReportZ: false,
69
+ supportsBugReportZProgress: false,
70
+ supportsBugReportZStream: false,
71
+ });
72
+ }
73
+
74
+ const match = stderr.match(BugReport.VERSION_REGEX);
75
+ if (!match) {
76
+ return new BugReport(adb, {
77
+ supportsBugReport: true,
78
+ bugReportZVersion: undefined,
79
+ supportsBugReportZ: false,
80
+ supportsBugReportZProgress: false,
81
+ supportsBugReportZStream: false,
82
+ });
83
+ }
84
+
85
+ const [major, minor] = match[0]
86
+ .split(".")
87
+ .map((x) => parseInt(x, 10)) as [number, number];
88
+ return new BugReport(adb, {
89
+ // Before BugReportZ version 1.2 (Android 12), BugReport was deprecated but still works.
90
+ supportsBugReport: major === 1 && minor <= 1,
91
+ bugReportZVersion: match[0],
92
+ supportsBugReportZ: true,
93
+ supportsBugReportZProgress: major > 1 || minor >= 1,
94
+ supportsBugReportZStream: major > 1 || minor >= 2,
95
+ });
96
+ }
97
+
98
+ #supportsBugReport: boolean;
99
+ /**
100
+ * Gets whether the device supports flat (text file, non-zipped) bugreport.
101
+ *
102
+ * Should be `true` for Android version <= 11.
103
+ */
104
+ get supportsBugReport(): boolean {
105
+ return this.#supportsBugReport;
106
+ }
107
+
108
+ #bugReportZVersion: string | undefined;
109
+ /**
110
+ * Gets the version of BugReportZ.
111
+ *
112
+ * Will be `undefined` if BugReportZ is not supported.
113
+ */
114
+ get bugReportZVersion(): string | undefined {
115
+ return this.#bugReportZVersion;
116
+ }
117
+
118
+ #supportsBugReportZ: boolean;
119
+ /**
120
+ * Gets whether the device supports zipped bugreport.
121
+ *
122
+ * Should be `true` for Android version >= 7.
123
+ */
124
+ get supportsBugReportZ(): boolean {
125
+ return this.#supportsBugReportZ;
126
+ }
127
+
128
+ #supportsBugReportZProgress: boolean;
129
+ /**
130
+ * Gets whether the device supports progress report for zipped bugreport.
131
+ *
132
+ * Should be `true` for Android version >= 8.
133
+ */
134
+ get supportsBugReportZProgress(): boolean {
135
+ return this.#supportsBugReportZProgress;
136
+ }
137
+
138
+ #supportsBugReportZStream: boolean;
139
+ /**
140
+ * Gets whether the device supports streaming zipped bugreport.
141
+ *
142
+ * Should be `true` for Android version >= 12.
143
+ */
144
+ get supportsBugReportZStream(): boolean {
145
+ return this.#supportsBugReportZStream;
146
+ }
147
+
148
+ constructor(adb: Adb, capabilities: BugReportCapabilities) {
149
+ super(adb);
150
+
151
+ this.#supportsBugReport = capabilities.supportsBugReport;
152
+ this.#bugReportZVersion = capabilities.bugReportZVersion;
153
+ this.#supportsBugReportZ = capabilities.supportsBugReportZ;
154
+ this.#supportsBugReportZProgress =
155
+ capabilities.supportsBugReportZProgress;
156
+ this.#supportsBugReportZStream = capabilities.supportsBugReportZStream;
157
+ }
158
+
159
+ /**
160
+ * Creates a legacy, non-zipped bugreport file, or throws an error if `supportsBugReport` is `false`.
161
+ *
162
+ * @returns A flat (text file, non-zipped) bugreport.
163
+ */
164
+ bugReport(): ReadableStream<Uint8Array> {
165
+ if (!this.#supportsBugReport) {
166
+ throw new Error(
167
+ "Flat (text file, non-zipped) bugreport is not supported.",
168
+ );
169
+ }
170
+
171
+ return new WrapReadableStream(async () => {
172
+ // https://cs.android.com/android/platform/superproject/+/master:frameworks/native/cmds/bugreport/bugreport.cpp;drc=9b73bf07d73dbab5b792632e1e233edbad77f5fd;bpv=0;bpt=0
173
+ const process = await this.adb.subprocess.spawn(["bugreport"]);
174
+ return process.stdout;
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Creates a zipped bugreport file, or throws an error if `supportsBugReportZ` is `false`.
180
+ *
181
+ * Compare to `bugReportZStream`, this method will write the output to a file on device.
182
+ * You can pull it later using sync protocol.
183
+ *
184
+ * @returns The path to the generated bugreport file on device filesystem.
185
+ */
186
+ async bugReportZ(options?: BugReportZOptions): Promise<string> {
187
+ if (options?.signal?.aborted) {
188
+ throw options?.signal.reason ?? new Error("Aborted");
189
+ }
190
+
191
+ if (!this.#supportsBugReportZ) {
192
+ throw new Error("bugreportz is not supported");
193
+ }
194
+
195
+ const args = ["bugreportz"];
196
+ if (options?.onProgress) {
197
+ if (!this.#supportsBugReportZProgress) {
198
+ throw new Error("bugreportz progress is not supported");
199
+ }
200
+ args.push("-p");
201
+ }
202
+
203
+ const process = await this.adb.subprocess.spawn(args, {
204
+ protocols: [AdbSubprocessShellProtocol],
205
+ });
206
+
207
+ options?.signal?.addEventListener("abort", () => {
208
+ void process.kill();
209
+ });
210
+
211
+ let filename: string | undefined;
212
+ let error: string | undefined;
213
+
214
+ await process.stdout
215
+ .pipeThrough(new TextDecoderStream())
216
+ .pipeThrough(new SplitStringStream("\n"))
217
+ .pipeTo(
218
+ new WritableStream<string>({
219
+ write(line) {
220
+ // `BEGIN:` and `PROGRESS:` only appear when `-p` is specified.
221
+ let match = line.match(BugReport.PROGRESS_REGEX);
222
+ if (match) {
223
+ options?.onProgress?.(match[1]!, match[2]!);
224
+ }
225
+
226
+ match = line.match(BugReport.BEGIN_REGEX);
227
+ if (match) {
228
+ filename = match[1]!;
229
+ }
230
+
231
+ match = line.match(BugReport.OK_REGEX);
232
+ if (match) {
233
+ filename = match[1];
234
+ }
235
+
236
+ match = line.match(BugReport.FAIL_REGEX);
237
+ if (match) {
238
+ // Don't report error now
239
+ // We want to gather all output.
240
+ error = match[1];
241
+ }
242
+ },
243
+ }),
244
+ );
245
+
246
+ if (error) {
247
+ throw new Error(error);
248
+ }
249
+
250
+ if (!filename) {
251
+ throw new Error("bugreportz did not return file name");
252
+ }
253
+
254
+ // Design choice: we don't automatically pull the file to avoid more dependency on `@yume-chan/adb`
255
+ return filename;
256
+ }
257
+
258
+ /**
259
+ * Creates a zipped bugreport file, or throws an error if `supportsBugReportZStream` is `false`.
260
+ *
261
+ * @returns The content of the generated bugreport file.
262
+ */
263
+ bugReportZStream(): ReadableStream<Uint8Array> {
264
+ return new PushReadableStream(async (controller) => {
265
+ const process = await this.adb.subprocess.spawn(
266
+ ["bugreportz", "-s"],
267
+ { protocols: [AdbSubprocessShellProtocol] },
268
+ );
269
+ process.stdout
270
+ .pipeTo(
271
+ new WritableStream({
272
+ async write(chunk) {
273
+ await controller.enqueue(chunk);
274
+ },
275
+ }),
276
+ )
277
+ .catch((e) => {
278
+ controller.error(e);
279
+ });
280
+ process.stderr
281
+ .pipeThrough(new TextDecoderStream())
282
+ .pipeTo(
283
+ new WritableStream({
284
+ write(chunk) {
285
+ controller.error(new Error(chunk));
286
+ },
287
+ }),
288
+ )
289
+ .catch((e) => {
290
+ controller.error(e);
291
+ });
292
+ await process.exit;
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Automatically choose the best bugreport method.
298
+ *
299
+ * * If `supportsBugReportZStream` is `true`, this method will return a stream of zipped bugreport.
300
+ * * If `supportsBugReportZ` is `true`, this method will return a stream of zipped bugreport, and will delete the file on device after the stream is closed.
301
+ * * If `supportsBugReport` is `true`, this method will return a stream of flat bugreport.
302
+ *
303
+ * @param onProgress
304
+ * If `supportsBugReportZStream` is `false` and `supportsBugReportZProgress` is `true`,
305
+ * this callback will be called when progress is updated.
306
+ */
307
+ automatic(onProgress?: (completed: string, total: string) => void): {
308
+ type: "bugreport" | "bugreportz";
309
+ stream: ReadableStream<Uint8Array>;
310
+ } {
311
+ if (this.#supportsBugReportZStream) {
312
+ return { type: "bugreportz", stream: this.bugReportZStream() };
313
+ }
314
+
315
+ if (this.#supportsBugReportZ) {
316
+ let path: string | undefined;
317
+ let sync: AdbSync | undefined;
318
+ const controller = new AbortController();
319
+ const cleanup = async () => {
320
+ controller.abort();
321
+ await sync?.dispose();
322
+ if (path) {
323
+ await this.adb.rm(path);
324
+ }
325
+ };
326
+
327
+ return {
328
+ type: "bugreportz",
329
+ stream: new WrapReadableStream({
330
+ start: async () => {
331
+ path = await this.bugReportZ({
332
+ signal: controller.signal,
333
+ onProgress: this.#supportsBugReportZProgress
334
+ ? onProgress
335
+ : undefined,
336
+ });
337
+ sync = await this.adb.sync();
338
+ return sync.read(path);
339
+ },
340
+ cancel: cleanup,
341
+ close: cleanup,
342
+ }),
343
+ };
344
+ }
345
+
346
+ return { type: "bugreport", stream: this.bugReport() };
347
+ }
348
+ }
package/src/cmd.ts ADDED
@@ -0,0 +1,113 @@
1
+ import type {
2
+ Adb,
3
+ AdbSubprocessProtocol,
4
+ AdbSubprocessProtocolConstructor,
5
+ AdbSubprocessWaitResult,
6
+ } from "@yume-chan/adb";
7
+ import {
8
+ AdbCommandBase,
9
+ AdbFeature,
10
+ AdbSubprocessNoneProtocol,
11
+ AdbSubprocessShellProtocol,
12
+ } from "@yume-chan/adb";
13
+ import { ConcatStringStream, TextDecoderStream } from "@yume-chan/stream-extra";
14
+
15
+ export class Cmd extends AdbCommandBase {
16
+ #supportsShellV2: boolean;
17
+ get supportsShellV2(): boolean {
18
+ return this.#supportsShellV2;
19
+ }
20
+
21
+ #supportsCmd: boolean;
22
+ get supportsCmd(): boolean {
23
+ return this.#supportsCmd;
24
+ }
25
+
26
+ #supportsAbb: boolean;
27
+ get supportsAbb(): boolean {
28
+ return this.#supportsAbb;
29
+ }
30
+
31
+ #supportsAbbExec: boolean;
32
+ get supportsAbbExec(): boolean {
33
+ return this.#supportsAbbExec;
34
+ }
35
+
36
+ constructor(adb: Adb) {
37
+ super(adb);
38
+ this.#supportsShellV2 = adb.canUseFeature(AdbFeature.ShellV2);
39
+ this.#supportsCmd = adb.canUseFeature(AdbFeature.Cmd);
40
+ this.#supportsAbb = adb.canUseFeature(AdbFeature.Abb);
41
+ this.#supportsAbbExec = adb.canUseFeature(AdbFeature.AbbExec);
42
+ }
43
+
44
+ /**
45
+ * Spawn a new `cmd` command. It will use ADB's `abb` command if available.
46
+ *
47
+ * @param shellProtocol
48
+ * Whether to use shell protocol. If `true`, `stdout` and `stderr` will be separated.
49
+ *
50
+ * `cmd` doesn't use PTY, so even when shell protocol is used,
51
+ * resizing terminal size and closing `stdin` are not supported.
52
+ * @param command The command to run.
53
+ * @param args The arguments to pass to the command.
54
+ * @returns An `AdbSubprocessProtocol` that provides output streams.
55
+ */
56
+ async spawn(
57
+ shellProtocol: boolean,
58
+ command: string,
59
+ ...args: string[]
60
+ ): Promise<AdbSubprocessProtocol> {
61
+ let supportsAbb: boolean;
62
+ let supportsCmd: boolean = this.#supportsCmd;
63
+ let service: string;
64
+ let Protocol: AdbSubprocessProtocolConstructor;
65
+ if (shellProtocol) {
66
+ supportsAbb = this.#supportsAbb;
67
+ supportsCmd &&= this.supportsShellV2;
68
+ service = "abb";
69
+ Protocol = AdbSubprocessShellProtocol;
70
+ } else {
71
+ supportsAbb = this.#supportsAbbExec;
72
+ service = "abb_exec";
73
+ Protocol = AdbSubprocessNoneProtocol;
74
+ }
75
+
76
+ if (supportsAbb) {
77
+ return new Protocol(
78
+ await this.adb.createSocket(
79
+ `${service}:${command}\0${args.join("\0")}\0`,
80
+ ),
81
+ );
82
+ }
83
+
84
+ if (supportsCmd) {
85
+ return Protocol.raw(this.adb, `cmd ${command} ${args.join(" ")}`);
86
+ }
87
+
88
+ throw new Error("Not supported");
89
+ }
90
+
91
+ async spawnAndWait(
92
+ command: string,
93
+ ...args: string[]
94
+ ): Promise<AdbSubprocessWaitResult> {
95
+ const process = await this.spawn(true, command, ...args);
96
+
97
+ const [stdout, stderr, exitCode] = await Promise.all([
98
+ process.stdout
99
+ .pipeThrough(new TextDecoderStream())
100
+ .pipeThrough(new ConcatStringStream()),
101
+ process.stderr
102
+ .pipeThrough(new TextDecoderStream())
103
+ .pipeThrough(new ConcatStringStream()),
104
+ process.exit,
105
+ ]);
106
+
107
+ return {
108
+ stdout,
109
+ stderr,
110
+ exitCode,
111
+ };
112
+ }
113
+ }
@@ -0,0 +1,232 @@
1
+ // cspell: ignore carriernetworkchange
2
+ // cspell: ignore powersave
3
+ // cspell: ignore nosim
4
+ // cspell: ignore systemui
5
+ // cspell: ignore sysui
6
+
7
+ import type { Adb } from "@yume-chan/adb";
8
+ import { AdbCommandBase } from "@yume-chan/adb";
9
+
10
+ import { Settings } from "./settings.js";
11
+
12
+ export enum DemoModeSignalStrength {
13
+ Hidden = "null",
14
+ Level0 = "0",
15
+ Level1 = "1",
16
+ Level2 = "2",
17
+ Level3 = "3",
18
+ Level4 = "4",
19
+ }
20
+
21
+ // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java;l=1362;drc=3b775bf7ad89902f94e03d191b0d8fbdebf2bdbf
22
+ export const DemoModeMobileDataTypes = [
23
+ "1x",
24
+ "3g",
25
+ "4g",
26
+ "4g+",
27
+ "5g",
28
+ "5ge",
29
+ "5g+",
30
+ "e",
31
+ "g",
32
+ "h",
33
+ "h+",
34
+ "lte",
35
+ "lte+",
36
+ "dis",
37
+ "not",
38
+ "null",
39
+ ] as const;
40
+
41
+ export type DemoModeMobileDataType = (typeof DemoModeMobileDataTypes)[number];
42
+
43
+ // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java;l=3136
44
+ export const DemoModeStatusBarModes = [
45
+ "opaque",
46
+ "translucent",
47
+ "semi-transparent",
48
+ "transparent",
49
+ "warning",
50
+ ] as const;
51
+
52
+ export type DemoModeStatusBarMode = (typeof DemoModeStatusBarModes)[number];
53
+
54
+ export class DemoMode extends AdbCommandBase {
55
+ #settings: Settings;
56
+
57
+ constructor(adb: Adb) {
58
+ super(adb);
59
+ this.#settings = new Settings(adb);
60
+ }
61
+
62
+ static readonly ALLOWED_SETTING_KEY = "sysui_demo_allowed";
63
+
64
+ // Demo Mode actually doesn't have a setting indicates its enablement
65
+ // However Developer Mode menu uses this key
66
+ // So we can only try our best to guess if it's enabled
67
+ static readonly ENABLED_SETTING_KEY = "sysui_tuner_demo_on";
68
+
69
+ async getAllowed(): Promise<boolean> {
70
+ const output = await this.#settings.get(
71
+ "global",
72
+ DemoMode.ALLOWED_SETTING_KEY,
73
+ );
74
+ return output === "1";
75
+ }
76
+
77
+ async setAllowed(value: boolean): Promise<void> {
78
+ if (value) {
79
+ await this.#settings.put(
80
+ "global",
81
+ DemoMode.ALLOWED_SETTING_KEY,
82
+ "1",
83
+ );
84
+ } else {
85
+ await this.setEnabled(false);
86
+ await this.#settings.delete("global", DemoMode.ALLOWED_SETTING_KEY);
87
+ }
88
+ }
89
+
90
+ async getEnabled(): Promise<boolean> {
91
+ const result = await this.#settings.get(
92
+ "global",
93
+ DemoMode.ENABLED_SETTING_KEY,
94
+ );
95
+ return result === "1";
96
+ }
97
+
98
+ async setEnabled(value: boolean): Promise<void> {
99
+ if (value) {
100
+ await this.#settings.put(
101
+ "global",
102
+ DemoMode.ENABLED_SETTING_KEY,
103
+ "1",
104
+ );
105
+ } else {
106
+ await this.#settings.delete("global", DemoMode.ENABLED_SETTING_KEY);
107
+ await this.broadcast("exit");
108
+ }
109
+ }
110
+
111
+ async broadcast(
112
+ command: string,
113
+ extra?: Record<string, string>,
114
+ ): Promise<void> {
115
+ await this.adb.subprocess.spawnAndWaitLegacy([
116
+ "am",
117
+ "broadcast",
118
+ "-a",
119
+ "com.android.systemui.demo",
120
+ "-e",
121
+ "command",
122
+ command,
123
+ ...(extra
124
+ ? Object.entries(extra).flatMap(([key, value]) => [
125
+ "-e",
126
+ key,
127
+ value,
128
+ ])
129
+ : []),
130
+ ]);
131
+ }
132
+
133
+ async setBatteryLevel(level: number): Promise<void> {
134
+ await this.broadcast("battery", { level: level.toString() });
135
+ }
136
+
137
+ async setBatteryCharging(value: boolean): Promise<void> {
138
+ await this.broadcast("battery", { plugged: value.toString() });
139
+ }
140
+
141
+ async setPowerSaveMode(value: boolean): Promise<void> {
142
+ await this.broadcast("battery", { powersave: value.toString() });
143
+ }
144
+
145
+ async setAirplaneMode(show: boolean): Promise<void> {
146
+ await this.broadcast("network", { airplane: show ? "show" : "hide" });
147
+ }
148
+
149
+ async setWifiSignalStrength(value: DemoModeSignalStrength): Promise<void> {
150
+ await this.broadcast("network", { wifi: "show", level: value });
151
+ }
152
+
153
+ async setMobileDataType(value: DemoModeMobileDataType): Promise<void> {
154
+ for (let i = 0; i < 2; i += 1) {
155
+ await this.broadcast("network", {
156
+ mobile: "show",
157
+ sims: "1",
158
+ nosim: "hide",
159
+ slot: "0",
160
+ datatype: value,
161
+ fully: "true",
162
+ roam: "false",
163
+ level: "4",
164
+ inflate: "false",
165
+ activity: "in",
166
+ carriernetworkchange: "hide",
167
+ });
168
+ }
169
+ }
170
+
171
+ async setMobileSignalStrength(
172
+ value: DemoModeSignalStrength,
173
+ ): Promise<void> {
174
+ await this.broadcast("network", { mobile: "show", level: value });
175
+ }
176
+
177
+ async setNoSimCardIcon(show: boolean): Promise<void> {
178
+ await this.broadcast("network", { nosim: show ? "show" : "hide" });
179
+ }
180
+
181
+ async setStatusBarMode(mode: DemoModeStatusBarMode): Promise<void> {
182
+ await this.broadcast("bars", { mode });
183
+ }
184
+
185
+ async setVibrateModeEnabled(value: boolean): Promise<void> {
186
+ // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java;l=103
187
+ await this.broadcast("status", { volume: value ? "vibrate" : "hide" });
188
+ }
189
+
190
+ async setBluetoothConnected(value: boolean): Promise<void> {
191
+ // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java;l=114
192
+ await this.broadcast("status", {
193
+ bluetooth: value ? "connected" : "hide",
194
+ });
195
+ }
196
+
197
+ async setLocatingIcon(show: boolean): Promise<void> {
198
+ await this.broadcast("status", { location: show ? "show" : "hide" });
199
+ }
200
+
201
+ async setAlarmIcon(show: boolean): Promise<void> {
202
+ await this.broadcast("status", { alarm: show ? "show" : "hide" });
203
+ }
204
+
205
+ async setSyncingIcon(show: boolean): Promise<void> {
206
+ await this.broadcast("status", { sync: show ? "show" : "hide" });
207
+ }
208
+
209
+ async setMuteIcon(show: boolean): Promise<void> {
210
+ await this.broadcast("status", { mute: show ? "show" : "hide" });
211
+ }
212
+
213
+ async setSpeakerPhoneIcon(show: boolean): Promise<void> {
214
+ await this.broadcast("status", {
215
+ speakerphone: show ? "show" : "hide",
216
+ });
217
+ }
218
+
219
+ async setNotificationsVisibility(show: boolean): Promise<void> {
220
+ // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java;l=3131
221
+ await this.broadcast("notifications", { visible: show.toString() });
222
+ }
223
+
224
+ async setTime(hour: number, minute: number): Promise<void> {
225
+ await this.broadcast("clock", {
226
+ // cspell: disable-next-line
227
+ hhmm:
228
+ hour.toString().padStart(2, "0") +
229
+ minute.toString().padStart(2, "0"),
230
+ });
231
+ }
232
+ }