@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.
@@ -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,6 @@
1
+ import type { DeviceStateController } from '@react-native-harness/platforms';
2
+ export declare const createAndroidDeviceState: (options: {
3
+ adbId: string;
4
+ bundleId: string;
5
+ }) => DeviceStateController;
6
+ //# sourceMappingURL=device-state.d.ts.map
@@ -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.5.0",
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/platforms": "1.5.0",
23
- "@react-native-harness/tools": "1.5.0",
24
- "@react-native-harness/config": "1.5.0"
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
  }