@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/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,66 +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
|
-
const inputPromise = adb.shell(command);
|
|
939
|
-
const timeoutPromise = new Promise((_, reject)=>setTimeout(()=>reject(new Error('YADB timeout')), 1000));
|
|
940
|
-
await Promise.race([
|
|
941
|
-
inputPromise,
|
|
942
|
-
timeoutPromise
|
|
943
|
-
]);
|
|
944
|
-
debugDevice(`YADB input completed: "${keyboardContent}"`);
|
|
945
|
-
} catch (error) {
|
|
946
|
-
const isAccessibilityConflict = error?.cause?.stderr?.includes('UiAutomationService') || error?.cause?.stderr?.includes('already registered') || error?.message?.includes('UiAutomationService');
|
|
947
|
-
if (isAccessibilityConflict) {
|
|
948
|
-
debugDevice("YADB failed due to AccessibilityService conflict (likely Appium running), falling back to clipboard method");
|
|
949
|
-
await this.inputViaClipboard(keyboardContent);
|
|
950
|
-
} else 'YADB timeout' === error.message ? debugDevice(`YADB timed out after 2s, assuming input succeeded: "${keyboardContent}"`) : debugDevice(`YADB execution may have completed despite error: ${error}`);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
async inputViaClipboard(text) {
|
|
954
|
-
const adb = await this.getAdb();
|
|
955
|
-
try {
|
|
956
|
-
debugDevice(`Inputting via clipboard: "${text}"`);
|
|
957
|
-
const escapedText = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
958
|
-
const setClipboardCmd = `
|
|
959
|
-
content insert --uri content://settings/system --bind name:s:clipboard_text --bind value:s:"${escapedText}"
|
|
960
|
-
`;
|
|
961
|
-
await adb.shell(setClipboardCmd);
|
|
962
|
-
await sleep(100);
|
|
963
|
-
await adb.shell('input keyevent KEYCODE_PASTE');
|
|
964
|
-
await sleep(100);
|
|
965
|
-
debugDevice(`Clipboard input completed via content provider: "${text}"`);
|
|
966
|
-
} catch (error1) {
|
|
967
|
-
debugDevice(`Content provider clipboard failed, trying clipper app: ${error1}`);
|
|
968
|
-
try {
|
|
969
|
-
const base64Text = Buffer.from(text, 'utf-8').toString('base64');
|
|
970
|
-
await adb.shell(`am broadcast -a clipper.set -e text "${base64Text}"`);
|
|
971
|
-
await sleep(100);
|
|
972
|
-
await adb.shell('input keyevent KEYCODE_PASTE');
|
|
973
|
-
await sleep(100);
|
|
974
|
-
debugDevice(`Clipboard input completed via clipper: "${text}"`);
|
|
975
|
-
} catch (error2) {
|
|
976
|
-
debugDevice(`All clipboard methods failed: ${error2}`);
|
|
977
|
-
const isPureAscii = /^[\x00-\x7F]*$/.test(text);
|
|
978
|
-
if (isPureAscii) {
|
|
979
|
-
debugDevice(`Using ADB inputText for ASCII text: "${text}"`);
|
|
980
|
-
await adb.inputText(text);
|
|
981
|
-
} else await this.inputCharByChar(text);
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
async inputCharByChar(text) {
|
|
986
|
-
const adb = await this.getAdb();
|
|
987
|
-
debugDevice(`Inputting character by character (slow method): "${text}"`);
|
|
988
|
-
const chars = Array.from(text);
|
|
989
|
-
for (const char of chars){
|
|
990
|
-
if (' ' === char) await adb.shell('input keyevent KEYCODE_SPACE');
|
|
991
|
-
else await adb.shell(`input text "${char}"`);
|
|
992
|
-
await sleep(50);
|
|
993
|
-
}
|
|
994
|
-
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}'`);
|
|
995
979
|
}
|
|
996
980
|
async getElementsInfo() {
|
|
997
981
|
return [];
|
|
@@ -1004,6 +988,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1004
988
|
}
|
|
1005
989
|
async getScreenSize() {
|
|
1006
990
|
const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
|
|
991
|
+
debugDevice(`getScreenSize: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedSize=${!!this.cachedScreenSize}`);
|
|
1007
992
|
if (shouldCache && this.cachedScreenSize) return this.cachedScreenSize;
|
|
1008
993
|
const adb = await this.getAdb();
|
|
1009
994
|
if ('number' == typeof this.options?.displayId) try {
|
|
@@ -1137,6 +1122,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1137
1122
|
}
|
|
1138
1123
|
async getDisplayOrientation() {
|
|
1139
1124
|
const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
|
|
1125
|
+
debugDevice(`getDisplayOrientation: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedOrientation=${null !== this.cachedOrientation}`);
|
|
1140
1126
|
if (shouldCache && null !== this.cachedOrientation) return this.cachedOrientation;
|
|
1141
1127
|
const adb = await this.getAdb();
|
|
1142
1128
|
let orientation = 0;
|
|
@@ -1162,6 +1148,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1162
1148
|
if (shouldCache) this.cachedOrientation = orientation;
|
|
1163
1149
|
return orientation;
|
|
1164
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
|
+
}
|
|
1165
1160
|
async size() {
|
|
1166
1161
|
const deviceInfo = await this.getDevicePhysicalInfo();
|
|
1167
1162
|
const adapter = this.getScrcpyAdapter();
|
|
@@ -1188,7 +1183,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1188
1183
|
height: logicalHeight
|
|
1189
1184
|
};
|
|
1190
1185
|
}
|
|
1191
|
-
async cacheFeatureForPoint(center
|
|
1186
|
+
async cacheFeatureForPoint(center) {
|
|
1192
1187
|
const { width, height } = await this.size();
|
|
1193
1188
|
debugDevice('cacheFeatureForPoint: center=[%s,%s], screen=[%s,%s]', center[0], center[1], width, height);
|
|
1194
1189
|
return {
|
|
@@ -1268,14 +1263,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1268
1263
|
const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
|
|
1269
1264
|
const useShellScreencap = 'number' == typeof this.options?.displayId;
|
|
1270
1265
|
try {
|
|
1271
|
-
if (useShellScreencap
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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');
|
|
1279
1283
|
}
|
|
1280
1284
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1281
1285
|
if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
|
|
@@ -1304,12 +1308,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1304
1308
|
screenshotBuffer = await external_node_fs_["default"].promises.readFile(screenshotPath);
|
|
1305
1309
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1306
1310
|
if (!screenshotBuffer || validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) throw new Error(`Fallback screenshot validation failed: buffer size ${screenshotBuffer?.length || 0} bytes (minimum: ${validScreenshotBufferSize})`);
|
|
1307
|
-
if (!
|
|
1311
|
+
if (!isValidImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1308
1312
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1309
1313
|
} finally{
|
|
1310
|
-
|
|
1311
|
-
|
|
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);
|
|
1312
1324
|
});
|
|
1325
|
+
child.unref();
|
|
1313
1326
|
}
|
|
1314
1327
|
}
|
|
1315
1328
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
|
|
@@ -1472,24 +1485,32 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1472
1485
|
if (!this.yadbPushed) {
|
|
1473
1486
|
const adb = await this.getAdb();
|
|
1474
1487
|
const androidPkgJson = (0, external_node_module_.createRequire)(import.meta.url).resolve('@aiscene/android/package.json');
|
|
1475
|
-
const
|
|
1476
|
-
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');
|
|
1477
1490
|
this.yadbPushed = true;
|
|
1478
1491
|
}
|
|
1479
1492
|
}
|
|
1480
1493
|
shouldUseYadbForText(text) {
|
|
1481
1494
|
const hasNonAscii = /[\x80-\uFFFF]/.test(text);
|
|
1482
1495
|
const hasFormatSpecifiers = /%[a-zA-Z]/.test(text);
|
|
1483
|
-
|
|
1496
|
+
const hasShellSpecialChars = /[\\`$]/.test(text);
|
|
1497
|
+
const hasBothQuotes = text.includes('"') && text.includes("'");
|
|
1498
|
+
return hasNonAscii || hasFormatSpecifiers || hasShellSpecialChars || hasBothQuotes;
|
|
1484
1499
|
}
|
|
1485
1500
|
async keyboardType(text, options) {
|
|
1486
1501
|
if (!text) return;
|
|
1487
1502
|
const adb = await this.getAdb();
|
|
1488
|
-
const shouldUseYadb = this.shouldUseYadbForText(text);
|
|
1489
1503
|
const IME_STRATEGY = (this.options?.imeStrategy || globalConfigManager.getEnvConfigValue(MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
|
|
1490
1504
|
const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
+
}
|
|
1493
1514
|
if (true === shouldAutoDismissKeyboard) await this.hideKeyboard(options);
|
|
1494
1515
|
}
|
|
1495
1516
|
normalizeKeyName(key) {
|
|
@@ -1537,12 +1558,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1537
1558
|
}
|
|
1538
1559
|
async mouseClick(x, y) {
|
|
1539
1560
|
const adb = await this.getAdb();
|
|
1540
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1561
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1541
1562
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} 150`);
|
|
1542
1563
|
}
|
|
1543
1564
|
async mouseDoubleClick(x, y) {
|
|
1544
1565
|
const adb = await this.getAdb();
|
|
1545
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1566
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1546
1567
|
const tapCommand = `input${this.getDisplayArg()} tap ${adjustedX} ${adjustedY}`;
|
|
1547
1568
|
await adb.shell(tapCommand);
|
|
1548
1569
|
await sleep(50);
|
|
@@ -1553,8 +1574,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1553
1574
|
}
|
|
1554
1575
|
async mouseDrag(from, to, duration) {
|
|
1555
1576
|
const adb = await this.getAdb();
|
|
1556
|
-
const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
|
|
1557
|
-
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);
|
|
1558
1579
|
const swipeDuration = duration ?? defaultNormalScrollDuration;
|
|
1559
1580
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
|
|
1560
1581
|
}
|
|
@@ -1564,16 +1585,16 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1564
1585
|
const n = 4;
|
|
1565
1586
|
const startX = Math.round(deltaX < 0 ? width / n * (n - 1) : width / n);
|
|
1566
1587
|
const startY = Math.round(deltaY < 0 ? height / n * (n - 1) : height / n);
|
|
1567
|
-
const
|
|
1568
|
-
const
|
|
1569
|
-
const
|
|
1570
|
-
const
|
|
1588
|
+
const maxPositiveDeltaX = startX;
|
|
1589
|
+
const maxNegativeDeltaX = width - startX;
|
|
1590
|
+
const maxPositiveDeltaY = startY;
|
|
1591
|
+
const maxNegativeDeltaY = height - startY;
|
|
1571
1592
|
deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
|
|
1572
1593
|
deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
|
|
1573
1594
|
const endX = Math.round(startX - deltaX);
|
|
1574
1595
|
const endY = Math.round(startY - deltaY);
|
|
1575
|
-
const { x: adjustedStartX, y: adjustedStartY } = this.adjustCoordinates(startX, startY);
|
|
1576
|
-
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);
|
|
1577
1598
|
const adb = await this.getAdb();
|
|
1578
1599
|
const swipeDuration = duration ?? defaultNormalScrollDuration;
|
|
1579
1600
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedStartX} ${adjustedStartY} ${adjustedEndX} ${adjustedEndY} ${swipeDuration}`);
|
|
@@ -1584,6 +1605,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1584
1605
|
this.cachedPhysicalDisplayId = void 0;
|
|
1585
1606
|
this.cachedScreenSize = null;
|
|
1586
1607
|
this.cachedOrientation = null;
|
|
1608
|
+
this.scalingRatio = 1;
|
|
1587
1609
|
if (this.scrcpyAdapter) {
|
|
1588
1610
|
await this.scrcpyAdapter.disconnect();
|
|
1589
1611
|
this.scrcpyAdapter = null;
|
|
@@ -1623,7 +1645,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1623
1645
|
}
|
|
1624
1646
|
async longPress(x, y, duration = 2000) {
|
|
1625
1647
|
const adb = await this.getAdb();
|
|
1626
|
-
const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
|
|
1648
|
+
const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
|
|
1627
1649
|
await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
|
|
1628
1650
|
}
|
|
1629
1651
|
async pullDown(startPoint, distance, duration = 800) {
|
|
@@ -1645,8 +1667,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1645
1667
|
}
|
|
1646
1668
|
async pullDrag(from, to, duration) {
|
|
1647
1669
|
const adb = await this.getAdb();
|
|
1648
|
-
const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
|
|
1649
|
-
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);
|
|
1650
1672
|
await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
|
|
1651
1673
|
}
|
|
1652
1674
|
async pullUp(startPoint, distance, duration = 600) {
|
|
@@ -1733,7 +1755,6 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1733
1755
|
device_define_property(this, "yadbPushed", false);
|
|
1734
1756
|
device_define_property(this, "devicePixelRatio", 1);
|
|
1735
1757
|
device_define_property(this, "devicePixelRatioInitialized", false);
|
|
1736
|
-
device_define_property(this, "scalingRatio", 1);
|
|
1737
1758
|
device_define_property(this, "adb", null);
|
|
1738
1759
|
device_define_property(this, "connectingAdb", null);
|
|
1739
1760
|
device_define_property(this, "destroyed", false);
|
|
@@ -1744,6 +1765,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1744
1765
|
device_define_property(this, "cachedPhysicalDisplayId", void 0);
|
|
1745
1766
|
device_define_property(this, "scrcpyAdapter", null);
|
|
1746
1767
|
device_define_property(this, "appNameMapping", {});
|
|
1768
|
+
device_define_property(this, "scalingRatio", 1);
|
|
1769
|
+
device_define_property(this, "takeScreenshotFailCount", 0);
|
|
1747
1770
|
device_define_property(this, "interfaceType", 'android');
|
|
1748
1771
|
device_define_property(this, "uri", void 0);
|
|
1749
1772
|
device_define_property(this, "options", void 0);
|
|
@@ -1753,6 +1776,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1753
1776
|
this.customActions = options?.customActions;
|
|
1754
1777
|
}
|
|
1755
1778
|
}
|
|
1779
|
+
device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
|
|
1756
1780
|
const runAdbShellParamSchema = z.object({
|
|
1757
1781
|
command: z.string().describe('ADB shell command to execute')
|
|
1758
1782
|
});
|
|
@@ -1765,6 +1789,9 @@ const createPlatformActions = (device)=>({
|
|
|
1765
1789
|
description: 'Execute ADB shell command on Android device',
|
|
1766
1790
|
interfaceAlias: 'runAdbShell',
|
|
1767
1791
|
paramSchema: runAdbShellParamSchema,
|
|
1792
|
+
sample: {
|
|
1793
|
+
command: 'dumpsys window displays | grep -E "mCurrentFocus"'
|
|
1794
|
+
},
|
|
1768
1795
|
call: async (param)=>{
|
|
1769
1796
|
if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
|
|
1770
1797
|
const adb = await device.getAdb();
|
|
@@ -1776,6 +1803,9 @@ const createPlatformActions = (device)=>({
|
|
|
1776
1803
|
description: 'Launch an Android app or URL',
|
|
1777
1804
|
interfaceAlias: 'launch',
|
|
1778
1805
|
paramSchema: launchParamSchema,
|
|
1806
|
+
sample: {
|
|
1807
|
+
uri: 'com.example.app'
|
|
1808
|
+
},
|
|
1779
1809
|
call: async (param)=>{
|
|
1780
1810
|
if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
|
|
1781
1811
|
await device.launch(param.uri);
|
|
@@ -1924,7 +1954,7 @@ class AndroidMidsceneTools extends BaseMidsceneTools {
|
|
|
1924
1954
|
const tools = new AndroidMidsceneTools();
|
|
1925
1955
|
runToolsCLI(tools, 'midscene-android', {
|
|
1926
1956
|
stripPrefix: 'android_',
|
|
1927
|
-
version: "1.6.
|
|
1957
|
+
version: "1.6.8"
|
|
1928
1958
|
}).catch((e)=>{
|
|
1929
1959
|
if (!(e instanceof CLIError)) console.error(e);
|
|
1930
1960
|
process.exit(e instanceof CLIError ? e.exitCode : 1);
|