@yume-chan/android-bin 0.0.20 → 0.0.22

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.json +54 -0
  2. package/CHANGELOG.md +25 -1
  3. package/LICENSE +21 -0
  4. package/README.md +40 -14
  5. package/esm/am.d.ts +17 -0
  6. package/esm/am.d.ts.map +1 -0
  7. package/esm/am.js +42 -0
  8. package/esm/am.js.map +1 -0
  9. package/esm/bu.d.ts +20 -7
  10. package/esm/bu.d.ts.map +1 -1
  11. package/esm/bu.js +47 -8
  12. package/esm/bu.js.map +1 -1
  13. package/esm/bug-report.d.ts +82 -21
  14. package/esm/bug-report.d.ts.map +1 -1
  15. package/esm/bug-report.js +187 -53
  16. package/esm/bug-report.js.map +1 -1
  17. package/esm/cmd.d.ts +4 -6
  18. package/esm/cmd.d.ts.map +1 -1
  19. package/esm/cmd.js +36 -21
  20. package/esm/cmd.js.map +1 -1
  21. package/esm/demo-mode.d.ts +3 -3
  22. package/esm/demo-mode.d.ts.map +1 -1
  23. package/esm/demo-mode.js +14 -15
  24. package/esm/demo-mode.js.map +1 -1
  25. package/esm/dumpsys.d.ts +39 -0
  26. package/esm/dumpsys.d.ts.map +1 -1
  27. package/esm/dumpsys.js +111 -0
  28. package/esm/dumpsys.js.map +1 -1
  29. package/esm/index.d.ts +6 -0
  30. package/esm/index.d.ts.map +1 -1
  31. package/esm/index.js +6 -0
  32. package/esm/index.js.map +1 -1
  33. package/esm/intent.d.ts +10 -0
  34. package/esm/intent.d.ts.map +1 -0
  35. package/esm/intent.js +51 -0
  36. package/esm/intent.js.map +1 -0
  37. package/esm/logcat.d.ts +1 -0
  38. package/esm/logcat.d.ts.map +1 -1
  39. package/esm/logcat.js +20 -18
  40. package/esm/logcat.js.map +1 -1
  41. package/esm/overlay-display.d.ts +20 -3
  42. package/esm/overlay-display.d.ts.map +1 -1
  43. package/esm/overlay-display.js +34 -61
  44. package/esm/overlay-display.js.map +1 -1
  45. package/esm/pm.d.ts +42 -5
  46. package/esm/pm.d.ts.map +1 -1
  47. package/esm/pm.js +164 -46
  48. package/esm/pm.js.map +1 -1
  49. package/esm/settings.d.ts +22 -8
  50. package/esm/settings.d.ts.map +1 -1
  51. package/esm/settings.js +50 -25
  52. package/esm/settings.js.map +1 -1
  53. package/esm/string-format.d.ts +40 -0
  54. package/esm/string-format.d.ts.map +1 -0
  55. package/esm/string-format.js +153 -0
  56. package/esm/string-format.js.map +1 -0
  57. package/esm/utils.d.ts +4 -0
  58. package/esm/utils.d.ts.map +1 -0
  59. package/esm/utils.js +23 -0
  60. package/esm/utils.js.map +1 -0
  61. package/package.json +13 -8
  62. package/src/am.ts +69 -0
  63. package/src/bu.ts +68 -14
  64. package/src/bug-report.ts +234 -68
  65. package/src/cmd.ts +64 -30
  66. package/src/demo-mode.ts +47 -43
  67. package/src/dumpsys.ts +129 -0
  68. package/src/index.ts +6 -0
  69. package/src/intent.ts +63 -0
  70. package/src/logcat.ts +45 -42
  71. package/src/overlay-display.ts +82 -85
  72. package/src/pm.ts +274 -61
  73. package/src/settings.ts +94 -43
  74. package/src/string-format.ts +199 -0
  75. package/src/utils.ts +29 -0
  76. package/tsconfig.build.tsbuildinfo +1 -1
@@ -2,6 +2,7 @@ import type { Adb } from "@yume-chan/adb";
2
2
  import { AdbCommandBase } from "@yume-chan/adb";
3
3
 
4
4
  import { Settings } from "./settings.js";
5
+ import { p } from "./string-format.js";
5
6
 
6
7
  export interface OverlayDisplayDeviceMode {
7
8
  width: number;
@@ -17,99 +18,95 @@ export interface OverlayDisplayDevice {
17
18
  }
18
19
 
19
20
  export class OverlayDisplay extends AdbCommandBase {
20
- private settings: Settings;
21
-
22
- public static readonly OVERLAY_DISPLAY_DEVICES_KEY =
23
- "overlay_display_devices";
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
+ 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
+ );
24
61
 
25
62
  constructor(adb: Adb) {
26
63
  super(adb);
27
- this.settings = new Settings(adb);
64
+ this.#settings = new Settings(adb);
28
65
  }
29
66
 
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;
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
+ }));
83
82
  }
84
83
 
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(
84
+ async set(devices: OverlayDisplayDevice[]) {
85
+ await this.#settings.put(
110
86
  "global",
111
- OverlayDisplay.OVERLAY_DISPLAY_DEVICES_KEY,
112
- settingString
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
+ ),
113
110
  );
114
111
  }
115
112
  }
package/src/pm.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // cspell:ignore dont
2
2
  // cspell:ignore instantapp
3
3
  // cspell:ignore apks
4
+ // cspell:ignore versioncode
4
5
 
5
6
  import type { Adb } from "@yume-chan/adb";
6
7
  import {
@@ -9,9 +10,16 @@ import {
9
10
  escapeArg,
10
11
  } from "@yume-chan/adb";
11
12
  import type { Consumable, ReadableStream } from "@yume-chan/stream-extra";
12
- import { DecodeUtf8Stream, WrapReadableStream } from "@yume-chan/stream-extra";
13
+ import {
14
+ ConcatStringStream,
15
+ DecodeUtf8Stream,
16
+ SplitStringStream,
17
+ } from "@yume-chan/stream-extra";
13
18
 
14
19
  import { Cmd } from "./cmd.js";
20
+ import type { IntentBuilder } from "./intent.js";
21
+ import type { SingleUserOrAll } from "./utils.js";
22
+ import { buildArguments } from "./utils.js";
15
23
 
16
24
  export enum PackageManagerInstallLocation {
17
25
  Auto,
@@ -96,7 +104,7 @@ export interface PackageManagerInstallOptions {
96
104
  /**
97
105
  * `--user`
98
106
  */
99
- userId: number;
107
+ user: SingleUserOrAll;
100
108
  /**
101
109
  * `--install-location`
102
110
  */
@@ -163,7 +171,7 @@ export const PACKAGE_MANAGER_INSTALL_OPTIONS_MAP: Record<
163
171
  instantApp: "--instant",
164
172
  full: "--full",
165
173
  preload: "--preload",
166
- userId: "--user",
174
+ user: "--user",
167
175
  installLocation: "--install-location",
168
176
  installReason: "--install-reason",
169
177
  forceUuid: "--force-uuid",
@@ -177,56 +185,107 @@ export const PACKAGE_MANAGER_INSTALL_OPTIONS_MAP: Record<
177
185
  bypassLowTargetSdkBlock: "--bypass-low-target-sdk-block",
178
186
  };
179
187
 
188
+ export interface PackageManagerListPackagesOptions {
189
+ listDisabled: boolean;
190
+ listEnabled: boolean;
191
+ showSourceDir: boolean;
192
+ showInstaller: boolean;
193
+ listSystem: boolean;
194
+ showUid: boolean;
195
+ listThirdParty: boolean;
196
+ showVersionCode: boolean;
197
+ listApexOnly: boolean;
198
+ user: SingleUserOrAll;
199
+ uid: number;
200
+ filter: string;
201
+ }
202
+
203
+ export const PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP: Record<
204
+ keyof PackageManagerListPackagesOptions,
205
+ string
206
+ > = {
207
+ listDisabled: "-d",
208
+ listEnabled: "-e",
209
+ showSourceDir: "-f",
210
+ showInstaller: "-i",
211
+ listSystem: "-s",
212
+ showUid: "-U",
213
+ listThirdParty: "-3",
214
+ showVersionCode: "--show-versioncode",
215
+ listApexOnly: "--apex-only",
216
+ user: "--user",
217
+ uid: "--uid",
218
+ filter: "",
219
+ };
220
+
221
+ export interface PackageManagerListPackagesResult {
222
+ packageName: string;
223
+ sourceDir?: string | undefined;
224
+ versionCode?: number | undefined;
225
+ installer?: string | undefined;
226
+ uid?: number | undefined;
227
+ }
228
+
229
+ export interface PackageManagerUninstallOptions {
230
+ keepData: boolean;
231
+ user: SingleUserOrAll;
232
+ versionCode: number;
233
+ splitNames: string[];
234
+ }
235
+
236
+ const PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP: Record<
237
+ keyof PackageManagerUninstallOptions,
238
+ string
239
+ > = {
240
+ keepData: "-k",
241
+ user: "--user",
242
+ versionCode: "--versionCode",
243
+ splitNames: "",
244
+ };
245
+
246
+ export interface PackageManagerResolveActivityOptions {
247
+ user?: SingleUserOrAll;
248
+ intent: IntentBuilder;
249
+ }
250
+
251
+ const PACKAGE_MANAGER_RESOLVE_ACTIVITY_OPTIONS_MAP: Partial<
252
+ Record<keyof PackageManagerResolveActivityOptions, string>
253
+ > = {
254
+ user: "--user",
255
+ };
256
+
180
257
  export class PackageManager extends AdbCommandBase {
181
- private _cmd: Cmd;
258
+ #cmd: Cmd;
182
259
 
183
- public constructor(adb: Adb) {
260
+ constructor(adb: Adb) {
184
261
  super(adb);
185
- this._cmd = new Cmd(adb);
262
+ this.#cmd = new Cmd(adb);
186
263
  }
187
264
 
188
- private buildInstallArgs(
189
- options?: Partial<PackageManagerInstallOptions>
265
+ #buildInstallArguments(
266
+ options: Partial<PackageManagerInstallOptions> | undefined,
190
267
  ): 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;
268
+ return buildArguments(
269
+ ["pm", "install"],
270
+ options,
271
+ PACKAGE_MANAGER_INSTALL_OPTIONS_MAP,
272
+ );
214
273
  }
215
274
 
216
- public async install(
275
+ async install(
217
276
  apks: string[],
218
- options?: Partial<PackageManagerInstallOptions>
277
+ options?: Partial<PackageManagerInstallOptions>,
219
278
  ): Promise<string> {
220
- const args = this.buildInstallArgs(options);
279
+ const args = this.#buildInstallArguments(options);
221
280
  // WIP: old version of pm doesn't support multiple apks
222
281
  args.push(...apks);
223
282
  return await this.adb.subprocess.spawnAndWaitLegacy(args);
224
283
  }
225
284
 
226
- public async pushAndInstallStream(
285
+ async pushAndInstallStream(
227
286
  stream: ReadableStream<Consumable<Uint8Array>>,
228
- options?: Partial<PackageManagerInstallOptions>
229
- ): Promise<ReadableStream<string>> {
287
+ options?: Partial<PackageManagerInstallOptions>,
288
+ ): Promise<void> {
230
289
  const sync = await this.adb.sync();
231
290
 
232
291
  const fileName = Math.random().toString().substring(2);
@@ -241,41 +300,195 @@ export class PackageManager extends AdbCommandBase {
241
300
  await sync.dispose();
242
301
  }
243
302
 
244
- const args = this.buildInstallArgs(options);
303
+ // Starting from Android 7, `pm` is only a wrapper for `cmd package`,
304
+ // and `cmd package` launches faster than `pm`.
305
+ // But `cmd package` can't read `/data/local/tmp` folder due to SELinux policy,
306
+ // so installing a file must use `pm`.
307
+ const args = this.#buildInstallArguments(options);
245
308
  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
- },
309
+ const process = await this.adb.subprocess.spawn(args.map(escapeArg), {
310
+ protocols: [AdbSubprocessNoneProtocol],
255
311
  });
312
+
313
+ const output = await process.stdout
314
+ .pipeThrough(new DecodeUtf8Stream())
315
+ .pipeThrough(new ConcatStringStream())
316
+ .then((output) => output.trim());
317
+
318
+ await this.adb.rm(filePath);
319
+
320
+ if (output !== "Success") {
321
+ throw new Error(output);
322
+ }
256
323
  }
257
324
 
258
- public async installStream(
325
+ async installStream(
259
326
  size: number,
260
327
  stream: ReadableStream<Consumable<Uint8Array>>,
261
- options?: Partial<PackageManagerInstallOptions>
262
- ): Promise<ReadableStream<string>> {
263
- if (!this._cmd.supportsCmd) {
264
- return this.pushAndInstallStream(stream, options);
328
+ options?: Partial<PackageManagerInstallOptions>,
329
+ ): Promise<void> {
330
+ // Android 7 added both `cmd` command and streaming install support,
331
+ // we can't detect whether `pm` supports streaming install,
332
+ // so we detect `cmd` command support instead.
333
+ if (!this.#cmd.supportsCmd) {
334
+ await this.pushAndInstallStream(stream, options);
335
+ return;
265
336
  }
266
337
 
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`
338
+ const args = this.#buildInstallArguments(options);
339
+ // Remove `pm` from args, final command will starts with `cmd package install`
273
340
  args.shift();
274
341
  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());
342
+ const process = await this.#cmd.spawn(false, "package", ...args);
343
+
344
+ const output = process.stdout
345
+ .pipeThrough(new DecodeUtf8Stream())
346
+ .pipeThrough(new ConcatStringStream())
347
+ .then((output) => output.trim());
348
+
349
+ await Promise.all([
350
+ stream.pipeTo(process.stdin),
351
+ output.then((output) => {
352
+ if (output !== "Success") {
353
+ throw new Error(output);
354
+ }
355
+ }),
356
+ ]);
278
357
  }
279
358
 
280
359
  // TODO: install: support split apk formats (`adb install-multiple`)
360
+
361
+ static parsePackageListItem(
362
+ line: string,
363
+ ): PackageManagerListPackagesResult {
364
+ line = line.substring("package:".length);
365
+
366
+ let packageName: string;
367
+ let sourceDir: string | undefined;
368
+ let versionCode: number | undefined;
369
+ let installer: string | undefined;
370
+ let uid: number | undefined;
371
+
372
+ // Parse backwards
373
+ let index = line.indexOf(" uid:");
374
+ if (index !== -1) {
375
+ uid = Number.parseInt(line.substring(index + " uid:".length), 10);
376
+ line = line.substring(0, index);
377
+ }
378
+
379
+ index = line.indexOf(" installer=");
380
+ if (index !== -1) {
381
+ installer = line.substring(index + " installer=".length);
382
+ line = line.substring(0, index);
383
+ }
384
+
385
+ index = line.indexOf(" versionCode:");
386
+ if (index !== -1) {
387
+ versionCode = Number.parseInt(
388
+ line.substring(index + " versionCode:".length),
389
+ 10,
390
+ );
391
+ line = line.substring(0, index);
392
+ }
393
+
394
+ // `sourceDir` may contain `=` so use `lastIndexOf`
395
+ index = line.lastIndexOf("=");
396
+ if (index !== -1) {
397
+ sourceDir = line.substring(0, index);
398
+ packageName = line.substring(index + "=".length);
399
+ } else {
400
+ packageName = line;
401
+ }
402
+
403
+ return {
404
+ packageName,
405
+ sourceDir,
406
+ versionCode,
407
+ installer,
408
+ uid,
409
+ };
410
+ }
411
+
412
+ async #cmdOrSubprocess(args: string[]) {
413
+ if (this.#cmd.supportsCmd) {
414
+ args.shift();
415
+ return await this.#cmd.spawn(false, "package", ...args);
416
+ }
417
+
418
+ return this.adb.subprocess.spawn(args);
419
+ }
420
+
421
+ async *listPackages(
422
+ options?: Partial<PackageManagerListPackagesOptions>,
423
+ ): AsyncGenerator<PackageManagerListPackagesResult, void, void> {
424
+ const args = buildArguments(
425
+ ["pm", "list", "packages"],
426
+ options,
427
+ PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP,
428
+ );
429
+ if (options?.filter) {
430
+ args.push(options.filter);
431
+ }
432
+
433
+ const process = await this.#cmdOrSubprocess(args);
434
+ const reader = process.stdout
435
+ .pipeThrough(new DecodeUtf8Stream())
436
+ .pipeThrough(new SplitStringStream("\n"))
437
+ .getReader();
438
+ while (true) {
439
+ const { done, value } = await reader.read();
440
+ if (done) {
441
+ break;
442
+ }
443
+ yield PackageManager.parsePackageListItem(value);
444
+ }
445
+ }
446
+
447
+ async uninstall(
448
+ packageName: string,
449
+ options?: Partial<PackageManagerUninstallOptions>,
450
+ ): Promise<void> {
451
+ const args = buildArguments(
452
+ ["pm", "uninstall"],
453
+ options,
454
+ PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP,
455
+ );
456
+ args.push(packageName);
457
+ if (options?.splitNames) {
458
+ args.push(...options.splitNames);
459
+ }
460
+
461
+ const process = await this.#cmdOrSubprocess(args);
462
+ const output = await process.stdout
463
+ .pipeThrough(new DecodeUtf8Stream())
464
+ .pipeThrough(new ConcatStringStream())
465
+ .then((output) => output.trim());
466
+ if (output !== "Success") {
467
+ throw new Error(output);
468
+ }
469
+ }
470
+
471
+ async resolveActivity(
472
+ options: PackageManagerResolveActivityOptions,
473
+ ): Promise<string | undefined> {
474
+ let args = buildArguments(
475
+ ["pm", "resolve-activity", "--components"],
476
+ options,
477
+ PACKAGE_MANAGER_RESOLVE_ACTIVITY_OPTIONS_MAP,
478
+ );
479
+
480
+ args = args.concat(options.intent.build());
481
+
482
+ const process = await this.#cmdOrSubprocess(args);
483
+ const output = await process.stdout
484
+ .pipeThrough(new DecodeUtf8Stream())
485
+ .pipeThrough(new ConcatStringStream())
486
+ .then((output) => output.trim());
487
+
488
+ if (output === "No activity found") {
489
+ return undefined;
490
+ }
491
+
492
+ return output;
493
+ }
281
494
  }