@aiscene/android 1.6.7 → 1.6.8
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/LICENSE +21 -0
- package/dist/es/cli.mjs +125 -95
- package/dist/es/index.mjs +124 -94
- package/dist/es/mcp-server.mjs +125 -95
- package/dist/lib/cli.js +123 -93
- package/dist/lib/index.js +122 -92
- package/dist/lib/mcp-server.js +123 -93
- package/dist/types/index.d.ts +40 -31
- package/dist/types/mcp-server.d.ts +40 -31
- package/package.json +24 -21
package/dist/es/index.mjs
CHANGED
|
@@ -3,11 +3,12 @@ import * as __rspack_external_node_fs_5ea92f0c from "node:fs";
|
|
|
3
3
|
import * as __rspack_external_node_module_ab9f2194 from "node:module";
|
|
4
4
|
import * as __rspack_external_node_path_c5b9b54f from "node:path";
|
|
5
5
|
import node_assert from "node:assert";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
6
7
|
import { getMidsceneLocationSchema, z } from "@midscene/core";
|
|
7
|
-
import { defineAction, defineActionClearInput, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionScroll, defineActionTap } from "@midscene/core/device";
|
|
8
|
+
import { defineAction, defineActionClearInput, defineActionCursorMove, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionPinch, defineActionScroll, defineActionSwipe, defineActionTap, normalizeMobileSwipeParam, normalizePinchParam } from "@midscene/core/device";
|
|
8
9
|
import { getTmpFile, sleep } from "@midscene/core/utils";
|
|
9
10
|
import { MIDSCENE_ADB_PATH, MIDSCENE_ADB_REMOTE_HOST, MIDSCENE_ADB_REMOTE_PORT, MIDSCENE_ANDROID_IME_STRATEGY, globalConfigManager, overrideAIConfig } from "@midscene/shared/env";
|
|
10
|
-
import { createImgBase64ByFormat,
|
|
11
|
+
import { createImgBase64ByFormat, isValidImageBuffer } from "@midscene/shared/img";
|
|
11
12
|
import { mergeAndNormalizeAppNameMapping, normalizeForComparison, repeat } from "@midscene/shared/utils";
|
|
12
13
|
import { ADB } from "appium-adb";
|
|
13
14
|
import { Agent } from "@midscene/core/agent";
|
|
@@ -578,6 +579,9 @@ const defaultNormalScrollDuration = 1000;
|
|
|
578
579
|
const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
|
|
579
580
|
const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
|
|
580
581
|
const debugDevice = (0, logger_.getDebug)('android:device');
|
|
582
|
+
function escapeForShell(text) {
|
|
583
|
+
return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
|
|
584
|
+
}
|
|
581
585
|
class AndroidDevice {
|
|
582
586
|
actionSpace() {
|
|
583
587
|
const defaultActions = [
|
|
@@ -605,6 +609,12 @@ class AndroidDevice {
|
|
|
605
609
|
]).default('replace').optional().describe('Input mode: "replace" (default) - clear the field and input the value; "typeOnly" - type the value directly without clearing the field first; "clear" - clear the field without inputting new text.')),
|
|
606
610
|
locate: getMidsceneLocationSchema().describe('The input field to be filled').optional()
|
|
607
611
|
}),
|
|
612
|
+
sample: {
|
|
613
|
+
value: 'test@example.com',
|
|
614
|
+
locate: {
|
|
615
|
+
prompt: 'the email input field'
|
|
616
|
+
}
|
|
617
|
+
},
|
|
608
618
|
call: async (param)=>{
|
|
609
619
|
const element = param.locate;
|
|
610
620
|
if ('typeOnly' !== param.mode) await this.clearInput(element);
|
|
@@ -650,9 +660,21 @@ class AndroidDevice {
|
|
|
650
660
|
y: to.center[1]
|
|
651
661
|
});
|
|
652
662
|
}),
|
|
663
|
+
defineActionSwipe(async (param)=>{
|
|
664
|
+
const { startPoint, endPoint, duration, repeatCount } = normalizeMobileSwipeParam(param, await this.size());
|
|
665
|
+
for(let i = 0; i < repeatCount; i++)await this.mouseDrag(startPoint, endPoint, duration);
|
|
666
|
+
}),
|
|
653
667
|
defineActionKeyboardPress(async (param)=>{
|
|
654
668
|
await this.keyboardPress(param.keyName);
|
|
655
669
|
}),
|
|
670
|
+
defineActionCursorMove(async (param)=>{
|
|
671
|
+
const arrowKey = 'left' === param.direction ? 'ArrowLeft' : 'ArrowRight';
|
|
672
|
+
const times = param.times ?? 1;
|
|
673
|
+
for(let i = 0; i < times; i++){
|
|
674
|
+
await this.keyboardPress(arrowKey);
|
|
675
|
+
await sleep(100);
|
|
676
|
+
}
|
|
677
|
+
}),
|
|
656
678
|
defineAction({
|
|
657
679
|
name: 'LongPress',
|
|
658
680
|
description: 'Trigger a long press on the screen at specified element',
|
|
@@ -660,6 +682,11 @@ class AndroidDevice {
|
|
|
660
682
|
duration: z.number().optional().describe('The duration of the long press in milliseconds'),
|
|
661
683
|
locate: getMidsceneLocationSchema().describe('The element to be long pressed')
|
|
662
684
|
}),
|
|
685
|
+
sample: {
|
|
686
|
+
locate: {
|
|
687
|
+
prompt: 'the message bubble'
|
|
688
|
+
}
|
|
689
|
+
},
|
|
663
690
|
call: async (param)=>{
|
|
664
691
|
const element = param.locate;
|
|
665
692
|
if (!element) throw new Error('LongPress requires an element to be located');
|
|
@@ -679,6 +706,12 @@ class AndroidDevice {
|
|
|
679
706
|
duration: z.number().optional().describe('The duration of the pull (in milliseconds)'),
|
|
680
707
|
locate: getMidsceneLocationSchema().optional().describe('The element to start the pull from (optional)')
|
|
681
708
|
}),
|
|
709
|
+
sample: {
|
|
710
|
+
direction: 'down',
|
|
711
|
+
locate: {
|
|
712
|
+
prompt: 'the center of the content list area'
|
|
713
|
+
}
|
|
714
|
+
},
|
|
682
715
|
call: async (param)=>{
|
|
683
716
|
const element = param.locate;
|
|
684
717
|
const startPoint = element ? {
|
|
@@ -691,6 +724,16 @@ class AndroidDevice {
|
|
|
691
724
|
else throw new Error(`Unknown pull direction: ${param.direction}`);
|
|
692
725
|
}
|
|
693
726
|
}),
|
|
727
|
+
defineActionPinch(async (param)=>{
|
|
728
|
+
const { centerX, centerY, startDistance, endDistance, duration } = normalizePinchParam(param, await this.size());
|
|
729
|
+
const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(centerX, centerY);
|
|
730
|
+
const ratio = 0 !== adjCenterX && 0 !== centerX ? adjCenterX / centerX : 1;
|
|
731
|
+
const adjStartDist = Math.round(startDistance * ratio);
|
|
732
|
+
const adjEndDist = Math.round(endDistance * ratio);
|
|
733
|
+
await this.ensureYadb();
|
|
734
|
+
const adb = await this.getAdb();
|
|
735
|
+
await adb.shell(`app_process${this.getDisplayArg()} -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -pinch ${adjCenterX} ${adjCenterY} ${adjStartDist} ${adjEndDist} ${duration}`);
|
|
736
|
+
}),
|
|
694
737
|
defineActionClearInput(async (param)=>{
|
|
695
738
|
await this.clearInput(param.locate);
|
|
696
739
|
})
|
|
@@ -835,66 +878,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
835
878
|
async execYadb(keyboardContent) {
|
|
836
879
|
await this.ensureYadb();
|
|
837
880
|
const adb = await this.getAdb();
|
|
838
|
-
|
|
839
|
-
const command = `app_process${this.getDisplayArg()} -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard "${keyboardContent}"`;
|
|
840
|
-
debugDevice(`Executing YADB input: "${keyboardContent}"`);
|
|
841
|
-
const inputPromise = adb.shell(command);
|
|
842
|
-
const timeoutPromise = new Promise((_, reject)=>setTimeout(()=>reject(new Error('YADB timeout')), 1000));
|
|
843
|
-
await Promise.race([
|
|
844
|
-
inputPromise,
|
|
845
|
-
timeoutPromise
|
|
846
|
-
]);
|
|
847
|
-
debugDevice(`YADB input completed: "${keyboardContent}"`);
|
|
848
|
-
} catch (error) {
|
|
849
|
-
const isAccessibilityConflict = error?.cause?.stderr?.includes('UiAutomationService') || error?.cause?.stderr?.includes('already registered') || error?.message?.includes('UiAutomationService');
|
|
850
|
-
if (isAccessibilityConflict) {
|
|
851
|
-
debugDevice("YADB failed due to AccessibilityService conflict (likely Appium running), falling back to clipboard method");
|
|
852
|
-
await this.inputViaClipboard(keyboardContent);
|
|
853
|
-
} else 'YADB timeout' === error.message ? debugDevice(`YADB timed out after 2s, assuming input succeeded: "${keyboardContent}"`) : debugDevice(`YADB execution may have completed despite error: ${error}`);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
async inputViaClipboard(text) {
|
|
857
|
-
const adb = await this.getAdb();
|
|
858
|
-
try {
|
|
859
|
-
debugDevice(`Inputting via clipboard: "${text}"`);
|
|
860
|
-
const escapedText = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
861
|
-
const setClipboardCmd = `
|
|
862
|
-
content insert --uri content://settings/system --bind name:s:clipboard_text --bind value:s:"${escapedText}"
|
|
863
|
-
`;
|
|
864
|
-
await adb.shell(setClipboardCmd);
|
|
865
|
-
await sleep(100);
|
|
866
|
-
await adb.shell('input keyevent KEYCODE_PASTE');
|
|
867
|
-
await sleep(100);
|
|
868
|
-
debugDevice(`Clipboard input completed via content provider: "${text}"`);
|
|
869
|
-
} catch (error1) {
|
|
870
|
-
debugDevice(`Content provider clipboard failed, trying clipper app: ${error1}`);
|
|
871
|
-
try {
|
|
872
|
-
const base64Text = Buffer.from(text, 'utf-8').toString('base64');
|
|
873
|
-
await adb.shell(`am broadcast -a clipper.set -e text "${base64Text}"`);
|
|
874
|
-
await sleep(100);
|
|
875
|
-
await adb.shell('input keyevent KEYCODE_PASTE');
|
|
876
|
-
await sleep(100);
|
|
877
|
-
debugDevice(`Clipboard input completed via clipper: "${text}"`);
|
|
878
|
-
} catch (error2) {
|
|
879
|
-
debugDevice(`All clipboard methods failed: ${error2}`);
|
|
880
|
-
const isPureAscii = /^[\x00-\x7F]*$/.test(text);
|
|
881
|
-
if (isPureAscii) {
|
|
882
|
-
debugDevice(`Using ADB inputText for ASCII text: "${text}"`);
|
|
883
|
-
await adb.inputText(text);
|
|
884
|
-
} else await this.inputCharByChar(text);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
async inputCharByChar(text) {
|
|
889
|
-
const adb = await this.getAdb();
|
|
890
|
-
debugDevice(`Inputting character by character (slow method): "${text}"`);
|
|
891
|
-
const chars = Array.from(text);
|
|
892
|
-
for (const char of chars){
|
|
893
|
-
if (' ' === char) await adb.shell('input keyevent KEYCODE_SPACE');
|
|
894
|
-
else await adb.shell(`input text "${char}"`);
|
|
895
|
-
await sleep(50);
|
|
896
|
-
}
|
|
897
|
-
debugDevice("Character-by-character input completed");
|
|
881
|
+
await adb.shell(`app_process${this.getDisplayArg()} -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard '${keyboardContent}'`);
|
|
898
882
|
}
|
|
899
883
|
async getElementsInfo() {
|
|
900
884
|
return [];
|
|
@@ -907,6 +891,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
907
891
|
}
|
|
908
892
|
async getScreenSize() {
|
|
909
893
|
const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
|
|
894
|
+
debugDevice(`getScreenSize: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedSize=${!!this.cachedScreenSize}`);
|
|
910
895
|
if (shouldCache && this.cachedScreenSize) return this.cachedScreenSize;
|
|
911
896
|
const adb = await this.getAdb();
|
|
912
897
|
if ('number' == typeof this.options?.displayId) try {
|
|
@@ -1040,6 +1025,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1040
1025
|
}
|
|
1041
1026
|
async getDisplayOrientation() {
|
|
1042
1027
|
const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
|
|
1028
|
+
debugDevice(`getDisplayOrientation: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedOrientation=${null !== this.cachedOrientation}`);
|
|
1043
1029
|
if (shouldCache && null !== this.cachedOrientation) return this.cachedOrientation;
|
|
1044
1030
|
const adb = await this.getAdb();
|
|
1045
1031
|
let orientation = 0;
|
|
@@ -1065,6 +1051,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1065
1051
|
if (shouldCache) this.cachedOrientation = orientation;
|
|
1066
1052
|
return orientation;
|
|
1067
1053
|
}
|
|
1054
|
+
async getOrientedPhysicalSize() {
|
|
1055
|
+
const info = await this.getDevicePhysicalInfo();
|
|
1056
|
+
const isLandscape = 1 === info.orientation || 3 === info.orientation;
|
|
1057
|
+
const shouldSwap = true !== info.isCurrentOrientation && isLandscape;
|
|
1058
|
+
return {
|
|
1059
|
+
width: shouldSwap ? info.physicalHeight : info.physicalWidth,
|
|
1060
|
+
height: shouldSwap ? info.physicalWidth : info.physicalHeight
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1068
1063
|
async size() {
|
|
1069
1064
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1070
1065
|
const adapter = this.getScrcpyAdapter();
|
|
@@ -1091,7 +1086,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1091
1086
|
height: logicalHeight
|
|
1092
1087
|
};
|
|
1093
1088
|
}
|
|
1094
|
-
async cacheFeatureForPoint(center
|
|
1089
|
+
async cacheFeatureForPoint(center) {
|
|
1095
1090
|
const { width, height } = await this.size();
|
|
1096
1091
|
debugDevice('cacheFeatureForPoint: center=[%s,%s], screen=[%s,%s]', center[0], center[1], width, height);
|
|
1097
1092
|
return {
|
|
@@ -1171,14 +1166,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1171
1166
|
const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
|
|
1172
1167
|
const useShellScreencap = 'number' == typeof this.options?.displayId;
|
|
1173
1168
|
try {
|
|
1174
|
-
if (useShellScreencap
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1169
|
+
if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
|
|
1170
|
+
debugDevice('Taking screenshot via adb.takeScreenshot');
|
|
1171
|
+
screenshotBuffer = await adb.takeScreenshot(null);
|
|
1172
|
+
debugDevice('adb.takeScreenshot completed');
|
|
1173
|
+
if (!screenshotBuffer) {
|
|
1174
|
+
this.takeScreenshotFailCount++;
|
|
1175
|
+
throw new Error('Failed to capture screenshot: screenshotBuffer is null');
|
|
1176
|
+
}
|
|
1177
|
+
if (!isValidImageBuffer(screenshotBuffer)) {
|
|
1178
|
+
debugDevice('Invalid image buffer detected: not a valid image format');
|
|
1179
|
+
this.takeScreenshotFailCount++;
|
|
1180
|
+
throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
|
|
1181
|
+
}
|
|
1182
|
+
this.takeScreenshotFailCount = 0;
|
|
1183
|
+
} else {
|
|
1184
|
+
if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
|
|
1185
|
+
throw new Error('Using shell screencap directly');
|
|
1182
1186
|
}
|
|
1183
1187
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1184
1188
|
if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
|
|
@@ -1207,12 +1211,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1207
1211
|
screenshotBuffer = await external_node_fs_["default"].promises.readFile(screenshotPath);
|
|
1208
1212
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1209
1213
|
if (!screenshotBuffer || validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) throw new Error(`Fallback screenshot validation failed: buffer size ${screenshotBuffer?.length || 0} bytes (minimum: ${validScreenshotBufferSize})`);
|
|
1210
|
-
if (!
|
|
1214
|
+
if (!isValidImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1211
1215
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1212
1216
|
} finally{
|
|
1213
|
-
|
|
1214
|
-
|
|
1217
|
+
const adbPath = adb.executable?.path ?? 'adb';
|
|
1218
|
+
const child = execFile(adbPath, [
|
|
1219
|
+
'-s',
|
|
1220
|
+
this.deviceId,
|
|
1221
|
+
'shell',
|
|
1222
|
+
`rm ${androidScreenshotPath}`
|
|
1223
|
+
], {
|
|
1224
|
+
timeout: 3000
|
|
1225
|
+
}, (err)=>{
|
|
1226
|
+
if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
|
|
1215
1227
|
});
|
|
1228
|
+
child.unref();
|
|
1216
1229
|
}
|
|
1217
1230
|
}
|
|
1218
1231
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
|
|
@@ -1375,24 +1388,32 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1375
1388
|
if (!this.yadbPushed) {
|
|
1376
1389
|
const adb = await this.getAdb();
|
|
1377
1390
|
const androidPkgJson = (0, external_node_module_.createRequire)(import.meta.url).resolve('@aiscene/android/package.json');
|
|
1378
|
-
const
|
|
1379
|
-
await adb.push(
|
|
1391
|
+
const yadbBin = external_node_path_["default"].join(external_node_path_["default"].dirname(androidPkgJson), 'bin', 'yadb');
|
|
1392
|
+
await adb.push(yadbBin, '/data/local/tmp');
|
|
1380
1393
|
this.yadbPushed = true;
|
|
1381
1394
|
}
|
|
1382
1395
|
}
|
|
1383
1396
|
shouldUseYadbForText(text) {
|
|
1384
1397
|
const hasNonAscii = /[\x80-\uFFFF]/.test(text);
|
|
1385
1398
|
const hasFormatSpecifiers = /%[a-zA-Z]/.test(text);
|
|
1386
|
-
|
|
1399
|
+
const hasShellSpecialChars = /[\\`$]/.test(text);
|
|
1400
|
+
const hasBothQuotes = text.includes('"') && text.includes("'");
|
|
1401
|
+
return hasNonAscii || hasFormatSpecifiers || hasShellSpecialChars || hasBothQuotes;
|
|
1387
1402
|
}
|
|
1388
1403
|
async keyboardType(text, options) {
|
|
1389
1404
|
if (!text) return;
|
|
1390
1405
|
const adb = await this.getAdb();
|
|
1391
|
-
const shouldUseYadb = this.shouldUseYadbForText(text);
|
|
1392
1406
|
const IME_STRATEGY = (this.options?.imeStrategy || globalConfigManager.getEnvConfigValue(MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
|
|
1393
1407
|
const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
|
|
1394
|
-
|
|
1395
|
-
|
|
1408
|
+
const useYadb = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && this.shouldUseYadbForText(text);
|
|
1409
|
+
if (useYadb) await this.execYadb(escapeForShell(text));
|
|
1410
|
+
else {
|
|
1411
|
+
const segments = text.split('\n');
|
|
1412
|
+
for(let i = 0; i < segments.length; i++){
|
|
1413
|
+
if (segments[i].length > 0) await adb.inputText(segments[i]);
|
|
1414
|
+
if (i < segments.length - 1) await adb.keyevent(66);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1396
1417
|
if (true === shouldAutoDismissKeyboard) await this.hideKeyboard(options);
|
|
1397
1418
|
}
|
|
1398
1419
|
normalizeKeyName(key) {
|
|
@@ -1440,12 +1461,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1440
1461
|
}
|
|
1441
1462
|
async mouseClick(x, y) {
|
|
1442
1463
|
const adb = await this.getAdb();
|
|
1443
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1464
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1444
1465
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} 150`);
|
|
1445
1466
|
}
|
|
1446
1467
|
async mouseDoubleClick(x, y) {
|
|
1447
1468
|
const adb = await this.getAdb();
|
|
1448
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1469
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1449
1470
|
const tapCommand = `input${this.getDisplayArg()} tap ${adjustedX} ${adjustedY}`;
|
|
1450
1471
|
await adb.shell(tapCommand);
|
|
1451
1472
|
await sleep(50);
|
|
@@ -1456,8 +1477,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1456
1477
|
}
|
|
1457
1478
|
async mouseDrag(from, to, duration) {
|
|
1458
1479
|
const adb = await this.getAdb();
|
|
1459
|
-
const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
|
|
1460
|
-
const { x: toX, y: toY } = this.adjustCoordinates(to.x, to.y);
|
|
1480
|
+
const { x: fromX, y: fromY } = await this.adjustCoordinates(from.x, from.y);
|
|
1481
|
+
const { x: toX, y: toY } = await this.adjustCoordinates(to.x, to.y);
|
|
1461
1482
|
const swipeDuration = duration ?? defaultNormalScrollDuration;
|
|
1462
1483
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
|
|
1463
1484
|
}
|
|
@@ -1467,16 +1488,16 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1467
1488
|
const n = 4;
|
|
1468
1489
|
const startX = Math.round(deltaX < 0 ? width / n * (n - 1) : width / n);
|
|
1469
1490
|
const startY = Math.round(deltaY < 0 ? height / n * (n - 1) : height / n);
|
|
1470
|
-
const
|
|
1471
|
-
const
|
|
1472
|
-
const
|
|
1473
|
-
const
|
|
1491
|
+
const maxPositiveDeltaX = startX;
|
|
1492
|
+
const maxNegativeDeltaX = width - startX;
|
|
1493
|
+
const maxPositiveDeltaY = startY;
|
|
1494
|
+
const maxNegativeDeltaY = height - startY;
|
|
1474
1495
|
deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
|
|
1475
1496
|
deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
|
|
1476
1497
|
const endX = Math.round(startX - deltaX);
|
|
1477
1498
|
const endY = Math.round(startY - deltaY);
|
|
1478
|
-
const { x: adjustedStartX, y: adjustedStartY } = this.adjustCoordinates(startX, startY);
|
|
1479
|
-
const { x: adjustedEndX, y: adjustedEndY } = this.adjustCoordinates(endX, endY);
|
|
1499
|
+
const { x: adjustedStartX, y: adjustedStartY } = await this.adjustCoordinates(startX, startY);
|
|
1500
|
+
const { x: adjustedEndX, y: adjustedEndY } = await this.adjustCoordinates(endX, endY);
|
|
1480
1501
|
const adb = await this.getAdb();
|
|
1481
1502
|
const swipeDuration = duration ?? defaultNormalScrollDuration;
|
|
1482
1503
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedStartX} ${adjustedStartY} ${adjustedEndX} ${adjustedEndY} ${swipeDuration}`);
|
|
@@ -1487,6 +1508,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1487
1508
|
this.cachedPhysicalDisplayId = void 0;
|
|
1488
1509
|
this.cachedScreenSize = null;
|
|
1489
1510
|
this.cachedOrientation = null;
|
|
1511
|
+
this.scalingRatio = 1;
|
|
1490
1512
|
if (this.scrcpyAdapter) {
|
|
1491
1513
|
await this.scrcpyAdapter.disconnect();
|
|
1492
1514
|
this.scrcpyAdapter = null;
|
|
@@ -1526,7 +1548,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1526
1548
|
}
|
|
1527
1549
|
async longPress(x, y, duration = 2000) {
|
|
1528
1550
|
const adb = await this.getAdb();
|
|
1529
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1551
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1530
1552
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
|
|
1531
1553
|
}
|
|
1532
1554
|
async pullDown(startPoint, distance, duration = 800) {
|
|
@@ -1548,8 +1570,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1548
1570
|
}
|
|
1549
1571
|
async pullDrag(from, to, duration) {
|
|
1550
1572
|
const adb = await this.getAdb();
|
|
1551
|
-
const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
|
|
1552
|
-
const { x: toX, y: toY } = this.adjustCoordinates(to.x, to.y);
|
|
1573
|
+
const { x: fromX, y: fromY } = await this.adjustCoordinates(from.x, from.y);
|
|
1574
|
+
const { x: toX, y: toY } = await this.adjustCoordinates(to.x, to.y);
|
|
1553
1575
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
|
|
1554
1576
|
}
|
|
1555
1577
|
async pullUp(startPoint, distance, duration = 600) {
|
|
@@ -1636,7 +1658,6 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1636
1658
|
device_define_property(this, "yadbPushed", false);
|
|
1637
1659
|
device_define_property(this, "devicePixelRatio", 1);
|
|
1638
1660
|
device_define_property(this, "devicePixelRatioInitialized", false);
|
|
1639
|
-
device_define_property(this, "scalingRatio", 1);
|
|
1640
1661
|
device_define_property(this, "adb", null);
|
|
1641
1662
|
device_define_property(this, "connectingAdb", null);
|
|
1642
1663
|
device_define_property(this, "destroyed", false);
|
|
@@ -1647,6 +1668,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1647
1668
|
device_define_property(this, "cachedPhysicalDisplayId", void 0);
|
|
1648
1669
|
device_define_property(this, "scrcpyAdapter", null);
|
|
1649
1670
|
device_define_property(this, "appNameMapping", {});
|
|
1671
|
+
device_define_property(this, "scalingRatio", 1);
|
|
1672
|
+
device_define_property(this, "takeScreenshotFailCount", 0);
|
|
1650
1673
|
device_define_property(this, "interfaceType", 'android');
|
|
1651
1674
|
device_define_property(this, "uri", void 0);
|
|
1652
1675
|
device_define_property(this, "options", void 0);
|
|
@@ -1656,6 +1679,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1656
1679
|
this.customActions = options?.customActions;
|
|
1657
1680
|
}
|
|
1658
1681
|
}
|
|
1682
|
+
device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
|
|
1659
1683
|
const runAdbShellParamSchema = z.object({
|
|
1660
1684
|
command: z.string().describe('ADB shell command to execute')
|
|
1661
1685
|
});
|
|
@@ -1668,6 +1692,9 @@ const createPlatformActions = (device)=>({
|
|
|
1668
1692
|
description: 'Execute ADB shell command on Android device',
|
|
1669
1693
|
interfaceAlias: 'runAdbShell',
|
|
1670
1694
|
paramSchema: runAdbShellParamSchema,
|
|
1695
|
+
sample: {
|
|
1696
|
+
command: 'dumpsys window displays | grep -E "mCurrentFocus"'
|
|
1697
|
+
},
|
|
1671
1698
|
call: async (param)=>{
|
|
1672
1699
|
if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
|
|
1673
1700
|
const adb = await device.getAdb();
|
|
@@ -1679,6 +1706,9 @@ const createPlatformActions = (device)=>({
|
|
|
1679
1706
|
description: 'Launch an Android app or URL',
|
|
1680
1707
|
interfaceAlias: 'launch',
|
|
1681
1708
|
paramSchema: launchParamSchema,
|
|
1709
|
+
sample: {
|
|
1710
|
+
uri: 'com.example.app'
|
|
1711
|
+
},
|
|
1682
1712
|
call: async (param)=>{
|
|
1683
1713
|
if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
|
|
1684
1714
|
await device.launch(param.uri);
|