@yume-chan/android-bin 0.0.18 → 0.0.20
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 +21 -0
- package/CHANGELOG.md +14 -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/dumpsys.d.ts +4 -0
- package/esm/dumpsys.d.ts.map +1 -0
- package/esm/dumpsys.js +4 -0
- package/esm/dumpsys.js.map +1 -0
- 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 +296 -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 +40 -24
- package/esm/settings.js.map +1 -1
- package/package.json +11 -10
- package/src/bug-report.ts +3 -3
- package/src/cmd.ts +67 -0
- package/src/demo-mode.ts +4 -3
- package/src/index.ts +6 -4
- package/src/logcat.ts +224 -25
- package/src/overlay-display.ts +115 -0
- package/src/pm.ts +281 -0
- package/src/settings.ts +54 -26
- package/tsconfig.build.json +11 -0
- 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 { AsyncExactReadable } 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,242 @@ 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 padZero(number: number, length: number) {
|
|
120
|
+
return number.toString().padStart(length, "0");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatSeconds(seconds: number, modifiers: LogcatFormatModifiers) {
|
|
124
|
+
if (modifiers.monotonic) {
|
|
125
|
+
return padZero(seconds, 6);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (modifiers.epoch) {
|
|
129
|
+
return padZero(seconds, 19);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const date = new Date(seconds * 1000);
|
|
133
|
+
|
|
134
|
+
const month = padZero(date.getMonth() + 1, 2);
|
|
135
|
+
const day = padZero(date.getDate(), 2);
|
|
136
|
+
const hour = padZero(date.getHours(), 2);
|
|
137
|
+
const minute = padZero(date.getMinutes(), 2);
|
|
138
|
+
const second = padZero(date.getSeconds(), 2);
|
|
139
|
+
const result = `${month}-${day} ${hour}:${minute}:${second}`;
|
|
140
|
+
|
|
141
|
+
if (modifiers.year) {
|
|
142
|
+
const year = padZero(date.getFullYear(), 4);
|
|
143
|
+
return `${year}-${result}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function formatNanoseconds(
|
|
150
|
+
nanoseconds: number,
|
|
151
|
+
modifiers: LogcatFormatModifiers
|
|
122
152
|
) {
|
|
123
|
-
|
|
153
|
+
if (modifiers.nanoseconds) {
|
|
154
|
+
return padZero(nanoseconds, 9);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (modifiers.microseconds) {
|
|
158
|
+
return padZero(nanoseconds / 1000, 6);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return padZero(nanoseconds / 1000000, 3);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatTimezone(seconds: number, modifiers: LogcatFormatModifiers) {
|
|
165
|
+
if (!modifiers.timezone || modifiers.monotonic || modifiers.epoch) {
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const date = new Date(seconds * 1000);
|
|
170
|
+
const offset = date.getTimezoneOffset();
|
|
171
|
+
const sign = offset <= 0 ? "+" : "-";
|
|
172
|
+
const absolute = Math.abs(offset);
|
|
173
|
+
const hours = (absolute / 60) | 0;
|
|
174
|
+
const minutes = absolute % 60;
|
|
175
|
+
|
|
176
|
+
// prettier-ignore
|
|
177
|
+
return ` ${
|
|
178
|
+
sign
|
|
179
|
+
}${
|
|
180
|
+
hours.toString().padStart(2, "0")
|
|
181
|
+
}:${
|
|
182
|
+
minutes.toString().padStart(2, "0")
|
|
183
|
+
}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function formatTime(
|
|
187
|
+
seconds: number,
|
|
188
|
+
nanoseconds: number,
|
|
189
|
+
modifiers: LogcatFormatModifiers
|
|
190
|
+
) {
|
|
191
|
+
const secondsString = formatSeconds(seconds, modifiers);
|
|
192
|
+
const nanosecondsString = formatNanoseconds(nanoseconds, modifiers);
|
|
193
|
+
const zoneString = formatTimezone(seconds, modifiers);
|
|
194
|
+
return `${secondsString}.${nanosecondsString}${zoneString}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatUid(
|
|
198
|
+
uid: number,
|
|
199
|
+
modifiers: LogcatFormatModifiers,
|
|
200
|
+
suffix: string
|
|
201
|
+
) {
|
|
202
|
+
return modifiers.uid ? `${uid.toString().padStart(5)}${suffix}` : "";
|
|
203
|
+
}
|
|
124
204
|
|
|
205
|
+
function getFormatPrefix(
|
|
206
|
+
entry: AndroidLogEntry,
|
|
207
|
+
format: LogcatFormat,
|
|
208
|
+
modifiers: LogcatFormatModifiers
|
|
209
|
+
) {
|
|
210
|
+
// https://cs.android.com/android/platform/superproject/+/master:system/logging/liblog/logprint.cpp;l=1415;drc=8dbf3b2bb6b6d1652d9797e477b9abd03278bb79
|
|
125
211
|
switch (format) {
|
|
126
212
|
// TODO: implement other formats
|
|
213
|
+
case LogcatFormat.Tag:
|
|
214
|
+
// prettier-ignore
|
|
215
|
+
return `${
|
|
216
|
+
AndroidLogPriorityToCharacter[entry.priority]
|
|
217
|
+
}/${
|
|
218
|
+
entry.tag.padEnd(8)
|
|
219
|
+
}: `;
|
|
220
|
+
case LogcatFormat.Process:
|
|
221
|
+
// prettier-ignore
|
|
222
|
+
return `${
|
|
223
|
+
AndroidLogPriorityToCharacter[entry.priority]
|
|
224
|
+
}(${
|
|
225
|
+
formatUid(entry.uid, modifiers, ":")
|
|
226
|
+
}${
|
|
227
|
+
entry.pid.toString().padStart(5)
|
|
228
|
+
}) `;
|
|
229
|
+
case LogcatFormat.Thread:
|
|
230
|
+
// prettier-ignore
|
|
231
|
+
return `${
|
|
232
|
+
AndroidLogPriorityToCharacter[entry.priority]
|
|
233
|
+
}(${
|
|
234
|
+
formatUid(entry.uid, modifiers, ":")
|
|
235
|
+
}${
|
|
236
|
+
entry.pid.toString().padStart(5)
|
|
237
|
+
}:${
|
|
238
|
+
entry.tid.toString().padStart(5)
|
|
239
|
+
}) `;
|
|
240
|
+
case LogcatFormat.Raw:
|
|
241
|
+
return "";
|
|
242
|
+
case LogcatFormat.Time:
|
|
243
|
+
// prettier-ignore
|
|
244
|
+
return `${
|
|
245
|
+
formatTime(entry.seconds, entry.nanoseconds, modifiers)
|
|
246
|
+
} ${
|
|
247
|
+
AndroidLogPriorityToCharacter[entry.priority]
|
|
248
|
+
}/${
|
|
249
|
+
entry.tag.padEnd(8)
|
|
250
|
+
}(${
|
|
251
|
+
formatUid(entry.uid, modifiers, ":")
|
|
252
|
+
}${
|
|
253
|
+
entry.pid.toString().padStart(5)
|
|
254
|
+
}): `;
|
|
255
|
+
case LogcatFormat.ThreadTime:
|
|
256
|
+
// prettier-ignore
|
|
257
|
+
return `${
|
|
258
|
+
formatTime(entry.seconds, entry.nanoseconds, modifiers)
|
|
259
|
+
} ${
|
|
260
|
+
formatUid(entry.uid, modifiers, " ")
|
|
261
|
+
}${
|
|
262
|
+
entry.pid.toString().padStart(5)
|
|
263
|
+
} ${
|
|
264
|
+
entry.tid.toString().padStart(5)
|
|
265
|
+
} ${
|
|
266
|
+
AndroidLogPriorityToCharacter[entry.priority]
|
|
267
|
+
} ${
|
|
268
|
+
entry.tag.toString().padEnd(8)
|
|
269
|
+
}: `;
|
|
270
|
+
case LogcatFormat.Brief:
|
|
127
271
|
default:
|
|
272
|
+
// prettier-ignore
|
|
128
273
|
return `${
|
|
129
274
|
AndroidLogPriorityToCharacter[entry.priority]
|
|
130
|
-
}/${
|
|
131
|
-
.
|
|
132
|
-
|
|
275
|
+
}/${
|
|
276
|
+
entry.tag.padEnd(8)
|
|
277
|
+
}(${
|
|
278
|
+
formatUid(entry.uid, modifiers, ":")
|
|
279
|
+
}${
|
|
280
|
+
entry.pid.toString().padStart(5)
|
|
281
|
+
}): `;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function getFormatSuffix(entry: AndroidLogEntry, format: LogcatFormat) {
|
|
286
|
+
switch (format) {
|
|
287
|
+
case LogcatFormat.Process:
|
|
288
|
+
return ` (${entry.tag})`;
|
|
289
|
+
default:
|
|
290
|
+
return "";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function formatEntryWrapLine(
|
|
295
|
+
entry: AndroidLogEntry,
|
|
296
|
+
format: LogcatFormat,
|
|
297
|
+
modifiers: LogcatFormatModifiers
|
|
298
|
+
) {
|
|
299
|
+
const prefix = getFormatPrefix(entry, format, modifiers);
|
|
300
|
+
const suffix = getFormatSuffix(entry, format);
|
|
301
|
+
return (
|
|
302
|
+
prefix + entry.message.replaceAll("\n", suffix + "\n" + prefix) + suffix
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function AndroidLogEntryToString(
|
|
307
|
+
this: AndroidLogEntry,
|
|
308
|
+
format: LogcatFormat = LogcatFormat.ThreadTime,
|
|
309
|
+
modifiers: LogcatFormatModifiers = {}
|
|
310
|
+
) {
|
|
311
|
+
switch (format) {
|
|
312
|
+
case LogcatFormat.Long:
|
|
313
|
+
// prettier-ignore
|
|
314
|
+
return `[ ${
|
|
315
|
+
formatTime(this.seconds, this.nanoseconds, modifiers)
|
|
316
|
+
} ${
|
|
317
|
+
formatUid(this.uid, modifiers, ":")
|
|
318
|
+
}${
|
|
319
|
+
this.pid.toString().padStart(5)
|
|
320
|
+
}:${
|
|
321
|
+
this.tid.toString().padStart(5)
|
|
322
|
+
} ${
|
|
323
|
+
AndroidLogPriorityToCharacter[this.priority]
|
|
324
|
+
}/${
|
|
325
|
+
this.tag.padEnd(8)
|
|
326
|
+
} ]\n${
|
|
327
|
+
this.message
|
|
328
|
+
}\n`;
|
|
329
|
+
default:
|
|
330
|
+
return formatEntryWrapLine(this, format, modifiers);
|
|
133
331
|
}
|
|
134
332
|
}
|
|
135
333
|
|
|
@@ -150,15 +348,14 @@ function findTagEnd(payload: Uint8Array) {
|
|
|
150
348
|
}
|
|
151
349
|
|
|
152
350
|
export async function deserializeAndroidLogEntry(
|
|
153
|
-
stream:
|
|
351
|
+
stream: AsyncExactReadable
|
|
154
352
|
): Promise<AndroidLogEntry> {
|
|
155
|
-
const entry = (await LoggerEntry.deserialize(
|
|
156
|
-
stream
|
|
157
|
-
)) as unknown as AndroidLogEntry;
|
|
353
|
+
const entry = (await LoggerEntry.deserialize(stream)) as AndroidLogEntry;
|
|
158
354
|
if (entry.headerSize !== LoggerEntry.size) {
|
|
159
|
-
await stream.
|
|
355
|
+
await stream.readExactly(entry.headerSize - LoggerEntry.size);
|
|
160
356
|
}
|
|
161
|
-
|
|
357
|
+
|
|
358
|
+
let payload = await stream.readExactly(entry.payloadSize);
|
|
162
359
|
|
|
163
360
|
// https://cs.android.com/android/platform/superproject/+/master:system/logging/logcat/logcat.cpp;l=193-194;drc=bbe77d66e7bee8bd1f0bc7e5492b5376b0207ef6
|
|
164
361
|
// TODO: payload for some log IDs are in binary format.
|
|
@@ -171,6 +368,7 @@ export async function deserializeAndroidLogEntry(
|
|
|
171
368
|
tagEnd < payload.length - 1
|
|
172
369
|
? decodeUtf8(payload.subarray(tagEnd + 1))
|
|
173
370
|
: "";
|
|
371
|
+
entry.toString = AndroidLogEntryToString;
|
|
174
372
|
return entry;
|
|
175
373
|
}
|
|
176
374
|
|
|
@@ -245,6 +443,7 @@ export class Logcat extends AdbCommandBase {
|
|
|
245
443
|
maxEntrySize: parseInt(match[8]!, 10),
|
|
246
444
|
maxPayloadSize: parseInt(match[9]!, 10),
|
|
247
445
|
});
|
|
446
|
+
return;
|
|
248
447
|
}
|
|
249
448
|
|
|
250
449
|
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
|
+
}
|