@react-native-harness/platform-android 1.5.0 → 1.6.0
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/dist/app-monitor.d.ts +13 -0
- package/dist/app-monitor.d.ts.map +1 -0
- package/dist/app-monitor.js +376 -0
- package/dist/config.d.ts +10 -10
- package/dist/device-state.d.ts +6 -0
- package/dist/device-state.d.ts.map +1 -0
- package/dist/device-state.js +142 -0
- package/package.json +4 -4
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AppMonitor, type AppCrashDetails, type CrashArtifactWriter, type CrashDetailsLookupOptions, type AppMonitorEvent } from '@react-native-harness/platforms';
|
|
2
|
+
declare const createAndroidLogEvent: (line: string, bundleId: string) => AppMonitorEvent | null;
|
|
3
|
+
export declare const createAndroidAppMonitor: ({ adbId, bundleId, appUid, crashArtifactWriter, }: {
|
|
4
|
+
adbId: string;
|
|
5
|
+
bundleId: string;
|
|
6
|
+
appUid: number;
|
|
7
|
+
crashArtifactWriter?: CrashArtifactWriter;
|
|
8
|
+
}) => AndroidAppMonitor;
|
|
9
|
+
export { createAndroidLogEvent };
|
|
10
|
+
export type AndroidAppMonitor = AppMonitor & {
|
|
11
|
+
getCrashDetails: (options: CrashDetailsLookupOptions) => Promise<AppCrashDetails | null>;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=app-monitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-monitor.d.ts","sourceRoot":"","sources":["../src/app-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,eAAe,EAErB,MAAM,iCAAiC,CAAC;AA+SzC,QAAA,MAAM,qBAAqB,GACzB,MAAM,MAAM,EACZ,UAAU,MAAM,KACf,eAAe,GAAG,IAwEpB,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,mDAKrC;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CAC3C,KAAG,iBA4KH,CAAC;AAEF,OAAO,EAAE,qBAAqB,EAAE,CAAC;AACjC,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG;IAC3C,eAAe,EAAE,CACf,OAAO,EAAE,yBAAyB,KAC/B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;CACtC,CAAC"}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { escapeRegExp, getEmitter, logger, SubprocessError, } from '@react-native-harness/tools';
|
|
2
|
+
import * as adb from './adb.js';
|
|
3
|
+
import { androidCrashParser } from './crash-parser.js';
|
|
4
|
+
const androidAppMonitorLogger = logger.child('android-app-monitor');
|
|
5
|
+
const getLogcatArgs = (uid, fromTime) => [
|
|
6
|
+
'logcat',
|
|
7
|
+
'-v',
|
|
8
|
+
'threadtime',
|
|
9
|
+
'-b',
|
|
10
|
+
'crash',
|
|
11
|
+
`--uid=${uid}`,
|
|
12
|
+
'-T',
|
|
13
|
+
fromTime,
|
|
14
|
+
];
|
|
15
|
+
const MAX_RECENT_LOG_LINES = 200;
|
|
16
|
+
const MAX_RECENT_CRASH_ARTIFACTS = 10;
|
|
17
|
+
const CRASH_ARTIFACT_SETTLE_DELAY_MS = 100;
|
|
18
|
+
const startProcPattern = (bundleId) => new RegExp(`Start proc (\\d+):${escapeRegExp(bundleId)}(?:/|\\s)`);
|
|
19
|
+
const processPattern = (bundleId) => new RegExp(`Process:\\s*${escapeRegExp(bundleId)},\\s*PID:\\s*(\\d+)`);
|
|
20
|
+
const nativeCrashPattern = (bundleId) => new RegExp(`>>>\\s*${escapeRegExp(bundleId)}\\s*<<<`);
|
|
21
|
+
const processDiedPattern = (bundleId) => new RegExp(`Process\\s+${escapeRegExp(bundleId)}\\s+\\(pid\\s+(\\d+)\\)\\s+has\\s+died`, 'i');
|
|
22
|
+
const getSignal = (line) => {
|
|
23
|
+
const namedSignalMatch = line.match(/\b(SIG[A-Z0-9]+)\b/);
|
|
24
|
+
if (namedSignalMatch) {
|
|
25
|
+
return namedSignalMatch[1];
|
|
26
|
+
}
|
|
27
|
+
const signalNumberMatch = line.match(/signal\s+(\d+)/i);
|
|
28
|
+
if (signalNumberMatch) {
|
|
29
|
+
return `signal ${signalNumberMatch[1]}`;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
};
|
|
33
|
+
const getAndroidLogLineCrashDetails = ({ line, bundleId, pid, }) => {
|
|
34
|
+
const fatalExceptionMatch = line.match(/FATAL EXCEPTION:\s*(.+)$/i);
|
|
35
|
+
const processMatch = line.match(processPattern(bundleId));
|
|
36
|
+
return {
|
|
37
|
+
source: 'logs',
|
|
38
|
+
summary: line.trim(),
|
|
39
|
+
signal: getSignal(line),
|
|
40
|
+
exceptionType: fatalExceptionMatch?.[1]?.trim(),
|
|
41
|
+
processName: processMatch
|
|
42
|
+
? bundleId
|
|
43
|
+
: line.includes(bundleId)
|
|
44
|
+
? bundleId
|
|
45
|
+
: undefined,
|
|
46
|
+
pid: pid ?? (processMatch ? Number(processMatch[1]) : undefined),
|
|
47
|
+
rawLines: [line],
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const CRASH_BLOCK_HEADER = '--------- beginning of crash';
|
|
51
|
+
const getLatestCrashBlock = (recentLogLines) => {
|
|
52
|
+
const lines = recentLogLines.map(({ line }) => line);
|
|
53
|
+
let latestCrashHeaderIndex = -1;
|
|
54
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
55
|
+
if (/FATAL EXCEPTION:|Process:\s+.+,\s+PID:/i.test(lines[index])) {
|
|
56
|
+
latestCrashHeaderIndex = index;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const blockStartIndex = Math.max(lines.lastIndexOf(CRASH_BLOCK_HEADER), latestCrashHeaderIndex);
|
|
61
|
+
if (blockStartIndex === -1) {
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
64
|
+
return lines.slice(blockStartIndex);
|
|
65
|
+
};
|
|
66
|
+
const getCrashBlockForArtifact = ({ artifact, recentLogLines, }) => {
|
|
67
|
+
const targetIndex = recentLogLines.findIndex(({ line, occurredAt }) => line === artifact.triggerLine &&
|
|
68
|
+
(artifact.triggerOccurredAt === undefined ||
|
|
69
|
+
occurredAt === artifact.triggerOccurredAt));
|
|
70
|
+
if (targetIndex === -1) {
|
|
71
|
+
return artifact.rawLines ?? [];
|
|
72
|
+
}
|
|
73
|
+
let blockStartIndex = targetIndex;
|
|
74
|
+
for (let index = targetIndex; index >= 0; index -= 1) {
|
|
75
|
+
const { line } = recentLogLines[index];
|
|
76
|
+
if (line === CRASH_BLOCK_HEADER) {
|
|
77
|
+
blockStartIndex = index;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let blockEndIndex = recentLogLines.length;
|
|
82
|
+
for (let index = targetIndex + 1; index < recentLogLines.length; index += 1) {
|
|
83
|
+
if (recentLogLines[index].line === CRASH_BLOCK_HEADER) {
|
|
84
|
+
blockEndIndex = index;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return recentLogLines
|
|
89
|
+
.slice(blockStartIndex, blockEndIndex)
|
|
90
|
+
.map(({ line }) => line);
|
|
91
|
+
};
|
|
92
|
+
const hydrateCrashArtifact = ({ artifact, recentLogLines, }) => {
|
|
93
|
+
const rawLines = getCrashBlockForArtifact({ artifact, recentLogLines });
|
|
94
|
+
if (rawLines.length === 0) {
|
|
95
|
+
return artifact;
|
|
96
|
+
}
|
|
97
|
+
const parsedDetails = androidCrashParser.parse({
|
|
98
|
+
contents: rawLines.join('\n'),
|
|
99
|
+
bundleId: artifact.processName ?? '',
|
|
100
|
+
pid: artifact.pid,
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
...artifact,
|
|
104
|
+
...parsedDetails,
|
|
105
|
+
artifactType: artifact.artifactType,
|
|
106
|
+
artifactPath: artifact.artifactPath,
|
|
107
|
+
rawLines,
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
const createCrashArtifact = ({ details, recentLogLines, }) => {
|
|
111
|
+
const occurredAt = Date.now();
|
|
112
|
+
const rawLines = getLatestCrashBlock(recentLogLines);
|
|
113
|
+
const triggerOccurredAt = [...recentLogLines]
|
|
114
|
+
.reverse()
|
|
115
|
+
.find(({ line }) => line === details.summary)?.occurredAt;
|
|
116
|
+
const contents = rawLines.length > 0
|
|
117
|
+
? rawLines.join('\n')
|
|
118
|
+
: (details.rawLines ?? []).join('\n');
|
|
119
|
+
const parsedDetails = details.processName !== undefined
|
|
120
|
+
? androidCrashParser.parse({
|
|
121
|
+
contents,
|
|
122
|
+
bundleId: details.processName,
|
|
123
|
+
pid: details.pid,
|
|
124
|
+
})
|
|
125
|
+
: details;
|
|
126
|
+
return {
|
|
127
|
+
...parsedDetails,
|
|
128
|
+
occurredAt,
|
|
129
|
+
triggerLine: details.summary ?? '',
|
|
130
|
+
triggerOccurredAt,
|
|
131
|
+
artifactType: 'logcat',
|
|
132
|
+
rawLines: rawLines.length > 0
|
|
133
|
+
? rawLines
|
|
134
|
+
: parsedDetails.rawLines ?? details.rawLines,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
const persistCrashArtifact = ({ details, crashArtifactWriter, }) => {
|
|
138
|
+
if (!crashArtifactWriter || details.artifactType !== 'logcat') {
|
|
139
|
+
return details;
|
|
140
|
+
}
|
|
141
|
+
const artifactBody = details.rawLines?.join('\n');
|
|
142
|
+
if (!artifactBody) {
|
|
143
|
+
return details;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
...details,
|
|
147
|
+
artifactPath: crashArtifactWriter.persistArtifact({
|
|
148
|
+
artifactKind: details.artifactType,
|
|
149
|
+
source: {
|
|
150
|
+
kind: 'text',
|
|
151
|
+
fileName: 'logcat.txt',
|
|
152
|
+
text: `${artifactBody}\n`,
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
const getLatestCrashArtifact = ({ crashArtifacts, recentLogLines, processName, pid, occurredAt, }) => {
|
|
158
|
+
const matchingByPid = pid
|
|
159
|
+
? crashArtifacts.filter((artifact) => artifact.pid === pid)
|
|
160
|
+
: [];
|
|
161
|
+
const matchingByProcess = processName
|
|
162
|
+
? crashArtifacts.filter((artifact) => artifact.processName === processName)
|
|
163
|
+
: [];
|
|
164
|
+
const candidates = matchingByPid.length > 0
|
|
165
|
+
? matchingByPid
|
|
166
|
+
: matchingByProcess.length > 0
|
|
167
|
+
? matchingByProcess
|
|
168
|
+
: crashArtifacts;
|
|
169
|
+
const sortedCandidates = [...candidates].sort((left, right) => Math.abs(left.occurredAt - occurredAt) -
|
|
170
|
+
Math.abs(right.occurredAt - occurredAt));
|
|
171
|
+
const artifact = sortedCandidates[0];
|
|
172
|
+
if (!artifact) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return hydrateCrashArtifact({
|
|
176
|
+
artifact,
|
|
177
|
+
recentLogLines,
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
const createAndroidLogEvent = (line, bundleId) => {
|
|
181
|
+
const startMatch = line.match(startProcPattern(bundleId));
|
|
182
|
+
if (startMatch) {
|
|
183
|
+
return {
|
|
184
|
+
type: 'app_started',
|
|
185
|
+
pid: Number(startMatch[1]),
|
|
186
|
+
source: 'logs',
|
|
187
|
+
line,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const processMatch = line.match(processPattern(bundleId));
|
|
191
|
+
if (processMatch) {
|
|
192
|
+
return {
|
|
193
|
+
type: 'possible_crash',
|
|
194
|
+
pid: Number(processMatch[1]),
|
|
195
|
+
source: 'logs',
|
|
196
|
+
line,
|
|
197
|
+
crashDetails: getAndroidLogLineCrashDetails({
|
|
198
|
+
line,
|
|
199
|
+
bundleId,
|
|
200
|
+
pid: Number(processMatch[1]),
|
|
201
|
+
}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (nativeCrashPattern(bundleId).test(line)) {
|
|
205
|
+
return {
|
|
206
|
+
type: 'possible_crash',
|
|
207
|
+
source: 'logs',
|
|
208
|
+
line,
|
|
209
|
+
crashDetails: getAndroidLogLineCrashDetails({
|
|
210
|
+
line,
|
|
211
|
+
bundleId,
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const diedMatch = line.match(processDiedPattern(bundleId));
|
|
216
|
+
if (diedMatch) {
|
|
217
|
+
return {
|
|
218
|
+
type: 'app_exited',
|
|
219
|
+
pid: Number(diedMatch[1]),
|
|
220
|
+
source: 'logs',
|
|
221
|
+
line,
|
|
222
|
+
crashDetails: getAndroidLogLineCrashDetails({
|
|
223
|
+
line,
|
|
224
|
+
bundleId,
|
|
225
|
+
pid: Number(diedMatch[1]),
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (line.includes(bundleId) &&
|
|
230
|
+
/fatal|crash|signal 11|signal 6|backtrace/i.test(line)) {
|
|
231
|
+
return {
|
|
232
|
+
type: 'possible_crash',
|
|
233
|
+
source: 'logs',
|
|
234
|
+
line,
|
|
235
|
+
crashDetails: getAndroidLogLineCrashDetails({
|
|
236
|
+
line,
|
|
237
|
+
bundleId,
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
};
|
|
243
|
+
export const createAndroidAppMonitor = ({ adbId, bundleId, appUid, crashArtifactWriter, }) => {
|
|
244
|
+
const emitter = getEmitter();
|
|
245
|
+
let isStarted = false;
|
|
246
|
+
let logcatProcess = null;
|
|
247
|
+
let logTask = null;
|
|
248
|
+
let recentLogLines = [];
|
|
249
|
+
let recentCrashArtifacts = [];
|
|
250
|
+
const emit = (event) => {
|
|
251
|
+
emitter.emit(event);
|
|
252
|
+
};
|
|
253
|
+
const recordLogLine = (line) => {
|
|
254
|
+
recentLogLines = [
|
|
255
|
+
...recentLogLines,
|
|
256
|
+
{ line, occurredAt: Date.now() },
|
|
257
|
+
].slice(-MAX_RECENT_LOG_LINES);
|
|
258
|
+
};
|
|
259
|
+
const recordCrashArtifact = (details) => {
|
|
260
|
+
if (!details) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
recentCrashArtifacts = [
|
|
264
|
+
...recentCrashArtifacts,
|
|
265
|
+
createCrashArtifact({
|
|
266
|
+
details,
|
|
267
|
+
recentLogLines,
|
|
268
|
+
}),
|
|
269
|
+
].slice(-MAX_RECENT_CRASH_ARTIFACTS);
|
|
270
|
+
};
|
|
271
|
+
const stopProcess = async (child) => {
|
|
272
|
+
if (!child) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
(await child.nodeChildProcess).kill();
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Ignore termination failures for background monitors.
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const startLogcat = async () => {
|
|
283
|
+
const logcatTimestamp = await adb.getLogcatTimestamp(adbId);
|
|
284
|
+
logcatProcess = adb.startLogcat(adbId, getLogcatArgs(appUid, logcatTimestamp));
|
|
285
|
+
const currentProcess = logcatProcess;
|
|
286
|
+
if (!currentProcess) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
logTask = (async () => {
|
|
290
|
+
try {
|
|
291
|
+
for await (const line of currentProcess) {
|
|
292
|
+
recordLogLine(line);
|
|
293
|
+
emit({ type: 'log', source: 'logs', line });
|
|
294
|
+
const event = createAndroidLogEvent(line, bundleId);
|
|
295
|
+
if (event) {
|
|
296
|
+
if (event.type === 'possible_crash' ||
|
|
297
|
+
event.type === 'app_exited') {
|
|
298
|
+
recordCrashArtifact(event.crashDetails);
|
|
299
|
+
}
|
|
300
|
+
emit(event);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (!(error instanceof SubprocessError && error.signalName === 'SIGTERM')) {
|
|
306
|
+
androidAppMonitorLogger.debug('Android logcat monitor stopped', error);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
})();
|
|
310
|
+
};
|
|
311
|
+
const start = async () => {
|
|
312
|
+
if (isStarted) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
await startLogcat();
|
|
317
|
+
isStarted = true;
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
const currentProcess = logcatProcess;
|
|
321
|
+
const currentTask = logTask;
|
|
322
|
+
logcatProcess = null;
|
|
323
|
+
logTask = null;
|
|
324
|
+
await stopProcess(currentProcess);
|
|
325
|
+
await currentTask;
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const stop = async () => {
|
|
330
|
+
if (!isStarted) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
isStarted = false;
|
|
334
|
+
const currentProcess = logcatProcess;
|
|
335
|
+
const currentTask = logTask;
|
|
336
|
+
logcatProcess = null;
|
|
337
|
+
logTask = null;
|
|
338
|
+
await stopProcess(currentProcess);
|
|
339
|
+
await currentTask;
|
|
340
|
+
};
|
|
341
|
+
const dispose = async () => {
|
|
342
|
+
await stop();
|
|
343
|
+
emitter.clearAllListeners();
|
|
344
|
+
recentLogLines = [];
|
|
345
|
+
recentCrashArtifacts = [];
|
|
346
|
+
};
|
|
347
|
+
const addListener = (listener) => {
|
|
348
|
+
emitter.addListener(listener);
|
|
349
|
+
};
|
|
350
|
+
const removeListener = (listener) => {
|
|
351
|
+
emitter.removeListener(listener);
|
|
352
|
+
};
|
|
353
|
+
return {
|
|
354
|
+
start,
|
|
355
|
+
stop,
|
|
356
|
+
dispose,
|
|
357
|
+
addListener,
|
|
358
|
+
removeListener,
|
|
359
|
+
getCrashDetails: async (options) => {
|
|
360
|
+
await new Promise((resolve) => setTimeout(resolve, CRASH_ARTIFACT_SETTLE_DELAY_MS));
|
|
361
|
+
const details = getLatestCrashArtifact({
|
|
362
|
+
crashArtifacts: recentCrashArtifacts,
|
|
363
|
+
recentLogLines,
|
|
364
|
+
...options,
|
|
365
|
+
});
|
|
366
|
+
if (!details) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
return persistCrashArtifact({
|
|
370
|
+
details,
|
|
371
|
+
crashArtifactWriter,
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
};
|
|
376
|
+
export { createAndroidLogEvent };
|
package/dist/config.d.ts
CHANGED
|
@@ -68,8 +68,8 @@ export declare const AndroidEmulatorSchema: z.ZodObject<{
|
|
|
68
68
|
} | undefined;
|
|
69
69
|
}>>;
|
|
70
70
|
}, "strip", z.ZodTypeAny, {
|
|
71
|
-
type: "emulator";
|
|
72
71
|
name: string;
|
|
72
|
+
type: "emulator";
|
|
73
73
|
avd?: {
|
|
74
74
|
apiLevel: number;
|
|
75
75
|
profile: string;
|
|
@@ -80,8 +80,8 @@ export declare const AndroidEmulatorSchema: z.ZodObject<{
|
|
|
80
80
|
} | undefined;
|
|
81
81
|
} | undefined;
|
|
82
82
|
}, {
|
|
83
|
-
type: "emulator";
|
|
84
83
|
name: string;
|
|
84
|
+
type: "emulator";
|
|
85
85
|
avd?: {
|
|
86
86
|
apiLevel: number;
|
|
87
87
|
profile: string;
|
|
@@ -138,8 +138,8 @@ export declare const AndroidDeviceSchema: z.ZodDiscriminatedUnion<"type", [z.Zod
|
|
|
138
138
|
} | undefined;
|
|
139
139
|
}>>;
|
|
140
140
|
}, "strip", z.ZodTypeAny, {
|
|
141
|
-
type: "emulator";
|
|
142
141
|
name: string;
|
|
142
|
+
type: "emulator";
|
|
143
143
|
avd?: {
|
|
144
144
|
apiLevel: number;
|
|
145
145
|
profile: string;
|
|
@@ -150,8 +150,8 @@ export declare const AndroidDeviceSchema: z.ZodDiscriminatedUnion<"type", [z.Zod
|
|
|
150
150
|
} | undefined;
|
|
151
151
|
} | undefined;
|
|
152
152
|
}, {
|
|
153
|
-
type: "emulator";
|
|
154
153
|
name: string;
|
|
154
|
+
type: "emulator";
|
|
155
155
|
avd?: {
|
|
156
156
|
apiLevel: number;
|
|
157
157
|
profile: string;
|
|
@@ -209,8 +209,8 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
|
|
|
209
209
|
} | undefined;
|
|
210
210
|
}>>;
|
|
211
211
|
}, "strip", z.ZodTypeAny, {
|
|
212
|
-
type: "emulator";
|
|
213
212
|
name: string;
|
|
213
|
+
type: "emulator";
|
|
214
214
|
avd?: {
|
|
215
215
|
apiLevel: number;
|
|
216
216
|
profile: string;
|
|
@@ -221,8 +221,8 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
|
|
|
221
221
|
} | undefined;
|
|
222
222
|
} | undefined;
|
|
223
223
|
}, {
|
|
224
|
-
type: "emulator";
|
|
225
224
|
name: string;
|
|
225
|
+
type: "emulator";
|
|
226
226
|
avd?: {
|
|
227
227
|
apiLevel: number;
|
|
228
228
|
profile: string;
|
|
@@ -255,10 +255,11 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
|
|
|
255
255
|
extras?: Record<string, string | number | boolean> | undefined;
|
|
256
256
|
}>>;
|
|
257
257
|
}, "strip", z.ZodTypeAny, {
|
|
258
|
+
bundleId: string;
|
|
258
259
|
name: string;
|
|
259
260
|
device: {
|
|
260
|
-
type: "emulator";
|
|
261
261
|
name: string;
|
|
262
|
+
type: "emulator";
|
|
262
263
|
avd?: {
|
|
263
264
|
apiLevel: number;
|
|
264
265
|
profile: string;
|
|
@@ -273,16 +274,16 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
|
|
|
273
274
|
manufacturer: string;
|
|
274
275
|
model: string;
|
|
275
276
|
};
|
|
276
|
-
bundleId: string;
|
|
277
277
|
activityName: string;
|
|
278
278
|
appLaunchOptions?: {
|
|
279
279
|
extras?: Record<string, string | number | boolean> | undefined;
|
|
280
280
|
} | undefined;
|
|
281
281
|
}, {
|
|
282
|
+
bundleId: string;
|
|
282
283
|
name: string;
|
|
283
284
|
device: {
|
|
284
|
-
type: "emulator";
|
|
285
285
|
name: string;
|
|
286
|
+
type: "emulator";
|
|
286
287
|
avd?: {
|
|
287
288
|
apiLevel: number;
|
|
288
289
|
profile: string;
|
|
@@ -297,7 +298,6 @@ export declare const AndroidPlatformConfigSchema: z.ZodObject<{
|
|
|
297
298
|
manufacturer: string;
|
|
298
299
|
model: string;
|
|
299
300
|
};
|
|
300
|
-
bundleId: string;
|
|
301
301
|
activityName?: string | undefined;
|
|
302
302
|
appLaunchOptions?: {
|
|
303
303
|
extras?: Record<string, string | number | boolean> | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-state.d.ts","sourceRoot":"","sources":["../src/device-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,qBAAqB,EAEtB,MAAM,iCAAiC,CAAC;AA0HzC,eAAO,MAAM,wBAAwB,GAAI,SAAS;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,KAAG,qBAqFH,CAAC"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as adb from './adb.js';
|
|
2
|
+
const ANDROID_PERMISSION_ALIASES = {
|
|
3
|
+
bluetooth: [
|
|
4
|
+
'android.permission.BLUETOOTH_CONNECT',
|
|
5
|
+
'android.permission.BLUETOOTH_SCAN',
|
|
6
|
+
],
|
|
7
|
+
calendar: [
|
|
8
|
+
'android.permission.READ_CALENDAR',
|
|
9
|
+
'android.permission.WRITE_CALENDAR',
|
|
10
|
+
],
|
|
11
|
+
camera: ['android.permission.CAMERA'],
|
|
12
|
+
contacts: [
|
|
13
|
+
'android.permission.READ_CONTACTS',
|
|
14
|
+
'android.permission.WRITE_CONTACTS',
|
|
15
|
+
],
|
|
16
|
+
location: [
|
|
17
|
+
'android.permission.ACCESS_FINE_LOCATION',
|
|
18
|
+
'android.permission.ACCESS_COARSE_LOCATION',
|
|
19
|
+
],
|
|
20
|
+
mediaLibrary: [
|
|
21
|
+
'android.permission.READ_MEDIA_IMAGES',
|
|
22
|
+
'android.permission.READ_MEDIA_VIDEO',
|
|
23
|
+
'android.permission.READ_MEDIA_AUDIO',
|
|
24
|
+
],
|
|
25
|
+
microphone: ['android.permission.RECORD_AUDIO'],
|
|
26
|
+
notifications: ['android.permission.POST_NOTIFICATIONS'],
|
|
27
|
+
phone: [
|
|
28
|
+
'android.permission.CALL_PHONE',
|
|
29
|
+
'android.permission.ANSWER_PHONE_CALLS',
|
|
30
|
+
],
|
|
31
|
+
sms: [
|
|
32
|
+
'android.permission.READ_SMS',
|
|
33
|
+
'android.permission.RECEIVE_SMS',
|
|
34
|
+
'android.permission.SEND_SMS',
|
|
35
|
+
],
|
|
36
|
+
storage: [
|
|
37
|
+
'android.permission.READ_EXTERNAL_STORAGE',
|
|
38
|
+
'android.permission.WRITE_EXTERNAL_STORAGE',
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
const normalizePermissionName = (name) => {
|
|
42
|
+
return name.trim();
|
|
43
|
+
};
|
|
44
|
+
const resolvePermissions = async (options) => {
|
|
45
|
+
if (options.command.kind === 'permission-all') {
|
|
46
|
+
return adb.getDeclaredDangerousPermissions(options.adbId, options.bundleId);
|
|
47
|
+
}
|
|
48
|
+
const normalized = normalizePermissionName(options.command.permission);
|
|
49
|
+
const aliases = ANDROID_PERMISSION_ALIASES[normalized];
|
|
50
|
+
if (aliases) {
|
|
51
|
+
return aliases;
|
|
52
|
+
}
|
|
53
|
+
return [normalized];
|
|
54
|
+
};
|
|
55
|
+
const snapshotPermissions = async (options) => {
|
|
56
|
+
const granted = await Promise.all(options.permissions.map(async (permission) => {
|
|
57
|
+
const isGranted = await adb.isPermissionGranted(options.adbId, options.bundleId, permission);
|
|
58
|
+
return isGranted ? permission : null;
|
|
59
|
+
}));
|
|
60
|
+
return granted.filter((permission) => permission != null);
|
|
61
|
+
};
|
|
62
|
+
const applyPermissionSnapshot = async (options) => {
|
|
63
|
+
const grantedSet = new Set(options.snapshot.previousGranted);
|
|
64
|
+
const toGrant = options.snapshot.permissions.filter((permission) => grantedSet.has(permission));
|
|
65
|
+
const toRevoke = options.snapshot.permissions.filter((permission) => !grantedSet.has(permission));
|
|
66
|
+
if (toGrant.length > 0) {
|
|
67
|
+
await adb.setPermissions(options.adbId, options.bundleId, toGrant, 'grant', {
|
|
68
|
+
bestEffort: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (toRevoke.length > 0) {
|
|
72
|
+
await adb.setPermissions(options.adbId, options.bundleId, toRevoke, 'reset', {
|
|
73
|
+
bestEffort: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
export const createAndroidDeviceState = (options) => {
|
|
78
|
+
const outstandingMutations = new Map();
|
|
79
|
+
let mutationCounter = 0;
|
|
80
|
+
const rememberMutation = (snapshot) => {
|
|
81
|
+
const id = `android-permissions-${++mutationCounter}`;
|
|
82
|
+
const mutation = {
|
|
83
|
+
id,
|
|
84
|
+
snapshot,
|
|
85
|
+
revert: async () => {
|
|
86
|
+
await controller.permissions.revert(id);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
outstandingMutations.set(id, mutation);
|
|
90
|
+
return mutation;
|
|
91
|
+
};
|
|
92
|
+
const controller = {
|
|
93
|
+
permissions: {
|
|
94
|
+
apply: async (command) => {
|
|
95
|
+
const permissions = await resolvePermissions({
|
|
96
|
+
adbId: options.adbId,
|
|
97
|
+
bundleId: options.bundleId,
|
|
98
|
+
command,
|
|
99
|
+
});
|
|
100
|
+
if (permissions.length === 0) {
|
|
101
|
+
return { mutation: null };
|
|
102
|
+
}
|
|
103
|
+
const previousGranted = await snapshotPermissions({
|
|
104
|
+
adbId: options.adbId,
|
|
105
|
+
bundleId: options.bundleId,
|
|
106
|
+
permissions,
|
|
107
|
+
});
|
|
108
|
+
await adb.setPermissions(options.adbId, options.bundleId, permissions, command.decision, { bestEffort: command.kind === 'permission-all' });
|
|
109
|
+
const mutation = rememberMutation({
|
|
110
|
+
kind: 'permission-set',
|
|
111
|
+
permissions,
|
|
112
|
+
previousGranted,
|
|
113
|
+
});
|
|
114
|
+
return { mutation };
|
|
115
|
+
},
|
|
116
|
+
revert: async (mutationId) => {
|
|
117
|
+
const mutation = outstandingMutations.get(mutationId);
|
|
118
|
+
if (!mutation) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
outstandingMutations.delete(mutationId);
|
|
122
|
+
await applyPermissionSnapshot({
|
|
123
|
+
adbId: options.adbId,
|
|
124
|
+
bundleId: options.bundleId,
|
|
125
|
+
snapshot: mutation.snapshot,
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
resetOutstanding: async () => {
|
|
129
|
+
const mutations = [...outstandingMutations.values()].reverse();
|
|
130
|
+
outstandingMutations.clear();
|
|
131
|
+
for (const mutation of mutations) {
|
|
132
|
+
await applyPermissionSnapshot({
|
|
133
|
+
adbId: options.adbId,
|
|
134
|
+
bundleId: options.bundleId,
|
|
135
|
+
snapshot: mutation.snapshot,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
return controller;
|
|
142
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-harness/platform-android",
|
|
3
3
|
"description": "Android platform for React Native Harness",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"vite": "^7.2.2",
|
|
20
20
|
"zod": "^3.25.67",
|
|
21
21
|
"tslib": "^2.3.0",
|
|
22
|
-
"@react-native-harness/
|
|
23
|
-
"@react-native-harness/
|
|
24
|
-
"@react-native-harness/config": "1.
|
|
22
|
+
"@react-native-harness/tools": "1.6.0",
|
|
23
|
+
"@react-native-harness/platforms": "1.6.0",
|
|
24
|
+
"@react-native-harness/config": "1.6.0"
|
|
25
25
|
},
|
|
26
26
|
"license": "MIT"
|
|
27
27
|
}
|