@aiscene/android 1.6.6 → 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 -90
- package/dist/es/index.mjs +124 -89
- package/dist/es/mcp-server.mjs +125 -90
- package/dist/lib/cli.js +123 -88
- package/dist/lib/index.js +122 -87
- package/dist/lib/mcp-server.js +123 -88
- package/dist/types/index.d.ts +40 -31
- package/dist/types/mcp-server.d.ts +40 -31
- package/package.json +24 -21
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present Bytedance, Inc. and its affiliates.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/es/cli.mjs
CHANGED
|
@@ -8,10 +8,11 @@ import { BaseMidsceneTools } from "@midscene/shared/mcp";
|
|
|
8
8
|
import { Agent } from "@midscene/core/agent";
|
|
9
9
|
import { mergeAndNormalizeAppNameMapping, normalizeForComparison, repeat } from "@midscene/shared/utils";
|
|
10
10
|
import node_assert from "node:assert";
|
|
11
|
-
import {
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { defineAction, defineActionClearInput, defineActionCursorMove, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionPinch, defineActionScroll, defineActionSwipe, defineActionTap, normalizeMobileSwipeParam, normalizePinchParam } from "@midscene/core/device";
|
|
12
13
|
import { getTmpFile, sleep } from "@midscene/core/utils";
|
|
13
14
|
import { MIDSCENE_ADB_PATH, MIDSCENE_ADB_REMOTE_HOST, MIDSCENE_ADB_REMOTE_PORT, MIDSCENE_ANDROID_IME_STRATEGY, globalConfigManager } from "@midscene/shared/env";
|
|
14
|
-
import { createImgBase64ByFormat,
|
|
15
|
+
import { createImgBase64ByFormat, isValidImageBuffer } from "@midscene/shared/img";
|
|
15
16
|
import { ADB } from "appium-adb";
|
|
16
17
|
var __webpack_modules__ = {
|
|
17
18
|
"./src/scrcpy-manager.ts" (__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
@@ -675,6 +676,9 @@ const defaultNormalScrollDuration = 1000;
|
|
|
675
676
|
const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
|
|
676
677
|
const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
|
|
677
678
|
const debugDevice = (0, logger_.getDebug)('android:device');
|
|
679
|
+
function escapeForShell(text) {
|
|
680
|
+
return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
|
|
681
|
+
}
|
|
678
682
|
class AndroidDevice {
|
|
679
683
|
actionSpace() {
|
|
680
684
|
const defaultActions = [
|
|
@@ -702,6 +706,12 @@ class AndroidDevice {
|
|
|
702
706
|
]).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.')),
|
|
703
707
|
locate: getMidsceneLocationSchema().describe('The input field to be filled').optional()
|
|
704
708
|
}),
|
|
709
|
+
sample: {
|
|
710
|
+
value: 'test@example.com',
|
|
711
|
+
locate: {
|
|
712
|
+
prompt: 'the email input field'
|
|
713
|
+
}
|
|
714
|
+
},
|
|
705
715
|
call: async (param)=>{
|
|
706
716
|
const element = param.locate;
|
|
707
717
|
if ('typeOnly' !== param.mode) await this.clearInput(element);
|
|
@@ -747,9 +757,21 @@ class AndroidDevice {
|
|
|
747
757
|
y: to.center[1]
|
|
748
758
|
});
|
|
749
759
|
}),
|
|
760
|
+
defineActionSwipe(async (param)=>{
|
|
761
|
+
const { startPoint, endPoint, duration, repeatCount } = normalizeMobileSwipeParam(param, await this.size());
|
|
762
|
+
for(let i = 0; i < repeatCount; i++)await this.mouseDrag(startPoint, endPoint, duration);
|
|
763
|
+
}),
|
|
750
764
|
defineActionKeyboardPress(async (param)=>{
|
|
751
765
|
await this.keyboardPress(param.keyName);
|
|
752
766
|
}),
|
|
767
|
+
defineActionCursorMove(async (param)=>{
|
|
768
|
+
const arrowKey = 'left' === param.direction ? 'ArrowLeft' : 'ArrowRight';
|
|
769
|
+
const times = param.times ?? 1;
|
|
770
|
+
for(let i = 0; i < times; i++){
|
|
771
|
+
await this.keyboardPress(arrowKey);
|
|
772
|
+
await sleep(100);
|
|
773
|
+
}
|
|
774
|
+
}),
|
|
753
775
|
defineAction({
|
|
754
776
|
name: 'LongPress',
|
|
755
777
|
description: 'Trigger a long press on the screen at specified element',
|
|
@@ -757,6 +779,11 @@ class AndroidDevice {
|
|
|
757
779
|
duration: z.number().optional().describe('The duration of the long press in milliseconds'),
|
|
758
780
|
locate: getMidsceneLocationSchema().describe('The element to be long pressed')
|
|
759
781
|
}),
|
|
782
|
+
sample: {
|
|
783
|
+
locate: {
|
|
784
|
+
prompt: 'the message bubble'
|
|
785
|
+
}
|
|
786
|
+
},
|
|
760
787
|
call: async (param)=>{
|
|
761
788
|
const element = param.locate;
|
|
762
789
|
if (!element) throw new Error('LongPress requires an element to be located');
|
|
@@ -776,6 +803,12 @@ class AndroidDevice {
|
|
|
776
803
|
duration: z.number().optional().describe('The duration of the pull (in milliseconds)'),
|
|
777
804
|
locate: getMidsceneLocationSchema().optional().describe('The element to start the pull from (optional)')
|
|
778
805
|
}),
|
|
806
|
+
sample: {
|
|
807
|
+
direction: 'down',
|
|
808
|
+
locate: {
|
|
809
|
+
prompt: 'the center of the content list area'
|
|
810
|
+
}
|
|
811
|
+
},
|
|
779
812
|
call: async (param)=>{
|
|
780
813
|
const element = param.locate;
|
|
781
814
|
const startPoint = element ? {
|
|
@@ -788,6 +821,16 @@ class AndroidDevice {
|
|
|
788
821
|
else throw new Error(`Unknown pull direction: ${param.direction}`);
|
|
789
822
|
}
|
|
790
823
|
}),
|
|
824
|
+
defineActionPinch(async (param)=>{
|
|
825
|
+
const { centerX, centerY, startDistance, endDistance, duration } = normalizePinchParam(param, await this.size());
|
|
826
|
+
const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(centerX, centerY);
|
|
827
|
+
const ratio = 0 !== adjCenterX && 0 !== centerX ? adjCenterX / centerX : 1;
|
|
828
|
+
const adjStartDist = Math.round(startDistance * ratio);
|
|
829
|
+
const adjEndDist = Math.round(endDistance * ratio);
|
|
830
|
+
await this.ensureYadb();
|
|
831
|
+
const adb = await this.getAdb();
|
|
832
|
+
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}`);
|
|
833
|
+
}),
|
|
791
834
|
defineActionClearInput(async (param)=>{
|
|
792
835
|
await this.clearInput(param.locate);
|
|
793
836
|
})
|
|
@@ -932,61 +975,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
932
975
|
async execYadb(keyboardContent) {
|
|
933
976
|
await this.ensureYadb();
|
|
934
977
|
const adb = await this.getAdb();
|
|
935
|
-
|
|
936
|
-
const command = `app_process${this.getDisplayArg()} -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard "${keyboardContent}"`;
|
|
937
|
-
debugDevice(`Executing YADB input: "${keyboardContent}"`);
|
|
938
|
-
await adb.shell(command);
|
|
939
|
-
debugDevice(`YADB input completed: "${keyboardContent}"`);
|
|
940
|
-
} catch (error) {
|
|
941
|
-
const isAccessibilityConflict = error?.cause?.stderr?.includes('UiAutomationService') || error?.cause?.stderr?.includes('already registered') || error?.message?.includes('UiAutomationService');
|
|
942
|
-
if (isAccessibilityConflict) {
|
|
943
|
-
debugDevice("YADB failed due to AccessibilityService conflict (likely Appium running), falling back to clipboard method");
|
|
944
|
-
await this.inputViaClipboard(keyboardContent);
|
|
945
|
-
} else debugDevice(`YADB execution may have completed despite error: ${error}`);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
async inputViaClipboard(text) {
|
|
949
|
-
const adb = await this.getAdb();
|
|
950
|
-
try {
|
|
951
|
-
debugDevice(`Inputting via clipboard: "${text}"`);
|
|
952
|
-
const escapedText = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
953
|
-
const setClipboardCmd = `
|
|
954
|
-
content insert --uri content://settings/system --bind name:s:clipboard_text --bind value:s:"${escapedText}"
|
|
955
|
-
`;
|
|
956
|
-
await adb.shell(setClipboardCmd);
|
|
957
|
-
await sleep(100);
|
|
958
|
-
await adb.shell('input keyevent KEYCODE_PASTE');
|
|
959
|
-
await sleep(100);
|
|
960
|
-
debugDevice(`Clipboard input completed via content provider: "${text}"`);
|
|
961
|
-
} catch (error1) {
|
|
962
|
-
debugDevice(`Content provider clipboard failed, trying clipper app: ${error1}`);
|
|
963
|
-
try {
|
|
964
|
-
const base64Text = Buffer.from(text, 'utf-8').toString('base64');
|
|
965
|
-
await adb.shell(`am broadcast -a clipper.set -e text "${base64Text}"`);
|
|
966
|
-
await sleep(100);
|
|
967
|
-
await adb.shell('input keyevent KEYCODE_PASTE');
|
|
968
|
-
await sleep(100);
|
|
969
|
-
debugDevice(`Clipboard input completed via clipper: "${text}"`);
|
|
970
|
-
} catch (error2) {
|
|
971
|
-
debugDevice(`All clipboard methods failed: ${error2}`);
|
|
972
|
-
const isPureAscii = /^[\x00-\x7F]*$/.test(text);
|
|
973
|
-
if (isPureAscii) {
|
|
974
|
-
debugDevice(`Using ADB inputText for ASCII text: "${text}"`);
|
|
975
|
-
await adb.inputText(text);
|
|
976
|
-
} else await this.inputCharByChar(text);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
async inputCharByChar(text) {
|
|
981
|
-
const adb = await this.getAdb();
|
|
982
|
-
debugDevice(`Inputting character by character (slow method): "${text}"`);
|
|
983
|
-
const chars = Array.from(text);
|
|
984
|
-
for (const char of chars){
|
|
985
|
-
if (' ' === char) await adb.shell('input keyevent KEYCODE_SPACE');
|
|
986
|
-
else await adb.shell(`input text "${char}"`);
|
|
987
|
-
await sleep(50);
|
|
988
|
-
}
|
|
989
|
-
debugDevice("Character-by-character input completed");
|
|
978
|
+
await adb.shell(`app_process${this.getDisplayArg()} -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard '${keyboardContent}'`);
|
|
990
979
|
}
|
|
991
980
|
async getElementsInfo() {
|
|
992
981
|
return [];
|
|
@@ -999,6 +988,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
999
988
|
}
|
|
1000
989
|
async getScreenSize() {
|
|
1001
990
|
const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
|
|
991
|
+
debugDevice(`getScreenSize: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedSize=${!!this.cachedScreenSize}`);
|
|
1002
992
|
if (shouldCache && this.cachedScreenSize) return this.cachedScreenSize;
|
|
1003
993
|
const adb = await this.getAdb();
|
|
1004
994
|
if ('number' == typeof this.options?.displayId) try {
|
|
@@ -1132,6 +1122,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1132
1122
|
}
|
|
1133
1123
|
async getDisplayOrientation() {
|
|
1134
1124
|
const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
|
|
1125
|
+
debugDevice(`getDisplayOrientation: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedOrientation=${null !== this.cachedOrientation}`);
|
|
1135
1126
|
if (shouldCache && null !== this.cachedOrientation) return this.cachedOrientation;
|
|
1136
1127
|
const adb = await this.getAdb();
|
|
1137
1128
|
let orientation = 0;
|
|
@@ -1157,6 +1148,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1157
1148
|
if (shouldCache) this.cachedOrientation = orientation;
|
|
1158
1149
|
return orientation;
|
|
1159
1150
|
}
|
|
1151
|
+
async getOrientedPhysicalSize() {
|
|
1152
|
+
const info = await this.getDevicePhysicalInfo();
|
|
1153
|
+
const isLandscape = 1 === info.orientation || 3 === info.orientation;
|
|
1154
|
+
const shouldSwap = true !== info.isCurrentOrientation && isLandscape;
|
|
1155
|
+
return {
|
|
1156
|
+
width: shouldSwap ? info.physicalHeight : info.physicalWidth,
|
|
1157
|
+
height: shouldSwap ? info.physicalWidth : info.physicalHeight
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
1160
|
async size() {
|
|
1161
1161
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1162
1162
|
const adapter = this.getScrcpyAdapter();
|
|
@@ -1183,7 +1183,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1183
1183
|
height: logicalHeight
|
|
1184
1184
|
};
|
|
1185
1185
|
}
|
|
1186
|
-
async cacheFeatureForPoint(center
|
|
1186
|
+
async cacheFeatureForPoint(center) {
|
|
1187
1187
|
const { width, height } = await this.size();
|
|
1188
1188
|
debugDevice('cacheFeatureForPoint: center=[%s,%s], screen=[%s,%s]', center[0], center[1], width, height);
|
|
1189
1189
|
return {
|
|
@@ -1263,14 +1263,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1263
1263
|
const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
|
|
1264
1264
|
const useShellScreencap = 'number' == typeof this.options?.displayId;
|
|
1265
1265
|
try {
|
|
1266
|
-
if (useShellScreencap
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1266
|
+
if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
|
|
1267
|
+
debugDevice('Taking screenshot via adb.takeScreenshot');
|
|
1268
|
+
screenshotBuffer = await adb.takeScreenshot(null);
|
|
1269
|
+
debugDevice('adb.takeScreenshot completed');
|
|
1270
|
+
if (!screenshotBuffer) {
|
|
1271
|
+
this.takeScreenshotFailCount++;
|
|
1272
|
+
throw new Error('Failed to capture screenshot: screenshotBuffer is null');
|
|
1273
|
+
}
|
|
1274
|
+
if (!isValidImageBuffer(screenshotBuffer)) {
|
|
1275
|
+
debugDevice('Invalid image buffer detected: not a valid image format');
|
|
1276
|
+
this.takeScreenshotFailCount++;
|
|
1277
|
+
throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
|
|
1278
|
+
}
|
|
1279
|
+
this.takeScreenshotFailCount = 0;
|
|
1280
|
+
} else {
|
|
1281
|
+
if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
|
|
1282
|
+
throw new Error('Using shell screencap directly');
|
|
1274
1283
|
}
|
|
1275
1284
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1276
1285
|
if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
|
|
@@ -1299,12 +1308,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1299
1308
|
screenshotBuffer = await external_node_fs_["default"].promises.readFile(screenshotPath);
|
|
1300
1309
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1301
1310
|
if (!screenshotBuffer || validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) throw new Error(`Fallback screenshot validation failed: buffer size ${screenshotBuffer?.length || 0} bytes (minimum: ${validScreenshotBufferSize})`);
|
|
1302
|
-
if (!
|
|
1311
|
+
if (!isValidImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1303
1312
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1304
1313
|
} finally{
|
|
1305
|
-
|
|
1306
|
-
|
|
1314
|
+
const adbPath = adb.executable?.path ?? 'adb';
|
|
1315
|
+
const child = execFile(adbPath, [
|
|
1316
|
+
'-s',
|
|
1317
|
+
this.deviceId,
|
|
1318
|
+
'shell',
|
|
1319
|
+
`rm ${androidScreenshotPath}`
|
|
1320
|
+
], {
|
|
1321
|
+
timeout: 3000
|
|
1322
|
+
}, (err)=>{
|
|
1323
|
+
if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
|
|
1307
1324
|
});
|
|
1325
|
+
child.unref();
|
|
1308
1326
|
}
|
|
1309
1327
|
}
|
|
1310
1328
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
|
|
@@ -1467,24 +1485,32 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1467
1485
|
if (!this.yadbPushed) {
|
|
1468
1486
|
const adb = await this.getAdb();
|
|
1469
1487
|
const androidPkgJson = (0, external_node_module_.createRequire)(import.meta.url).resolve('@aiscene/android/package.json');
|
|
1470
|
-
const
|
|
1471
|
-
await adb.push(
|
|
1488
|
+
const yadbBin = external_node_path_["default"].join(external_node_path_["default"].dirname(androidPkgJson), 'bin', 'yadb');
|
|
1489
|
+
await adb.push(yadbBin, '/data/local/tmp');
|
|
1472
1490
|
this.yadbPushed = true;
|
|
1473
1491
|
}
|
|
1474
1492
|
}
|
|
1475
1493
|
shouldUseYadbForText(text) {
|
|
1476
1494
|
const hasNonAscii = /[\x80-\uFFFF]/.test(text);
|
|
1477
1495
|
const hasFormatSpecifiers = /%[a-zA-Z]/.test(text);
|
|
1478
|
-
|
|
1496
|
+
const hasShellSpecialChars = /[\\`$]/.test(text);
|
|
1497
|
+
const hasBothQuotes = text.includes('"') && text.includes("'");
|
|
1498
|
+
return hasNonAscii || hasFormatSpecifiers || hasShellSpecialChars || hasBothQuotes;
|
|
1479
1499
|
}
|
|
1480
1500
|
async keyboardType(text, options) {
|
|
1481
1501
|
if (!text) return;
|
|
1482
1502
|
const adb = await this.getAdb();
|
|
1483
|
-
const shouldUseYadb = this.shouldUseYadbForText(text);
|
|
1484
1503
|
const IME_STRATEGY = (this.options?.imeStrategy || globalConfigManager.getEnvConfigValue(MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
|
|
1485
1504
|
const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
|
|
1486
|
-
|
|
1487
|
-
|
|
1505
|
+
const useYadb = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && this.shouldUseYadbForText(text);
|
|
1506
|
+
if (useYadb) await this.execYadb(escapeForShell(text));
|
|
1507
|
+
else {
|
|
1508
|
+
const segments = text.split('\n');
|
|
1509
|
+
for(let i = 0; i < segments.length; i++){
|
|
1510
|
+
if (segments[i].length > 0) await adb.inputText(segments[i]);
|
|
1511
|
+
if (i < segments.length - 1) await adb.keyevent(66);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1488
1514
|
if (true === shouldAutoDismissKeyboard) await this.hideKeyboard(options);
|
|
1489
1515
|
}
|
|
1490
1516
|
normalizeKeyName(key) {
|
|
@@ -1532,12 +1558,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1532
1558
|
}
|
|
1533
1559
|
async mouseClick(x, y) {
|
|
1534
1560
|
const adb = await this.getAdb();
|
|
1535
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1561
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1536
1562
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} 150`);
|
|
1537
1563
|
}
|
|
1538
1564
|
async mouseDoubleClick(x, y) {
|
|
1539
1565
|
const adb = await this.getAdb();
|
|
1540
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1566
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1541
1567
|
const tapCommand = `input${this.getDisplayArg()} tap ${adjustedX} ${adjustedY}`;
|
|
1542
1568
|
await adb.shell(tapCommand);
|
|
1543
1569
|
await sleep(50);
|
|
@@ -1548,8 +1574,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1548
1574
|
}
|
|
1549
1575
|
async mouseDrag(from, to, duration) {
|
|
1550
1576
|
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);
|
|
1577
|
+
const { x: fromX, y: fromY } = await this.adjustCoordinates(from.x, from.y);
|
|
1578
|
+
const { x: toX, y: toY } = await this.adjustCoordinates(to.x, to.y);
|
|
1553
1579
|
const swipeDuration = duration ?? defaultNormalScrollDuration;
|
|
1554
1580
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
|
|
1555
1581
|
}
|
|
@@ -1559,16 +1585,16 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1559
1585
|
const n = 4;
|
|
1560
1586
|
const startX = Math.round(deltaX < 0 ? width / n * (n - 1) : width / n);
|
|
1561
1587
|
const startY = Math.round(deltaY < 0 ? height / n * (n - 1) : height / n);
|
|
1562
|
-
const
|
|
1563
|
-
const
|
|
1564
|
-
const
|
|
1565
|
-
const
|
|
1588
|
+
const maxPositiveDeltaX = startX;
|
|
1589
|
+
const maxNegativeDeltaX = width - startX;
|
|
1590
|
+
const maxPositiveDeltaY = startY;
|
|
1591
|
+
const maxNegativeDeltaY = height - startY;
|
|
1566
1592
|
deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
|
|
1567
1593
|
deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
|
|
1568
1594
|
const endX = Math.round(startX - deltaX);
|
|
1569
1595
|
const endY = Math.round(startY - deltaY);
|
|
1570
|
-
const { x: adjustedStartX, y: adjustedStartY } = this.adjustCoordinates(startX, startY);
|
|
1571
|
-
const { x: adjustedEndX, y: adjustedEndY } = this.adjustCoordinates(endX, endY);
|
|
1596
|
+
const { x: adjustedStartX, y: adjustedStartY } = await this.adjustCoordinates(startX, startY);
|
|
1597
|
+
const { x: adjustedEndX, y: adjustedEndY } = await this.adjustCoordinates(endX, endY);
|
|
1572
1598
|
const adb = await this.getAdb();
|
|
1573
1599
|
const swipeDuration = duration ?? defaultNormalScrollDuration;
|
|
1574
1600
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedStartX} ${adjustedStartY} ${adjustedEndX} ${adjustedEndY} ${swipeDuration}`);
|
|
@@ -1579,6 +1605,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1579
1605
|
this.cachedPhysicalDisplayId = void 0;
|
|
1580
1606
|
this.cachedScreenSize = null;
|
|
1581
1607
|
this.cachedOrientation = null;
|
|
1608
|
+
this.scalingRatio = 1;
|
|
1582
1609
|
if (this.scrcpyAdapter) {
|
|
1583
1610
|
await this.scrcpyAdapter.disconnect();
|
|
1584
1611
|
this.scrcpyAdapter = null;
|
|
@@ -1618,7 +1645,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1618
1645
|
}
|
|
1619
1646
|
async longPress(x, y, duration = 2000) {
|
|
1620
1647
|
const adb = await this.getAdb();
|
|
1621
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1648
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1622
1649
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
|
|
1623
1650
|
}
|
|
1624
1651
|
async pullDown(startPoint, distance, duration = 800) {
|
|
@@ -1640,8 +1667,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1640
1667
|
}
|
|
1641
1668
|
async pullDrag(from, to, duration) {
|
|
1642
1669
|
const adb = await this.getAdb();
|
|
1643
|
-
const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
|
|
1644
|
-
const { x: toX, y: toY } = this.adjustCoordinates(to.x, to.y);
|
|
1670
|
+
const { x: fromX, y: fromY } = await this.adjustCoordinates(from.x, from.y);
|
|
1671
|
+
const { x: toX, y: toY } = await this.adjustCoordinates(to.x, to.y);
|
|
1645
1672
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
|
|
1646
1673
|
}
|
|
1647
1674
|
async pullUp(startPoint, distance, duration = 600) {
|
|
@@ -1728,7 +1755,6 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1728
1755
|
device_define_property(this, "yadbPushed", false);
|
|
1729
1756
|
device_define_property(this, "devicePixelRatio", 1);
|
|
1730
1757
|
device_define_property(this, "devicePixelRatioInitialized", false);
|
|
1731
|
-
device_define_property(this, "scalingRatio", 1);
|
|
1732
1758
|
device_define_property(this, "adb", null);
|
|
1733
1759
|
device_define_property(this, "connectingAdb", null);
|
|
1734
1760
|
device_define_property(this, "destroyed", false);
|
|
@@ -1739,6 +1765,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1739
1765
|
device_define_property(this, "cachedPhysicalDisplayId", void 0);
|
|
1740
1766
|
device_define_property(this, "scrcpyAdapter", null);
|
|
1741
1767
|
device_define_property(this, "appNameMapping", {});
|
|
1768
|
+
device_define_property(this, "scalingRatio", 1);
|
|
1769
|
+
device_define_property(this, "takeScreenshotFailCount", 0);
|
|
1742
1770
|
device_define_property(this, "interfaceType", 'android');
|
|
1743
1771
|
device_define_property(this, "uri", void 0);
|
|
1744
1772
|
device_define_property(this, "options", void 0);
|
|
@@ -1748,6 +1776,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1748
1776
|
this.customActions = options?.customActions;
|
|
1749
1777
|
}
|
|
1750
1778
|
}
|
|
1779
|
+
device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
|
|
1751
1780
|
const runAdbShellParamSchema = z.object({
|
|
1752
1781
|
command: z.string().describe('ADB shell command to execute')
|
|
1753
1782
|
});
|
|
@@ -1760,6 +1789,9 @@ const createPlatformActions = (device)=>({
|
|
|
1760
1789
|
description: 'Execute ADB shell command on Android device',
|
|
1761
1790
|
interfaceAlias: 'runAdbShell',
|
|
1762
1791
|
paramSchema: runAdbShellParamSchema,
|
|
1792
|
+
sample: {
|
|
1793
|
+
command: 'dumpsys window displays | grep -E "mCurrentFocus"'
|
|
1794
|
+
},
|
|
1763
1795
|
call: async (param)=>{
|
|
1764
1796
|
if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
|
|
1765
1797
|
const adb = await device.getAdb();
|
|
@@ -1771,6 +1803,9 @@ const createPlatformActions = (device)=>({
|
|
|
1771
1803
|
description: 'Launch an Android app or URL',
|
|
1772
1804
|
interfaceAlias: 'launch',
|
|
1773
1805
|
paramSchema: launchParamSchema,
|
|
1806
|
+
sample: {
|
|
1807
|
+
uri: 'com.example.app'
|
|
1808
|
+
},
|
|
1774
1809
|
call: async (param)=>{
|
|
1775
1810
|
if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
|
|
1776
1811
|
await device.launch(param.uri);
|
|
@@ -1919,7 +1954,7 @@ class AndroidMidsceneTools extends BaseMidsceneTools {
|
|
|
1919
1954
|
const tools = new AndroidMidsceneTools();
|
|
1920
1955
|
runToolsCLI(tools, 'midscene-android', {
|
|
1921
1956
|
stripPrefix: 'android_',
|
|
1922
|
-
version: "1.6.
|
|
1957
|
+
version: "1.6.8"
|
|
1923
1958
|
}).catch((e)=>{
|
|
1924
1959
|
if (!(e instanceof CLIError)) console.error(e);
|
|
1925
1960
|
process.exit(e instanceof CLIError ? e.exitCode : 1);
|