@mobileaidev/ai-app-bridge 0.2.5 → 0.2.7
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/README.md +84 -58
- package/bin/ai-app-bridge.js +548 -395
- package/bin/mcp-server.js +730 -661
- package/package.json +7 -6
- package/skills/ai-app-bridge-use/SKILL.md +183 -0
- package/skills/ai-app-bridge-use/agents/openai.yaml +4 -0
package/bin/ai-app-bridge.js
CHANGED
|
@@ -31,6 +31,8 @@ Commands:
|
|
|
31
31
|
network Read generic in-app network records.
|
|
32
32
|
state Read generic in-app state records.
|
|
33
33
|
events Read generic in-app event records.
|
|
34
|
+
freeze-app Stop the target app processes with SIGSTOP.
|
|
35
|
+
thaw-app Resume the target app processes with SIGCONT.
|
|
34
36
|
|
|
35
37
|
H5/WebView commands:
|
|
36
38
|
h5-dom Read native Android WebView DOM.
|
|
@@ -48,7 +50,7 @@ Flutter commands:
|
|
|
48
50
|
flutter-nodes Read Flutter operable nodes.
|
|
49
51
|
flutter-action Dispatch a raw Flutter action payload.
|
|
50
52
|
tap-flutter-text Tap a Flutter node by text.
|
|
51
|
-
input-flutter-text Set Flutter text through the Flutter action bridge.
|
|
53
|
+
input-flutter-text Set Flutter text through the Flutter action bridge.
|
|
52
54
|
scroll-flutter Scroll Flutter content.
|
|
53
55
|
flutter-h5-dom Read DOM through a Flutter H5 adapter.
|
|
54
56
|
flutter-h5-eval Execute JavaScript through a Flutter H5 adapter.
|
|
@@ -62,18 +64,19 @@ Device/action commands:
|
|
|
62
64
|
tap-text Tap a visible node by exact text or content description.
|
|
63
65
|
tap-uia-text Tap a UIAutomator node by text.
|
|
64
66
|
wait-text Wait until text appears in bridge or UIAutomator output.
|
|
65
|
-
input-text Set native Android text through the in-app bridge; ASCII can fall back to ADB.
|
|
67
|
+
input-text Set native Android text through the in-app bridge; ASCII can fall back to ADB.
|
|
66
68
|
swipe Swipe device coordinates through ADB.
|
|
67
69
|
keyevent Send an Android keyevent through ADB.
|
|
68
70
|
keyboard-state Read Android soft keyboard visibility from dumpsys.
|
|
69
71
|
hide-keyboard Hide the Android soft keyboard when it is visible.
|
|
70
72
|
|
|
71
|
-
App/permission commands:
|
|
72
|
-
install-apk Install an APK and assist device-side installer screens.
|
|
73
|
-
|
|
74
|
-
launch-
|
|
75
|
-
launch-
|
|
76
|
-
launch-
|
|
73
|
+
App/permission commands:
|
|
74
|
+
install-apk Install an APK and assist device-side installer screens.
|
|
75
|
+
clear-app-data Clear target app local data through the bridge runtime.
|
|
76
|
+
launch-app Launch the target package LAUNCHER Activity.
|
|
77
|
+
launch-activity Launch an explicit Android Activity component.
|
|
78
|
+
launch-native-test Launch the debug native Android bridge test Activity.
|
|
79
|
+
launch-flutter Launch the Flutter Activity.
|
|
77
80
|
permission-state Read Android runtime permission state.
|
|
78
81
|
permission-grant Grant an Android runtime permission.
|
|
79
82
|
permission-revoke Revoke an Android runtime permission.
|
|
@@ -94,14 +97,14 @@ Options:
|
|
|
94
97
|
--adb-timeout-ms <ms> Timeout for ADB subprocesses.
|
|
95
98
|
--out-file <path> Screenshot output path.
|
|
96
99
|
--artifact-dir <path> Directory for generated screenshot/artifact defaults.
|
|
97
|
-
--apk-path <path> APK path used by install-apk.
|
|
98
|
-
--activity <name> Activity class for launch-activity or launch-app override.
|
|
99
|
-
--component <pkg/act> Explicit Android component for launch-activity.
|
|
100
|
-
--action <name> Intent action for launch-activity.
|
|
101
|
-
--category <name> Intent category for launch-activity; may be repeated.
|
|
102
|
-
--data <uri> Intent data URI for launch-activity.
|
|
103
|
-
--extra <key=value> String intent extra for launch-activity; may be repeated.
|
|
104
|
-
--text <text> Text used by Unicode-safe bridge input commands.
|
|
100
|
+
--apk-path <path> APK path used by install-apk.
|
|
101
|
+
--activity <name> Activity class for launch-activity or launch-app override.
|
|
102
|
+
--component <pkg/act> Explicit Android component for launch-activity.
|
|
103
|
+
--action <name> Intent action for launch-activity.
|
|
104
|
+
--category <name> Intent category for launch-activity; may be repeated.
|
|
105
|
+
--data <uri> Intent data URI for launch-activity.
|
|
106
|
+
--extra <key=value> String intent extra for launch-activity; may be repeated.
|
|
107
|
+
--text <text> Text used by Unicode-safe bridge input commands.
|
|
105
108
|
--value <text> Text value used by h5-input or flutter-h5-input.
|
|
106
109
|
--selector <css> CSS selector used by H5 commands.
|
|
107
110
|
--target-text <text> Text used by tap-text or wait-text.
|
|
@@ -155,7 +158,7 @@ Options:
|
|
|
155
158
|
--permission <name> Android permission for permission-* commands.
|
|
156
159
|
--op <name> App-op name for appops-set.
|
|
157
160
|
--mode <mode> App-op mode for appops-set.
|
|
158
|
-
--pid <pid|current> PID filter for logcat.
|
|
161
|
+
--pid <pid|current> PID filter for logcat, or explicit process for freeze/thaw.
|
|
159
162
|
--app-pid Filter logcat by current app pid.
|
|
160
163
|
--tag <tag[,tag]> Logcat tag filter.
|
|
161
164
|
--level <level> Minimum logcat level.
|
|
@@ -229,19 +232,19 @@ async function runCommand(command, options, ctx) {
|
|
|
229
232
|
}
|
|
230
233
|
case 'flutter-nodes':
|
|
231
234
|
return flutterNodes(ctx);
|
|
232
|
-
case 'tap-flutter-text':
|
|
233
|
-
return flutterAction(ctx, { action: 'tapText', text: requiredString(options.targetText, 'targetText') });
|
|
234
|
-
case 'input-flutter-text': {
|
|
235
|
-
const result = await flutterAction(ctx, {
|
|
236
|
-
action: 'inputText',
|
|
237
|
-
text: requiredString(options.text, 'text'),
|
|
238
|
-
...(options.tapX !== undefined && options.tapY !== undefined ? { x: Number(options.tapX), y: Number(options.tapY) } : {}),
|
|
239
|
-
});
|
|
240
|
-
if (booleanOption(options.hideKeyboard)) {
|
|
241
|
-
result.keyboard = await hideKeyboard(ctx, options);
|
|
242
|
-
}
|
|
243
|
-
return result;
|
|
244
|
-
}
|
|
235
|
+
case 'tap-flutter-text':
|
|
236
|
+
return flutterAction(ctx, { action: 'tapText', text: requiredString(options.targetText, 'targetText') });
|
|
237
|
+
case 'input-flutter-text': {
|
|
238
|
+
const result = await flutterAction(ctx, {
|
|
239
|
+
action: 'inputText',
|
|
240
|
+
text: requiredString(options.text, 'text'),
|
|
241
|
+
...(options.tapX !== undefined && options.tapY !== undefined ? { x: Number(options.tapX), y: Number(options.tapY) } : {}),
|
|
242
|
+
});
|
|
243
|
+
if (booleanOption(options.hideKeyboard)) {
|
|
244
|
+
result.keyboard = await hideKeyboard(ctx, options);
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
245
248
|
case 'scroll-flutter':
|
|
246
249
|
return options.targetText
|
|
247
250
|
? flutterAction(ctx, { action: 'scrollUntilText', text: options.targetText, maxSwipes: Number(options.maxSwipes || 12) })
|
|
@@ -256,6 +259,10 @@ async function runCommand(command, options, ctx) {
|
|
|
256
259
|
return bridgeGet(ctx, withQuery('/v1/state', captureQuery(options)));
|
|
257
260
|
case 'events':
|
|
258
261
|
return bridgeGet(ctx, withQuery('/v1/events', captureQuery(options)));
|
|
262
|
+
case 'freeze-app':
|
|
263
|
+
return freezeApp(ctx, options);
|
|
264
|
+
case 'thaw-app':
|
|
265
|
+
return thawApp(ctx, options);
|
|
259
266
|
case 'h5-dom':
|
|
260
267
|
return bridgeGet(ctx, '/v1/h5/dom');
|
|
261
268
|
case 'h5-eval':
|
|
@@ -298,6 +305,8 @@ async function runCommand(command, options, ctx) {
|
|
|
298
305
|
return hideKeyboard(ctx, options);
|
|
299
306
|
case 'install-apk':
|
|
300
307
|
return installApk(ctx, options);
|
|
308
|
+
case 'clear-app-data':
|
|
309
|
+
return clearAppData(ctx);
|
|
301
310
|
case 'webview-pages':
|
|
302
311
|
return webviewPages(ctx, options);
|
|
303
312
|
case 'webview-network':
|
|
@@ -327,15 +336,15 @@ async function runCommand(command, options, ctx) {
|
|
|
327
336
|
return appopsSet(ctx, requiredString(options.op, 'op'), requiredString(options.mode, 'mode'));
|
|
328
337
|
case 'tap-uia-text':
|
|
329
338
|
return tapUiaText(ctx, requiredString(options.targetText, 'targetText'), options);
|
|
330
|
-
case 'permission-dialog':
|
|
331
|
-
return permissionDialog(ctx, options);
|
|
332
|
-
case 'launch-app':
|
|
333
|
-
return launchApp(ctx, options);
|
|
334
|
-
case 'launch-activity':
|
|
335
|
-
return launchActivity(ctx, options);
|
|
336
|
-
case 'launch-native-test':
|
|
337
|
-
return launchNativeTest(ctx);
|
|
338
|
-
case 'launch-flutter':
|
|
339
|
+
case 'permission-dialog':
|
|
340
|
+
return permissionDialog(ctx, options);
|
|
341
|
+
case 'launch-app':
|
|
342
|
+
return launchApp(ctx, options);
|
|
343
|
+
case 'launch-activity':
|
|
344
|
+
return launchActivity(ctx, options);
|
|
345
|
+
case 'launch-native-test':
|
|
346
|
+
return launchNativeTest(ctx);
|
|
347
|
+
case 'launch-flutter':
|
|
339
348
|
return launchFlutter(ctx, options.initialRoute || '');
|
|
340
349
|
case 'smoke':
|
|
341
350
|
return smoke(ctx, options);
|
|
@@ -359,25 +368,25 @@ function parseArgs(argv) {
|
|
|
359
368
|
const rawName = arg.slice(2);
|
|
360
369
|
const name = rawName.replace(/-([a-z])/g, (_, value) => value.toUpperCase());
|
|
361
370
|
const next = argv[index + 1];
|
|
362
|
-
if (next === undefined || next.startsWith('--')) {
|
|
363
|
-
appendOption(options, name, true);
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
appendOption(options, name, next);
|
|
367
|
-
index += 1;
|
|
368
|
-
}
|
|
369
|
-
return { command, options };
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function appendOption(options, name, value) {
|
|
373
|
-
if (name === 'extra' || name === 'category') {
|
|
374
|
-
if (!Array.isArray(options[name])) options[name] = [];
|
|
375
|
-
options[name].push(value);
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
options[name] = value;
|
|
379
|
-
}
|
|
380
|
-
|
|
371
|
+
if (next === undefined || next.startsWith('--')) {
|
|
372
|
+
appendOption(options, name, true);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
appendOption(options, name, next);
|
|
376
|
+
index += 1;
|
|
377
|
+
}
|
|
378
|
+
return { command, options };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function appendOption(options, name, value) {
|
|
382
|
+
if (name === 'extra' || name === 'category') {
|
|
383
|
+
if (!Array.isArray(options[name])) options[name] = [];
|
|
384
|
+
options[name].push(value);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
options[name] = value;
|
|
388
|
+
}
|
|
389
|
+
|
|
381
390
|
async function adb(ctx, args, { binary = false } = {}) {
|
|
382
391
|
const allArgs = adbArgs(ctx, args);
|
|
383
392
|
return new Promise((resolve, reject) => {
|
|
@@ -782,41 +791,41 @@ async function ensureForward(ctx) {
|
|
|
782
791
|
};
|
|
783
792
|
}
|
|
784
793
|
|
|
785
|
-
async function resolveDevicePort(ctx) {
|
|
786
|
-
if (ctx.explicitPort) return { port: ctx.port, source: 'explicit-port' };
|
|
787
|
-
try {
|
|
788
|
-
const result = await adb(ctx, ['shell', 'run-as', ctx.packageName, 'cat', 'files/ai_app_bridge_port.json']);
|
|
789
|
-
const state = JSON.parse(result.stdout.trim());
|
|
790
|
-
const discoveredPort = Number(state.port);
|
|
791
|
-
if (state.ok === true && Number.isInteger(discoveredPort) && discoveredPort > 0) {
|
|
792
|
-
return { port: discoveredPort, source: 'package-port-file', state };
|
|
793
|
-
}
|
|
794
|
-
} catch (error) {
|
|
795
|
-
if (!shouldUseDefaultPortFallback(ctx)) {
|
|
796
|
-
ctx.devicePortSource = 'package-port-file';
|
|
797
|
-
ctx.devicePortError = firstErrorLine(error);
|
|
798
|
-
error.aiAppBridgePortDiscovery = true;
|
|
799
|
-
throw error;
|
|
800
|
-
}
|
|
801
|
-
return {
|
|
802
|
-
port: ctx.port,
|
|
803
|
-
source: 'default-port',
|
|
804
|
-
error: firstErrorLine(error),
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
if (!shouldUseDefaultPortFallback(ctx)) {
|
|
808
|
-
const error = new Error(`bridge port discovery failed for explicit package: ${ctx.packageName}`);
|
|
809
|
-
error.aiAppBridgePortDiscovery = true;
|
|
810
|
-
ctx.devicePortSource = 'package-port-file';
|
|
811
|
-
ctx.devicePortError = firstErrorLine(error);
|
|
812
|
-
throw error;
|
|
813
|
-
}
|
|
814
|
-
return { port: ctx.port, source: 'default-port' };
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function shouldUseDefaultPortFallback(ctx) {
|
|
818
|
-
return !ctx.explicitPackageName;
|
|
819
|
-
}
|
|
794
|
+
async function resolveDevicePort(ctx) {
|
|
795
|
+
if (ctx.explicitPort) return { port: ctx.port, source: 'explicit-port' };
|
|
796
|
+
try {
|
|
797
|
+
const result = await adb(ctx, ['shell', 'run-as', ctx.packageName, 'cat', 'files/ai_app_bridge_port.json']);
|
|
798
|
+
const state = JSON.parse(result.stdout.trim());
|
|
799
|
+
const discoveredPort = Number(state.port);
|
|
800
|
+
if (state.ok === true && Number.isInteger(discoveredPort) && discoveredPort > 0) {
|
|
801
|
+
return { port: discoveredPort, source: 'package-port-file', state };
|
|
802
|
+
}
|
|
803
|
+
} catch (error) {
|
|
804
|
+
if (!shouldUseDefaultPortFallback(ctx)) {
|
|
805
|
+
ctx.devicePortSource = 'package-port-file';
|
|
806
|
+
ctx.devicePortError = firstErrorLine(error);
|
|
807
|
+
error.aiAppBridgePortDiscovery = true;
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
port: ctx.port,
|
|
812
|
+
source: 'default-port',
|
|
813
|
+
error: firstErrorLine(error),
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
if (!shouldUseDefaultPortFallback(ctx)) {
|
|
817
|
+
const error = new Error(`bridge port discovery failed for explicit package: ${ctx.packageName}`);
|
|
818
|
+
error.aiAppBridgePortDiscovery = true;
|
|
819
|
+
ctx.devicePortSource = 'package-port-file';
|
|
820
|
+
ctx.devicePortError = firstErrorLine(error);
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
return { port: ctx.port, source: 'default-port' };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function shouldUseDefaultPortFallback(ctx) {
|
|
827
|
+
return !ctx.explicitPackageName;
|
|
828
|
+
}
|
|
820
829
|
|
|
821
830
|
async function bridgeStatus(ctx, options = {}) {
|
|
822
831
|
try {
|
|
@@ -969,24 +978,24 @@ function normalizeBridgeError(error) {
|
|
|
969
978
|
suggestion: 'Confirm the target app is running and that the resolved bridge port belongs to this package.',
|
|
970
979
|
};
|
|
971
980
|
}
|
|
972
|
-
if (lower.includes('adb timed out')) {
|
|
973
|
-
return {
|
|
974
|
-
code: 'adb_timeout',
|
|
975
|
-
message,
|
|
976
|
-
suggestion: 'Check the device state and retry; if the bridge port is known, pass --port to skip package port discovery.',
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
if (error?.aiAppBridgePackageMismatch || lower.includes('bridge package mismatch')) {
|
|
980
|
-
return {
|
|
981
|
-
code: 'bridge_package_mismatch',
|
|
982
|
-
message,
|
|
983
|
-
suggestion: 'The resolved bridge port belongs to another package. Relaunch the target app and retry with the explicit packageName.',
|
|
984
|
-
};
|
|
985
|
-
}
|
|
986
|
-
if (error?.aiAppBridgePortDiscovery || lower.includes('bridge port discovery failed') || lower.includes('run-as') || lower.includes('package not found')) {
|
|
987
|
-
return {
|
|
988
|
-
code: 'bridge_port_discovery_failed',
|
|
989
|
-
message,
|
|
981
|
+
if (lower.includes('adb timed out')) {
|
|
982
|
+
return {
|
|
983
|
+
code: 'adb_timeout',
|
|
984
|
+
message,
|
|
985
|
+
suggestion: 'Check the device state and retry; if the bridge port is known, pass --port to skip package port discovery.',
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
if (error?.aiAppBridgePackageMismatch || lower.includes('bridge package mismatch')) {
|
|
989
|
+
return {
|
|
990
|
+
code: 'bridge_package_mismatch',
|
|
991
|
+
message,
|
|
992
|
+
suggestion: 'The resolved bridge port belongs to another package. Relaunch the target app and retry with the explicit packageName.',
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
if (error?.aiAppBridgePortDiscovery || lower.includes('bridge port discovery failed') || lower.includes('run-as') || lower.includes('package not found')) {
|
|
996
|
+
return {
|
|
997
|
+
code: 'bridge_port_discovery_failed',
|
|
998
|
+
message,
|
|
990
999
|
suggestion: 'Install a debuggable build for the requested package or pass --port explicitly.',
|
|
991
1000
|
};
|
|
992
1001
|
}
|
|
@@ -1008,38 +1017,38 @@ function firstErrorLine(error) {
|
|
|
1008
1017
|
return String(error?.message || error || 'unknown_error').split(/\r?\n/).find(Boolean) || 'unknown_error';
|
|
1009
1018
|
}
|
|
1010
1019
|
|
|
1011
|
-
async function bridgeGet(ctx, requestPath) {
|
|
1012
|
-
await ensureForward(ctx);
|
|
1013
|
-
const body = await httpGet(bridgeUrl(ctx, requestPath));
|
|
1014
|
-
const payload = JSON.parse(body);
|
|
1015
|
-
verifyBridgeTargetPackage(ctx, payload, requestPath);
|
|
1016
|
-
return payload;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
async function bridgePost(ctx, requestPath, payload) {
|
|
1020
|
-
await ensureForward(ctx);
|
|
1021
|
-
const body = await httpPost(bridgeUrl(ctx, requestPath), payload);
|
|
1022
|
-
const responsePayload = JSON.parse(body);
|
|
1023
|
-
verifyBridgeTargetPackage(ctx, responsePayload, requestPath);
|
|
1024
|
-
return responsePayload;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
function bridgeUrl(ctx, requestPath) {
|
|
1028
|
-
return `http://127.0.0.1:${ctx.hostPort || ctx.port}${requestPath}`;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
function verifyBridgeTargetPackage(ctx, payload, requestPath) {
|
|
1032
|
-
if (!ctx.explicitPackageName || !payload || typeof payload !== 'object') return;
|
|
1033
|
-
const responsePackageName = payload.app && typeof payload.app === 'object'
|
|
1034
|
-
? payload.app.packageName
|
|
1035
|
-
: undefined;
|
|
1036
|
-
if (!responsePackageName || responsePackageName === ctx.packageName) return;
|
|
1037
|
-
const error = new Error(`bridge package mismatch for ${requestPath}: expected ${ctx.packageName}, got ${responsePackageName}`);
|
|
1038
|
-
error.aiAppBridgePackageMismatch = true;
|
|
1039
|
-
error.expectedPackageName = ctx.packageName;
|
|
1040
|
-
error.actualPackageName = responsePackageName;
|
|
1041
|
-
throw error;
|
|
1042
|
-
}
|
|
1020
|
+
async function bridgeGet(ctx, requestPath) {
|
|
1021
|
+
await ensureForward(ctx);
|
|
1022
|
+
const body = await httpGet(bridgeUrl(ctx, requestPath));
|
|
1023
|
+
const payload = JSON.parse(body);
|
|
1024
|
+
verifyBridgeTargetPackage(ctx, payload, requestPath);
|
|
1025
|
+
return payload;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
async function bridgePost(ctx, requestPath, payload) {
|
|
1029
|
+
await ensureForward(ctx);
|
|
1030
|
+
const body = await httpPost(bridgeUrl(ctx, requestPath), payload);
|
|
1031
|
+
const responsePayload = JSON.parse(body);
|
|
1032
|
+
verifyBridgeTargetPackage(ctx, responsePayload, requestPath);
|
|
1033
|
+
return responsePayload;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function bridgeUrl(ctx, requestPath) {
|
|
1037
|
+
return `http://127.0.0.1:${ctx.hostPort || ctx.port}${requestPath}`;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function verifyBridgeTargetPackage(ctx, payload, requestPath) {
|
|
1041
|
+
if (!ctx.explicitPackageName || !payload || typeof payload !== 'object') return;
|
|
1042
|
+
const responsePackageName = payload.app && typeof payload.app === 'object'
|
|
1043
|
+
? payload.app.packageName
|
|
1044
|
+
: undefined;
|
|
1045
|
+
if (!responsePackageName || responsePackageName === ctx.packageName) return;
|
|
1046
|
+
const error = new Error(`bridge package mismatch for ${requestPath}: expected ${ctx.packageName}, got ${responsePackageName}`);
|
|
1047
|
+
error.aiAppBridgePackageMismatch = true;
|
|
1048
|
+
error.expectedPackageName = ctx.packageName;
|
|
1049
|
+
error.actualPackageName = responsePackageName;
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
1043
1052
|
|
|
1044
1053
|
function httpGet(url) {
|
|
1045
1054
|
return new Promise((resolve, reject) => {
|
|
@@ -1528,6 +1537,95 @@ async function packagePidsFor(ctx) {
|
|
|
1528
1537
|
}
|
|
1529
1538
|
}
|
|
1530
1539
|
|
|
1540
|
+
async function freezeApp(ctx, options = {}) {
|
|
1541
|
+
return signalAppProcesses(ctx, options, 'SIGSTOP', 'freeze-app');
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
async function thawApp(ctx, options = {}) {
|
|
1545
|
+
return signalAppProcesses(ctx, options, 'SIGCONT', 'thaw-app');
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
async function signalAppProcesses(ctx, options, signal, action) {
|
|
1549
|
+
const pids = await targetAppPids(ctx, options);
|
|
1550
|
+
if (pids.length === 0) {
|
|
1551
|
+
return {
|
|
1552
|
+
ok: false,
|
|
1553
|
+
packageName: ctx.packageName,
|
|
1554
|
+
action,
|
|
1555
|
+
signal,
|
|
1556
|
+
error: 'app_process_not_found',
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
const results = [];
|
|
1561
|
+
for (const pid of pids) {
|
|
1562
|
+
try {
|
|
1563
|
+
const result = await adb(ctx, ['shell', 'run-as', ctx.packageName, 'kill', `-${signal}`, pid]);
|
|
1564
|
+
results.push({
|
|
1565
|
+
pid,
|
|
1566
|
+
ok: true,
|
|
1567
|
+
stdout: result.stdout.trim(),
|
|
1568
|
+
stderr: result.stderr.trim(),
|
|
1569
|
+
});
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
results.push({
|
|
1572
|
+
pid,
|
|
1573
|
+
ok: false,
|
|
1574
|
+
error: firstErrorLine(error),
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const failed = results.filter((item) => !item.ok);
|
|
1580
|
+
return {
|
|
1581
|
+
ok: failed.length === 0,
|
|
1582
|
+
packageName: ctx.packageName,
|
|
1583
|
+
action,
|
|
1584
|
+
signal,
|
|
1585
|
+
pids,
|
|
1586
|
+
results,
|
|
1587
|
+
...(failed.length > 0 ? { error: 'signal_failed' } : {}),
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
async function targetAppPids(ctx, options = {}) {
|
|
1592
|
+
if (options.pid && options.pid !== true && options.pid !== 'current') {
|
|
1593
|
+
const explicitPid = String(options.pid).trim();
|
|
1594
|
+
return /^\d+$/.test(explicitPid) ? [explicitPid] : [];
|
|
1595
|
+
}
|
|
1596
|
+
const psPids = await packagePidsFromPs(ctx);
|
|
1597
|
+
if (psPids.length > 0) return psPids;
|
|
1598
|
+
return packagePidsFor(ctx);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
async function packagePidsFromPs(ctx) {
|
|
1602
|
+
if (!ctx.packageName) return [];
|
|
1603
|
+
try {
|
|
1604
|
+
const result = await adb(ctx, ['shell', 'ps', '-A', '-o', 'PID,NAME']);
|
|
1605
|
+
return parsePackagePidsFromPs(result.stdout, ctx.packageName);
|
|
1606
|
+
} catch (_) {
|
|
1607
|
+
return [];
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function parsePackagePidsFromPs(stdout, packageName) {
|
|
1612
|
+
const pids = [];
|
|
1613
|
+
const seen = new Set();
|
|
1614
|
+
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
1615
|
+
const trimmed = line.trim();
|
|
1616
|
+
if (!trimmed || /^PID\s+NAME$/i.test(trimmed)) continue;
|
|
1617
|
+
const match = /^(\d+)\s+(\S+)$/.exec(trimmed);
|
|
1618
|
+
if (!match) continue;
|
|
1619
|
+
const pid = match[1];
|
|
1620
|
+
const processName = match[2];
|
|
1621
|
+
if (processName !== packageName && !processName.startsWith(`${packageName}:`)) continue;
|
|
1622
|
+
if (seen.has(pid)) continue;
|
|
1623
|
+
seen.add(pid);
|
|
1624
|
+
pids.push(pid);
|
|
1625
|
+
}
|
|
1626
|
+
return pids;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1531
1629
|
function parseWebViewDevToolsSockets(procNetUnix, packagePids = []) {
|
|
1532
1630
|
const packagePidSet = new Set(packagePids.map(String));
|
|
1533
1631
|
const sockets = [];
|
|
@@ -2843,19 +2941,19 @@ async function tapFlutterText(ctx, targetText, options = {}) {
|
|
|
2843
2941
|
};
|
|
2844
2942
|
}
|
|
2845
2943
|
|
|
2846
|
-
async function inputFlutterText(ctx, targetText, text) {
|
|
2847
|
-
const operable = await flutterNodes(ctx);
|
|
2848
|
-
const node = findFlutterNode(operable, targetText, 'input');
|
|
2849
|
-
if (!node) {
|
|
2850
|
-
throw new Error(`flutter input node not found: ${targetText}`);
|
|
2851
|
-
}
|
|
2852
|
-
const point = flutterNodePoint(node.input?.bounds || node.bounds, operable.viewport);
|
|
2853
|
-
const result = await flutterAction(ctx, { action: 'inputText', text, x: point.x, y: point.y });
|
|
2854
|
-
return {
|
|
2855
|
-
...result,
|
|
2856
|
-
source: 'flutter-operable-tree',
|
|
2857
|
-
targetText,
|
|
2858
|
-
text,
|
|
2944
|
+
async function inputFlutterText(ctx, targetText, text) {
|
|
2945
|
+
const operable = await flutterNodes(ctx);
|
|
2946
|
+
const node = findFlutterNode(operable, targetText, 'input');
|
|
2947
|
+
if (!node) {
|
|
2948
|
+
throw new Error(`flutter input node not found: ${targetText}`);
|
|
2949
|
+
}
|
|
2950
|
+
const point = flutterNodePoint(node.input?.bounds || node.bounds, operable.viewport);
|
|
2951
|
+
const result = await flutterAction(ctx, { action: 'inputText', text, x: point.x, y: point.y });
|
|
2952
|
+
return {
|
|
2953
|
+
...result,
|
|
2954
|
+
source: 'flutter-operable-tree',
|
|
2955
|
+
targetText,
|
|
2956
|
+
text,
|
|
2859
2957
|
node,
|
|
2860
2958
|
x: point.x,
|
|
2861
2959
|
y: point.y,
|
|
@@ -3529,78 +3627,78 @@ function shouldDismissKeyboardForPoint({ point, viewport, keyboardVisible }) {
|
|
|
3529
3627
|
};
|
|
3530
3628
|
}
|
|
3531
3629
|
|
|
3532
|
-
async function inputText(ctx, text, options = {}) {
|
|
3533
|
-
const bridgePayload = inputTextBridgePayload(text, options);
|
|
3534
|
-
let bridgeAttempt = null;
|
|
3535
|
-
try {
|
|
3536
|
-
const bridgeResult = await bridgePost(ctx, '/v1/action/input-text', bridgePayload);
|
|
3537
|
-
if (bridgeResult?.ok) {
|
|
3538
|
-
const result = {
|
|
3539
|
-
...bridgeResult,
|
|
3540
|
-
transport: 'bridge',
|
|
3541
|
-
source: bridgeResult.source || 'native-view',
|
|
3542
|
-
request: bridgePayload,
|
|
3543
|
-
};
|
|
3544
|
-
if (booleanOption(options.hideKeyboard)) {
|
|
3545
|
-
result.keyboard = await hideKeyboard(ctx, options);
|
|
3546
|
-
}
|
|
3547
|
-
return result;
|
|
3548
|
-
}
|
|
3549
|
-
bridgeAttempt = {
|
|
3550
|
-
ok: false,
|
|
3551
|
-
command: 'input-text',
|
|
3552
|
-
requestPath: '/v1/action/input-text',
|
|
3553
|
-
result: bridgeResult,
|
|
3554
|
-
error: bridgeResult?.error || 'bridge_input_failed',
|
|
3555
|
-
message: bridgeResult?.message || 'The app bridge did not accept native text input.',
|
|
3556
|
-
};
|
|
3557
|
-
} catch (error) {
|
|
3558
|
-
bridgeAttempt = buildBridgeFailureResult(ctx, 'input-text', '/v1/action/input-text', error);
|
|
3559
|
-
}
|
|
3560
|
-
|
|
3561
|
-
if (!isAdbInputTextSafe(text)) {
|
|
3562
|
-
return {
|
|
3563
|
-
ok: false,
|
|
3564
|
-
error: 'unicode_text_requires_bridge_input',
|
|
3565
|
-
message: 'This text contains non-ASCII characters. Android adb shell input text cannot reliably enter Unicode; use an app build with AI App Bridge Android runtime 0.1.9+ and retry input-text/input_text.',
|
|
3566
|
-
textLength: text.length,
|
|
3567
|
-
bridge: bridgeAttempt,
|
|
3568
|
-
suggestion: 'Update the target app bridge dependency to ai-app-bridge-android 0.1.9+ and pass --package-name so the CLI can discover the app bridge port.',
|
|
3569
|
-
};
|
|
3570
|
-
}
|
|
3571
|
-
|
|
3572
|
-
await adb(ctx, ['shell', 'input', 'text', text.replace(/ /g, '%s')]);
|
|
3573
|
-
const result = {
|
|
3574
|
-
ok: true,
|
|
3575
|
-
transport: 'adb',
|
|
3576
|
-
source: 'adb-input-text-fallback',
|
|
3577
|
-
text,
|
|
3578
|
-
bridge: bridgeAttempt,
|
|
3579
|
-
};
|
|
3580
|
-
if (booleanOption(options.hideKeyboard)) {
|
|
3581
|
-
result.keyboard = await hideKeyboard(ctx, options);
|
|
3582
|
-
}
|
|
3583
|
-
return result;
|
|
3584
|
-
}
|
|
3585
|
-
|
|
3586
|
-
function inputTextBridgePayload(text, options = {}) {
|
|
3587
|
-
const payload = { text };
|
|
3588
|
-
const x = Number(options.tapX);
|
|
3589
|
-
const y = Number(options.tapY);
|
|
3590
|
-
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
3591
|
-
payload.x = x;
|
|
3592
|
-
payload.y = y;
|
|
3593
|
-
}
|
|
3594
|
-
return payload;
|
|
3595
|
-
}
|
|
3596
|
-
|
|
3597
|
-
function isAdbInputTextSafe(text) {
|
|
3598
|
-
return /^[\x20-\x7e]*$/.test(String(text));
|
|
3599
|
-
}
|
|
3600
|
-
|
|
3601
|
-
async function swipe(ctx, startX, startY, endX, endY, durationMs) {
|
|
3602
|
-
await adb(ctx, ['shell', 'input', 'swipe', String(startX), String(startY), String(endX), String(endY), String(durationMs)]);
|
|
3603
|
-
return { ok: true, transport: 'adb', startX, startY, endX, endY, durationMs };
|
|
3630
|
+
async function inputText(ctx, text, options = {}) {
|
|
3631
|
+
const bridgePayload = inputTextBridgePayload(text, options);
|
|
3632
|
+
let bridgeAttempt = null;
|
|
3633
|
+
try {
|
|
3634
|
+
const bridgeResult = await bridgePost(ctx, '/v1/action/input-text', bridgePayload);
|
|
3635
|
+
if (bridgeResult?.ok) {
|
|
3636
|
+
const result = {
|
|
3637
|
+
...bridgeResult,
|
|
3638
|
+
transport: 'bridge',
|
|
3639
|
+
source: bridgeResult.source || 'native-view',
|
|
3640
|
+
request: bridgePayload,
|
|
3641
|
+
};
|
|
3642
|
+
if (booleanOption(options.hideKeyboard)) {
|
|
3643
|
+
result.keyboard = await hideKeyboard(ctx, options);
|
|
3644
|
+
}
|
|
3645
|
+
return result;
|
|
3646
|
+
}
|
|
3647
|
+
bridgeAttempt = {
|
|
3648
|
+
ok: false,
|
|
3649
|
+
command: 'input-text',
|
|
3650
|
+
requestPath: '/v1/action/input-text',
|
|
3651
|
+
result: bridgeResult,
|
|
3652
|
+
error: bridgeResult?.error || 'bridge_input_failed',
|
|
3653
|
+
message: bridgeResult?.message || 'The app bridge did not accept native text input.',
|
|
3654
|
+
};
|
|
3655
|
+
} catch (error) {
|
|
3656
|
+
bridgeAttempt = buildBridgeFailureResult(ctx, 'input-text', '/v1/action/input-text', error);
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
if (!isAdbInputTextSafe(text)) {
|
|
3660
|
+
return {
|
|
3661
|
+
ok: false,
|
|
3662
|
+
error: 'unicode_text_requires_bridge_input',
|
|
3663
|
+
message: 'This text contains non-ASCII characters. Android adb shell input text cannot reliably enter Unicode; use an app build with AI App Bridge Android runtime 0.1.9+ and retry input-text/input_text.',
|
|
3664
|
+
textLength: text.length,
|
|
3665
|
+
bridge: bridgeAttempt,
|
|
3666
|
+
suggestion: 'Update the target app bridge dependency to ai-app-bridge-android 0.1.9+ and pass --package-name so the CLI can discover the app bridge port.',
|
|
3667
|
+
};
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
await adb(ctx, ['shell', 'input', 'text', text.replace(/ /g, '%s')]);
|
|
3671
|
+
const result = {
|
|
3672
|
+
ok: true,
|
|
3673
|
+
transport: 'adb',
|
|
3674
|
+
source: 'adb-input-text-fallback',
|
|
3675
|
+
text,
|
|
3676
|
+
bridge: bridgeAttempt,
|
|
3677
|
+
};
|
|
3678
|
+
if (booleanOption(options.hideKeyboard)) {
|
|
3679
|
+
result.keyboard = await hideKeyboard(ctx, options);
|
|
3680
|
+
}
|
|
3681
|
+
return result;
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
function inputTextBridgePayload(text, options = {}) {
|
|
3685
|
+
const payload = { text };
|
|
3686
|
+
const x = Number(options.tapX);
|
|
3687
|
+
const y = Number(options.tapY);
|
|
3688
|
+
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
3689
|
+
payload.x = x;
|
|
3690
|
+
payload.y = y;
|
|
3691
|
+
}
|
|
3692
|
+
return payload;
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
function isAdbInputTextSafe(text) {
|
|
3696
|
+
return /^[\x20-\x7e]*$/.test(String(text));
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
async function swipe(ctx, startX, startY, endX, endY, durationMs) {
|
|
3700
|
+
await adb(ctx, ['shell', 'input', 'swipe', String(startX), String(startY), String(endX), String(endY), String(durationMs)]);
|
|
3701
|
+
return { ok: true, transport: 'adb', startX, startY, endX, endY, durationMs };
|
|
3604
3702
|
}
|
|
3605
3703
|
|
|
3606
3704
|
async function keyevent(ctx, keyCode) {
|
|
@@ -3710,6 +3808,59 @@ async function appopsSet(ctx, op, mode) {
|
|
|
3710
3808
|
};
|
|
3711
3809
|
}
|
|
3712
3810
|
|
|
3811
|
+
async function clearAppData(ctx) {
|
|
3812
|
+
if (!ctx.explicitPackageName) {
|
|
3813
|
+
throw new Error('packageName is required for clear-app-data');
|
|
3814
|
+
}
|
|
3815
|
+
let bridgeError = null;
|
|
3816
|
+
try {
|
|
3817
|
+
const bridgeResult = await bridgePost(ctx, '/v1/app/clear-data', {});
|
|
3818
|
+
return {
|
|
3819
|
+
...bridgeResult,
|
|
3820
|
+
packageName: ctx.packageName,
|
|
3821
|
+
method: 'bridge-runtime',
|
|
3822
|
+
};
|
|
3823
|
+
} catch (error) {
|
|
3824
|
+
bridgeError = firstErrorLine(error);
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
let result;
|
|
3828
|
+
try {
|
|
3829
|
+
result = await adb(ctx, clearAppDataAdbArgs(ctx.packageName));
|
|
3830
|
+
} catch (error) {
|
|
3831
|
+
return {
|
|
3832
|
+
ok: false,
|
|
3833
|
+
packageName: ctx.packageName,
|
|
3834
|
+
action: 'clear-app-data',
|
|
3835
|
+
error: 'clear_app_data_failed',
|
|
3836
|
+
bridgeError,
|
|
3837
|
+
message: error.message || String(error),
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
3841
|
+
if (!/^Success\b/im.test(output)) {
|
|
3842
|
+
return {
|
|
3843
|
+
ok: false,
|
|
3844
|
+
packageName: ctx.packageName,
|
|
3845
|
+
error: 'clear_app_data_failed',
|
|
3846
|
+
bridgeError,
|
|
3847
|
+
output,
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
return {
|
|
3851
|
+
ok: true,
|
|
3852
|
+
packageName: ctx.packageName,
|
|
3853
|
+
action: 'clear-app-data',
|
|
3854
|
+
method: 'pm-clear',
|
|
3855
|
+
bridgeError,
|
|
3856
|
+
output,
|
|
3857
|
+
};
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
function clearAppDataAdbArgs(packageName) {
|
|
3861
|
+
return ['shell', 'pm', 'clear', packageName];
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3713
3864
|
async function resolveLogcatPid(ctx, options) {
|
|
3714
3865
|
if (options.pid && options.pid !== true && options.pid !== 'current') {
|
|
3715
3866
|
return String(options.pid);
|
|
@@ -3725,17 +3876,17 @@ async function resolveLogcatPid(ctx, options) {
|
|
|
3725
3876
|
return '';
|
|
3726
3877
|
}
|
|
3727
3878
|
|
|
3728
|
-
function filterLogcat(text, options) {
|
|
3729
|
-
const tags = splitCsv(options.tag || options.tags);
|
|
3730
|
-
const grep = options.grep ? String(options.grep) : '';
|
|
3731
|
-
const grepCaseSensitive = Boolean(options.grepCaseSensitive);
|
|
3732
|
-
const minLevel = priorityValue(options.level || options.minLevel || '');
|
|
3733
|
-
const pid = options.pid ? String(options.pid) : '';
|
|
3734
|
-
const requiresAppPid = options.appPid || options.packagePid || options.pid === 'current';
|
|
3735
|
-
if (requiresAppPid && !pid) {
|
|
3736
|
-
return '';
|
|
3737
|
-
}
|
|
3738
|
-
const lines = String(text || '').split(/\r?\n/);
|
|
3879
|
+
function filterLogcat(text, options) {
|
|
3880
|
+
const tags = splitCsv(options.tag || options.tags);
|
|
3881
|
+
const grep = options.grep ? String(options.grep) : '';
|
|
3882
|
+
const grepCaseSensitive = Boolean(options.grepCaseSensitive);
|
|
3883
|
+
const minLevel = priorityValue(options.level || options.minLevel || '');
|
|
3884
|
+
const pid = options.pid ? String(options.pid) : '';
|
|
3885
|
+
const requiresAppPid = options.appPid || options.packagePid || options.pid === 'current';
|
|
3886
|
+
if (requiresAppPid && !pid) {
|
|
3887
|
+
return '';
|
|
3888
|
+
}
|
|
3889
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
3739
3890
|
const filtered = [];
|
|
3740
3891
|
let previousIncluded = false;
|
|
3741
3892
|
for (const line of lines) {
|
|
@@ -3818,134 +3969,134 @@ function adbFollow(ctx, args, durationMs) {
|
|
|
3818
3969
|
});
|
|
3819
3970
|
}
|
|
3820
3971
|
|
|
3821
|
-
async function launchApp(ctx, options = {}) {
|
|
3822
|
-
if (options.component || options.activity) {
|
|
3823
|
-
return launchActivity(ctx, options);
|
|
3824
|
-
}
|
|
3825
|
-
|
|
3826
|
-
const candidates = await launcherActivityCandidates(ctx);
|
|
3827
|
-
if (candidates.length === 0) {
|
|
3828
|
-
return {
|
|
3829
|
-
ok: false,
|
|
3830
|
-
error: 'launcher_not_found',
|
|
3831
|
-
packageName: ctx.packageName,
|
|
3832
|
-
launcherCandidates: candidates,
|
|
3833
|
-
};
|
|
3834
|
-
}
|
|
3835
|
-
if (candidates.length > 1) {
|
|
3836
|
-
return {
|
|
3837
|
-
ok: false,
|
|
3838
|
-
error: 'launcher_ambiguous',
|
|
3839
|
-
packageName: ctx.packageName,
|
|
3840
|
-
launcherCandidates: candidates,
|
|
3841
|
-
suggestion: 'Pass --component or --activity to choose the intended launcher Activity.',
|
|
3842
|
-
};
|
|
3843
|
-
}
|
|
3844
|
-
|
|
3845
|
-
return startActivity(ctx, candidates[0], options, {
|
|
3846
|
-
packageName: ctx.packageName,
|
|
3847
|
-
launcherCandidates: candidates,
|
|
3848
|
-
});
|
|
3849
|
-
}
|
|
3850
|
-
|
|
3851
|
-
async function launchActivity(ctx, options = {}) {
|
|
3852
|
-
const component = normalizeActivityComponent(
|
|
3853
|
-
ctx.packageName,
|
|
3854
|
-
options.component || requiredString(options.activity, 'activity'),
|
|
3855
|
-
);
|
|
3856
|
-
return startActivity(ctx, component, options, { packageName: ctx.packageName });
|
|
3857
|
-
}
|
|
3858
|
-
|
|
3859
|
-
async function launchNativeTest(ctx) {
|
|
3860
|
-
const component = normalizeActivityComponent(ctx.packageName, ctx.nativeActivity);
|
|
3861
|
-
return startActivity(ctx, component, {}, { packageName: ctx.packageName });
|
|
3862
|
-
}
|
|
3863
|
-
|
|
3864
|
-
async function launchFlutter(ctx, initialRoute) {
|
|
3865
|
-
const component = normalizeActivityComponent(ctx.packageName, ctx.flutterActivity);
|
|
3866
|
-
const options = initialRoute ? { extra: [`ai_app_initial_route=${initialRoute}`] } : {};
|
|
3867
|
-
const result = await startActivity(ctx, component, options, { packageName: ctx.packageName });
|
|
3868
|
-
return { ...result, initialRoute };
|
|
3869
|
-
}
|
|
3870
|
-
|
|
3871
|
-
async function launcherActivityCandidates(ctx) {
|
|
3872
|
-
const result = await adb(ctx, [
|
|
3873
|
-
'shell',
|
|
3874
|
-
'cmd',
|
|
3875
|
-
'package',
|
|
3876
|
-
'query-activities',
|
|
3877
|
-
'--brief',
|
|
3878
|
-
'-a',
|
|
3879
|
-
'android.intent.action.MAIN',
|
|
3880
|
-
'-c',
|
|
3881
|
-
'android.intent.category.LAUNCHER',
|
|
3882
|
-
ctx.packageName,
|
|
3883
|
-
]);
|
|
3884
|
-
return parseLauncherActivityCandidates(result.stdout);
|
|
3885
|
-
}
|
|
3886
|
-
|
|
3887
|
-
function parseLauncherActivityCandidates(stdout) {
|
|
3888
|
-
const candidates = [];
|
|
3889
|
-
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
3890
|
-
const candidate = line.trim();
|
|
3891
|
-
if (/^[A-Za-z0-9_.$]+\/[A-Za-z0-9_.$]+$/.test(candidate) && !candidates.includes(candidate)) {
|
|
3892
|
-
candidates.push(candidate);
|
|
3893
|
-
}
|
|
3894
|
-
}
|
|
3895
|
-
return candidates;
|
|
3896
|
-
}
|
|
3897
|
-
|
|
3898
|
-
function normalizeActivityComponent(packageName, activityOrComponent) {
|
|
3899
|
-
const value = requiredString(activityOrComponent, 'activity');
|
|
3900
|
-
if (value.includes('/')) return value;
|
|
3901
|
-
return `${packageName}/${value}`;
|
|
3902
|
-
}
|
|
3903
|
-
|
|
3904
|
-
async function startActivity(ctx, component, options = {}, extraResult = {}) {
|
|
3905
|
-
const args = buildAmStartArgs(component, options);
|
|
3906
|
-
const result = await adb(ctx, args);
|
|
3907
|
-
return {
|
|
3908
|
-
ok: true,
|
|
3909
|
-
transport: 'adb',
|
|
3910
|
-
component,
|
|
3911
|
-
...extraResult,
|
|
3912
|
-
stdout: result.stdout.trim(),
|
|
3913
|
-
stderr: result.stderr.trim(),
|
|
3914
|
-
};
|
|
3915
|
-
}
|
|
3916
|
-
|
|
3917
|
-
function buildAmStartArgs(component, options = {}) {
|
|
3918
|
-
const args = ['shell', 'am', 'start'];
|
|
3919
|
-
if (options.action) args.push('-a', options.action);
|
|
3920
|
-
for (const category of optionList(options.category)) {
|
|
3921
|
-
args.push('-c', category);
|
|
3922
|
-
}
|
|
3923
|
-
if (options.data) args.push('-d', options.data);
|
|
3924
|
-
for (const extra of parseStartExtras(options.extra)) {
|
|
3925
|
-
args.push('-e', extra.key, extra.value);
|
|
3926
|
-
}
|
|
3927
|
-
args.push('-n', component);
|
|
3928
|
-
return args;
|
|
3929
|
-
}
|
|
3930
|
-
|
|
3931
|
-
function parseStartExtras(rawExtras) {
|
|
3932
|
-
return optionList(rawExtras).map((rawExtra) => {
|
|
3933
|
-
const value = String(rawExtra);
|
|
3934
|
-
const separator = value.indexOf('=');
|
|
3935
|
-
if (separator <= 0) {
|
|
3936
|
-
throw new Error('extra must use key=value');
|
|
3937
|
-
}
|
|
3938
|
-
return {
|
|
3939
|
-
key: value.slice(0, separator),
|
|
3940
|
-
value: value.slice(separator + 1),
|
|
3941
|
-
};
|
|
3942
|
-
});
|
|
3943
|
-
}
|
|
3944
|
-
|
|
3945
|
-
function optionList(value) {
|
|
3946
|
-
if (value === undefined || value === null || value === false || value === '') return [];
|
|
3947
|
-
return Array.isArray(value) ? value : [value];
|
|
3948
|
-
}
|
|
3972
|
+
async function launchApp(ctx, options = {}) {
|
|
3973
|
+
if (options.component || options.activity) {
|
|
3974
|
+
return launchActivity(ctx, options);
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
const candidates = await launcherActivityCandidates(ctx);
|
|
3978
|
+
if (candidates.length === 0) {
|
|
3979
|
+
return {
|
|
3980
|
+
ok: false,
|
|
3981
|
+
error: 'launcher_not_found',
|
|
3982
|
+
packageName: ctx.packageName,
|
|
3983
|
+
launcherCandidates: candidates,
|
|
3984
|
+
};
|
|
3985
|
+
}
|
|
3986
|
+
if (candidates.length > 1) {
|
|
3987
|
+
return {
|
|
3988
|
+
ok: false,
|
|
3989
|
+
error: 'launcher_ambiguous',
|
|
3990
|
+
packageName: ctx.packageName,
|
|
3991
|
+
launcherCandidates: candidates,
|
|
3992
|
+
suggestion: 'Pass --component or --activity to choose the intended launcher Activity.',
|
|
3993
|
+
};
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
return startActivity(ctx, candidates[0], options, {
|
|
3997
|
+
packageName: ctx.packageName,
|
|
3998
|
+
launcherCandidates: candidates,
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
async function launchActivity(ctx, options = {}) {
|
|
4003
|
+
const component = normalizeActivityComponent(
|
|
4004
|
+
ctx.packageName,
|
|
4005
|
+
options.component || requiredString(options.activity, 'activity'),
|
|
4006
|
+
);
|
|
4007
|
+
return startActivity(ctx, component, options, { packageName: ctx.packageName });
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
async function launchNativeTest(ctx) {
|
|
4011
|
+
const component = normalizeActivityComponent(ctx.packageName, ctx.nativeActivity);
|
|
4012
|
+
return startActivity(ctx, component, {}, { packageName: ctx.packageName });
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
async function launchFlutter(ctx, initialRoute) {
|
|
4016
|
+
const component = normalizeActivityComponent(ctx.packageName, ctx.flutterActivity);
|
|
4017
|
+
const options = initialRoute ? { extra: [`ai_app_initial_route=${initialRoute}`] } : {};
|
|
4018
|
+
const result = await startActivity(ctx, component, options, { packageName: ctx.packageName });
|
|
4019
|
+
return { ...result, initialRoute };
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
async function launcherActivityCandidates(ctx) {
|
|
4023
|
+
const result = await adb(ctx, [
|
|
4024
|
+
'shell',
|
|
4025
|
+
'cmd',
|
|
4026
|
+
'package',
|
|
4027
|
+
'query-activities',
|
|
4028
|
+
'--brief',
|
|
4029
|
+
'-a',
|
|
4030
|
+
'android.intent.action.MAIN',
|
|
4031
|
+
'-c',
|
|
4032
|
+
'android.intent.category.LAUNCHER',
|
|
4033
|
+
ctx.packageName,
|
|
4034
|
+
]);
|
|
4035
|
+
return parseLauncherActivityCandidates(result.stdout);
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
function parseLauncherActivityCandidates(stdout) {
|
|
4039
|
+
const candidates = [];
|
|
4040
|
+
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
4041
|
+
const candidate = line.trim();
|
|
4042
|
+
if (/^[A-Za-z0-9_.$]+\/[A-Za-z0-9_.$]+$/.test(candidate) && !candidates.includes(candidate)) {
|
|
4043
|
+
candidates.push(candidate);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
return candidates;
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
function normalizeActivityComponent(packageName, activityOrComponent) {
|
|
4050
|
+
const value = requiredString(activityOrComponent, 'activity');
|
|
4051
|
+
if (value.includes('/')) return value;
|
|
4052
|
+
return `${packageName}/${value}`;
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
async function startActivity(ctx, component, options = {}, extraResult = {}) {
|
|
4056
|
+
const args = buildAmStartArgs(component, options);
|
|
4057
|
+
const result = await adb(ctx, args);
|
|
4058
|
+
return {
|
|
4059
|
+
ok: true,
|
|
4060
|
+
transport: 'adb',
|
|
4061
|
+
component,
|
|
4062
|
+
...extraResult,
|
|
4063
|
+
stdout: result.stdout.trim(),
|
|
4064
|
+
stderr: result.stderr.trim(),
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
function buildAmStartArgs(component, options = {}) {
|
|
4069
|
+
const args = ['shell', 'am', 'start'];
|
|
4070
|
+
if (options.action) args.push('-a', options.action);
|
|
4071
|
+
for (const category of optionList(options.category)) {
|
|
4072
|
+
args.push('-c', category);
|
|
4073
|
+
}
|
|
4074
|
+
if (options.data) args.push('-d', options.data);
|
|
4075
|
+
for (const extra of parseStartExtras(options.extra)) {
|
|
4076
|
+
args.push('-e', extra.key, extra.value);
|
|
4077
|
+
}
|
|
4078
|
+
args.push('-n', component);
|
|
4079
|
+
return args;
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
function parseStartExtras(rawExtras) {
|
|
4083
|
+
return optionList(rawExtras).map((rawExtra) => {
|
|
4084
|
+
const value = String(rawExtra);
|
|
4085
|
+
const separator = value.indexOf('=');
|
|
4086
|
+
if (separator <= 0) {
|
|
4087
|
+
throw new Error('extra must use key=value');
|
|
4088
|
+
}
|
|
4089
|
+
return {
|
|
4090
|
+
key: value.slice(0, separator),
|
|
4091
|
+
value: value.slice(separator + 1),
|
|
4092
|
+
};
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
function optionList(value) {
|
|
4097
|
+
if (value === undefined || value === null || value === false || value === '') return [];
|
|
4098
|
+
return Array.isArray(value) ? value : [value];
|
|
4099
|
+
}
|
|
3949
4100
|
|
|
3950
4101
|
async function smoke(ctx, options) {
|
|
3951
4102
|
const summary = {
|
|
@@ -4304,6 +4455,7 @@ function requiredNumber(value, name) {
|
|
|
4304
4455
|
|
|
4305
4456
|
module.exports = {
|
|
4306
4457
|
buildBridgeFailureResult,
|
|
4458
|
+
clearAppDataAdbArgs,
|
|
4307
4459
|
defaultInstallerButtonTexts,
|
|
4308
4460
|
artifactTimestamp,
|
|
4309
4461
|
compactBridgeTree,
|
|
@@ -4311,41 +4463,42 @@ module.exports = {
|
|
|
4311
4463
|
compactUiaTree,
|
|
4312
4464
|
defaultArtifactDirectory,
|
|
4313
4465
|
defaultArtifactPath,
|
|
4314
|
-
findFlutterNode,
|
|
4315
|
-
findTappableNodeByText,
|
|
4316
|
-
filterLogcat,
|
|
4317
|
-
firstErrorLine,
|
|
4466
|
+
findFlutterNode,
|
|
4467
|
+
findTappableNodeByText,
|
|
4468
|
+
filterLogcat,
|
|
4469
|
+
firstErrorLine,
|
|
4318
4470
|
flutterNodePoint,
|
|
4319
4471
|
flutterPhysicalViewport,
|
|
4320
4472
|
helpText,
|
|
4321
|
-
installerButtonTextsForSurface,
|
|
4322
|
-
isAdbInputTextSafe,
|
|
4323
|
-
isLikelyInstallerSurface,
|
|
4473
|
+
installerButtonTextsForSurface,
|
|
4474
|
+
isAdbInputTextSafe,
|
|
4475
|
+
isLikelyInstallerSurface,
|
|
4324
4476
|
nodeTapState,
|
|
4325
|
-
normalizeBridgeError,
|
|
4326
|
-
normalizeActivityComponent,
|
|
4327
|
-
parseWebViewDevToolsSockets,
|
|
4328
|
-
parseArgs,
|
|
4329
|
-
parseKeyboardState,
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4477
|
+
normalizeBridgeError,
|
|
4478
|
+
normalizeActivityComponent,
|
|
4479
|
+
parseWebViewDevToolsSockets,
|
|
4480
|
+
parseArgs,
|
|
4481
|
+
parseKeyboardState,
|
|
4482
|
+
parsePackagePidsFromPs,
|
|
4483
|
+
parseLauncherActivityCandidates,
|
|
4484
|
+
parseStartExtras,
|
|
4485
|
+
parseUiaBounds,
|
|
4333
4486
|
parseUiaViewport,
|
|
4334
4487
|
parseComponentFromWindowLine,
|
|
4335
4488
|
parseForegroundWindow,
|
|
4336
4489
|
chooseWebViewDevToolsSocket,
|
|
4337
4490
|
chooseWebViewPage,
|
|
4338
|
-
shapeNetworkCapture,
|
|
4339
|
-
compactNetworkRecord,
|
|
4340
|
-
pruneGeneratedArtifacts,
|
|
4341
|
-
shouldSkipInstallerTapForInstalledPackage,
|
|
4342
|
-
shouldDismissKeyboardForPoint,
|
|
4343
|
-
shouldUseDefaultPortFallback,
|
|
4344
|
-
screenshotOutputPath,
|
|
4345
|
-
statusSearchText,
|
|
4346
|
-
uiautomatorLockPath,
|
|
4347
|
-
verifyBridgeTargetPackage,
|
|
4348
|
-
waitTextConditionsMet,
|
|
4349
|
-
withFileLock,
|
|
4350
|
-
};
|
|
4491
|
+
shapeNetworkCapture,
|
|
4492
|
+
compactNetworkRecord,
|
|
4493
|
+
pruneGeneratedArtifacts,
|
|
4494
|
+
shouldSkipInstallerTapForInstalledPackage,
|
|
4495
|
+
shouldDismissKeyboardForPoint,
|
|
4496
|
+
shouldUseDefaultPortFallback,
|
|
4497
|
+
screenshotOutputPath,
|
|
4498
|
+
statusSearchText,
|
|
4499
|
+
uiautomatorLockPath,
|
|
4500
|
+
verifyBridgeTargetPackage,
|
|
4501
|
+
waitTextConditionsMet,
|
|
4502
|
+
withFileLock,
|
|
4503
|
+
};
|
|
4351
4504
|
|