@aiscene/android 1.6.7 → 1.6.9

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.
@@ -564,6 +564,7 @@ var __webpack_exports__ = {};
564
564
  };
565
565
  const external_node_assert_namespaceObject = require("node:assert");
566
566
  var external_node_assert_default = /*#__PURE__*/ __webpack_require__.n(external_node_assert_namespaceObject);
567
+ const external_node_child_process_namespaceObject = require("node:child_process");
567
568
  var external_node_fs_ = __webpack_require__("node:fs");
568
569
  var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_);
569
570
  var external_node_module_ = __webpack_require__("node:module");
@@ -705,6 +706,9 @@ var __webpack_exports__ = {};
705
706
  const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
706
707
  const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
707
708
  const debugDevice = (0, logger_.getDebug)('android:device');
709
+ function escapeForShell(text) {
710
+ return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
711
+ }
708
712
  class AndroidDevice {
709
713
  actionSpace() {
710
714
  const defaultActions = [
@@ -732,6 +736,12 @@ var __webpack_exports__ = {};
732
736
  ]).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.')),
733
737
  locate: (0, core_namespaceObject.getMidsceneLocationSchema)().describe('The input field to be filled').optional()
734
738
  }),
739
+ sample: {
740
+ value: 'test@example.com',
741
+ locate: {
742
+ prompt: 'the email input field'
743
+ }
744
+ },
735
745
  call: async (param)=>{
736
746
  const element = param.locate;
737
747
  if ('typeOnly' !== param.mode) await this.clearInput(element);
@@ -777,9 +787,21 @@ var __webpack_exports__ = {};
777
787
  y: to.center[1]
778
788
  });
779
789
  }),
790
+ (0, device_namespaceObject.defineActionSwipe)(async (param)=>{
791
+ const { startPoint, endPoint, duration, repeatCount } = (0, device_namespaceObject.normalizeMobileSwipeParam)(param, await this.size());
792
+ for(let i = 0; i < repeatCount; i++)await this.mouseDrag(startPoint, endPoint, duration);
793
+ }),
780
794
  (0, device_namespaceObject.defineActionKeyboardPress)(async (param)=>{
781
795
  await this.keyboardPress(param.keyName);
782
796
  }),
797
+ (0, device_namespaceObject.defineActionCursorMove)(async (param)=>{
798
+ const arrowKey = 'left' === param.direction ? 'ArrowLeft' : 'ArrowRight';
799
+ const times = param.times ?? 1;
800
+ for(let i = 0; i < times; i++){
801
+ await this.keyboardPress(arrowKey);
802
+ await (0, core_utils_namespaceObject.sleep)(100);
803
+ }
804
+ }),
783
805
  (0, device_namespaceObject.defineAction)({
784
806
  name: 'LongPress',
785
807
  description: 'Trigger a long press on the screen at specified element',
@@ -787,6 +809,11 @@ var __webpack_exports__ = {};
787
809
  duration: core_namespaceObject.z.number().optional().describe('The duration of the long press in milliseconds'),
788
810
  locate: (0, core_namespaceObject.getMidsceneLocationSchema)().describe('The element to be long pressed')
789
811
  }),
812
+ sample: {
813
+ locate: {
814
+ prompt: 'the message bubble'
815
+ }
816
+ },
790
817
  call: async (param)=>{
791
818
  const element = param.locate;
792
819
  if (!element) throw new Error('LongPress requires an element to be located');
@@ -806,6 +833,12 @@ var __webpack_exports__ = {};
806
833
  duration: core_namespaceObject.z.number().optional().describe('The duration of the pull (in milliseconds)'),
807
834
  locate: (0, core_namespaceObject.getMidsceneLocationSchema)().optional().describe('The element to start the pull from (optional)')
808
835
  }),
836
+ sample: {
837
+ direction: 'down',
838
+ locate: {
839
+ prompt: 'the center of the content list area'
840
+ }
841
+ },
809
842
  call: async (param)=>{
810
843
  const element = param.locate;
811
844
  const startPoint = element ? {
@@ -818,6 +851,16 @@ var __webpack_exports__ = {};
818
851
  else throw new Error(`Unknown pull direction: ${param.direction}`);
819
852
  }
820
853
  }),
854
+ (0, device_namespaceObject.defineActionPinch)(async (param)=>{
855
+ const { centerX, centerY, startDistance, endDistance, duration } = (0, device_namespaceObject.normalizePinchParam)(param, await this.size());
856
+ const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(centerX, centerY);
857
+ const ratio = 0 !== adjCenterX && 0 !== centerX ? adjCenterX / centerX : 1;
858
+ const adjStartDist = Math.round(startDistance * ratio);
859
+ const adjEndDist = Math.round(endDistance * ratio);
860
+ await this.ensureYadb();
861
+ const adb = await this.getAdb();
862
+ 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}`);
863
+ }),
821
864
  (0, device_namespaceObject.defineActionClearInput)(async (param)=>{
822
865
  await this.clearInput(param.locate);
823
866
  })
@@ -962,66 +1005,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
962
1005
  async execYadb(keyboardContent) {
963
1006
  await this.ensureYadb();
964
1007
  const adb = await this.getAdb();
965
- try {
966
- const command = `app_process${this.getDisplayArg()} -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard "${keyboardContent}"`;
967
- debugDevice(`Executing YADB input: "${keyboardContent}"`);
968
- const inputPromise = adb.shell(command);
969
- const timeoutPromise = new Promise((_, reject)=>setTimeout(()=>reject(new Error('YADB timeout')), 1000));
970
- await Promise.race([
971
- inputPromise,
972
- timeoutPromise
973
- ]);
974
- debugDevice(`YADB input completed: "${keyboardContent}"`);
975
- } catch (error) {
976
- const isAccessibilityConflict = error?.cause?.stderr?.includes('UiAutomationService') || error?.cause?.stderr?.includes('already registered') || error?.message?.includes('UiAutomationService');
977
- if (isAccessibilityConflict) {
978
- debugDevice("YADB failed due to AccessibilityService conflict (likely Appium running), falling back to clipboard method");
979
- await this.inputViaClipboard(keyboardContent);
980
- } else 'YADB timeout' === error.message ? debugDevice(`YADB timed out after 2s, assuming input succeeded: "${keyboardContent}"`) : debugDevice(`YADB execution may have completed despite error: ${error}`);
981
- }
982
- }
983
- async inputViaClipboard(text) {
984
- const adb = await this.getAdb();
985
- try {
986
- debugDevice(`Inputting via clipboard: "${text}"`);
987
- const escapedText = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
988
- const setClipboardCmd = `
989
- content insert --uri content://settings/system --bind name:s:clipboard_text --bind value:s:"${escapedText}"
990
- `;
991
- await adb.shell(setClipboardCmd);
992
- await (0, core_utils_namespaceObject.sleep)(100);
993
- await adb.shell('input keyevent KEYCODE_PASTE');
994
- await (0, core_utils_namespaceObject.sleep)(100);
995
- debugDevice(`Clipboard input completed via content provider: "${text}"`);
996
- } catch (error1) {
997
- debugDevice(`Content provider clipboard failed, trying clipper app: ${error1}`);
998
- try {
999
- const base64Text = Buffer.from(text, 'utf-8').toString('base64');
1000
- await adb.shell(`am broadcast -a clipper.set -e text "${base64Text}"`);
1001
- await (0, core_utils_namespaceObject.sleep)(100);
1002
- await adb.shell('input keyevent KEYCODE_PASTE');
1003
- await (0, core_utils_namespaceObject.sleep)(100);
1004
- debugDevice(`Clipboard input completed via clipper: "${text}"`);
1005
- } catch (error2) {
1006
- debugDevice(`All clipboard methods failed: ${error2}`);
1007
- const isPureAscii = /^[\x00-\x7F]*$/.test(text);
1008
- if (isPureAscii) {
1009
- debugDevice(`Using ADB inputText for ASCII text: "${text}"`);
1010
- await adb.inputText(text);
1011
- } else await this.inputCharByChar(text);
1012
- }
1013
- }
1014
- }
1015
- async inputCharByChar(text) {
1016
- const adb = await this.getAdb();
1017
- debugDevice(`Inputting character by character (slow method): "${text}"`);
1018
- const chars = Array.from(text);
1019
- for (const char of chars){
1020
- if (' ' === char) await adb.shell('input keyevent KEYCODE_SPACE');
1021
- else await adb.shell(`input text "${char}"`);
1022
- await (0, core_utils_namespaceObject.sleep)(50);
1023
- }
1024
- debugDevice("Character-by-character input completed");
1008
+ await adb.shell(`app_process${this.getDisplayArg()} -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard '${keyboardContent}'`);
1025
1009
  }
1026
1010
  async getElementsInfo() {
1027
1011
  return [];
@@ -1034,6 +1018,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1034
1018
  }
1035
1019
  async getScreenSize() {
1036
1020
  const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
1021
+ debugDevice(`getScreenSize: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedSize=${!!this.cachedScreenSize}`);
1037
1022
  if (shouldCache && this.cachedScreenSize) return this.cachedScreenSize;
1038
1023
  const adb = await this.getAdb();
1039
1024
  if ('number' == typeof this.options?.displayId) try {
@@ -1167,6 +1152,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1167
1152
  }
1168
1153
  async getDisplayOrientation() {
1169
1154
  const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
1155
+ debugDevice(`getDisplayOrientation: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedOrientation=${null !== this.cachedOrientation}`);
1170
1156
  if (shouldCache && null !== this.cachedOrientation) return this.cachedOrientation;
1171
1157
  const adb = await this.getAdb();
1172
1158
  let orientation = 0;
@@ -1192,6 +1178,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1192
1178
  if (shouldCache) this.cachedOrientation = orientation;
1193
1179
  return orientation;
1194
1180
  }
1181
+ async getOrientedPhysicalSize() {
1182
+ const info = await this.getDevicePhysicalInfo();
1183
+ const isLandscape = 1 === info.orientation || 3 === info.orientation;
1184
+ const shouldSwap = true !== info.isCurrentOrientation && isLandscape;
1185
+ return {
1186
+ width: shouldSwap ? info.physicalHeight : info.physicalWidth,
1187
+ height: shouldSwap ? info.physicalWidth : info.physicalHeight
1188
+ };
1189
+ }
1195
1190
  async size() {
1196
1191
  const deviceInfo = await this.getDevicePhysicalInfo();
1197
1192
  const adapter = this.getScrcpyAdapter();
@@ -1218,7 +1213,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1218
1213
  height: logicalHeight
1219
1214
  };
1220
1215
  }
1221
- async cacheFeatureForPoint(center, options) {
1216
+ async cacheFeatureForPoint(center) {
1222
1217
  const { width, height } = await this.size();
1223
1218
  debugDevice('cacheFeatureForPoint: center=[%s,%s], screen=[%s,%s]', center[0], center[1], width, height);
1224
1219
  return {
@@ -1298,14 +1293,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1298
1293
  const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
1299
1294
  const useShellScreencap = 'number' == typeof this.options?.displayId;
1300
1295
  try {
1301
- if (useShellScreencap) throw new Error('Using shell screencap for displayId');
1302
- debugDevice('Taking screenshot via adb.takeScreenshot');
1303
- screenshotBuffer = await adb.takeScreenshot(null);
1304
- debugDevice('adb.takeScreenshot completed');
1305
- if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1306
- if (!(0, img_namespaceObject.isValidPNGImageBuffer)(screenshotBuffer)) {
1307
- debugDevice('Invalid image buffer detected: not a valid image format');
1308
- throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1296
+ if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
1297
+ debugDevice('Taking screenshot via adb.takeScreenshot');
1298
+ screenshotBuffer = await adb.takeScreenshot(null);
1299
+ debugDevice('adb.takeScreenshot completed');
1300
+ if (!screenshotBuffer) {
1301
+ this.takeScreenshotFailCount++;
1302
+ throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1303
+ }
1304
+ if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) {
1305
+ debugDevice('Invalid image buffer detected: not a valid image format');
1306
+ this.takeScreenshotFailCount++;
1307
+ throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1308
+ }
1309
+ this.takeScreenshotFailCount = 0;
1310
+ } else {
1311
+ if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
1312
+ throw new Error('Using shell screencap directly');
1309
1313
  }
1310
1314
  const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
1311
1315
  if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
@@ -1334,12 +1338,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1334
1338
  screenshotBuffer = await external_node_fs_default().promises.readFile(screenshotPath);
1335
1339
  const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
1336
1340
  if (!screenshotBuffer || validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) throw new Error(`Fallback screenshot validation failed: buffer size ${screenshotBuffer?.length || 0} bytes (minimum: ${validScreenshotBufferSize})`);
1337
- if (!(0, img_namespaceObject.isValidPNGImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
1341
+ if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
1338
1342
  debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
1339
1343
  } finally{
1340
- Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
1341
- debugDevice(`Failed to delete remote screenshot: ${error}`);
1344
+ const adbPath = adb.executable?.path ?? 'adb';
1345
+ const child = (0, external_node_child_process_namespaceObject.execFile)(adbPath, [
1346
+ '-s',
1347
+ this.deviceId,
1348
+ 'shell',
1349
+ `rm ${androidScreenshotPath}`
1350
+ ], {
1351
+ timeout: 3000
1352
+ }, (err)=>{
1353
+ if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
1342
1354
  });
1355
+ child.unref();
1343
1356
  }
1344
1357
  }
1345
1358
  if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
@@ -1502,24 +1515,32 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1502
1515
  if (!this.yadbPushed) {
1503
1516
  const adb = await this.getAdb();
1504
1517
  const androidPkgJson = (0, external_node_module_.createRequire)(__rslib_import_meta_url__).resolve('@aiscene/android/package.json');
1505
- const yadbDir = external_node_path_default().join(external_node_path_default().dirname(androidPkgJson), 'bin');
1506
- await adb.push(yadbDir, '/data/local/tmp');
1518
+ const yadbBin = external_node_path_default().join(external_node_path_default().dirname(androidPkgJson), 'bin', 'yadb');
1519
+ await adb.push(yadbBin, '/data/local/tmp');
1507
1520
  this.yadbPushed = true;
1508
1521
  }
1509
1522
  }
1510
1523
  shouldUseYadbForText(text) {
1511
1524
  const hasNonAscii = /[\x80-\uFFFF]/.test(text);
1512
1525
  const hasFormatSpecifiers = /%[a-zA-Z]/.test(text);
1513
- return hasNonAscii || hasFormatSpecifiers;
1526
+ const hasShellSpecialChars = /[\\`$]/.test(text);
1527
+ const hasBothQuotes = text.includes('"') && text.includes("'");
1528
+ return hasNonAscii || hasFormatSpecifiers || hasShellSpecialChars || hasBothQuotes;
1514
1529
  }
1515
1530
  async keyboardType(text, options) {
1516
1531
  if (!text) return;
1517
1532
  const adb = await this.getAdb();
1518
- const shouldUseYadb = this.shouldUseYadbForText(text);
1519
1533
  const IME_STRATEGY = (this.options?.imeStrategy || env_namespaceObject.globalConfigManager.getEnvConfigValue(env_namespaceObject.MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
1520
1534
  const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
1521
- if (IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && shouldUseYadb) await this.execYadb(text);
1522
- else await adb.inputText(text);
1535
+ const useYadb = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && this.shouldUseYadbForText(text);
1536
+ if (useYadb) await this.execYadb(escapeForShell(text));
1537
+ else {
1538
+ const segments = text.split('\n');
1539
+ for(let i = 0; i < segments.length; i++){
1540
+ if (segments[i].length > 0) await adb.inputText(segments[i]);
1541
+ if (i < segments.length - 1) await adb.keyevent(66);
1542
+ }
1543
+ }
1523
1544
  if (true === shouldAutoDismissKeyboard) await this.hideKeyboard(options);
1524
1545
  }
1525
1546
  normalizeKeyName(key) {
@@ -1567,12 +1588,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1567
1588
  }
1568
1589
  async mouseClick(x, y) {
1569
1590
  const adb = await this.getAdb();
1570
- const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
1591
+ const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
1571
1592
  await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} 150`);
1572
1593
  }
1573
1594
  async mouseDoubleClick(x, y) {
1574
1595
  const adb = await this.getAdb();
1575
- const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
1596
+ const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
1576
1597
  const tapCommand = `input${this.getDisplayArg()} tap ${adjustedX} ${adjustedY}`;
1577
1598
  await adb.shell(tapCommand);
1578
1599
  await (0, core_utils_namespaceObject.sleep)(50);
@@ -1583,8 +1604,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1583
1604
  }
1584
1605
  async mouseDrag(from, to, duration) {
1585
1606
  const adb = await this.getAdb();
1586
- const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
1587
- const { x: toX, y: toY } = this.adjustCoordinates(to.x, to.y);
1607
+ const { x: fromX, y: fromY } = await this.adjustCoordinates(from.x, from.y);
1608
+ const { x: toX, y: toY } = await this.adjustCoordinates(to.x, to.y);
1588
1609
  const swipeDuration = duration ?? defaultNormalScrollDuration;
1589
1610
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
1590
1611
  }
@@ -1594,16 +1615,16 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1594
1615
  const n = 4;
1595
1616
  const startX = Math.round(deltaX < 0 ? width / n * (n - 1) : width / n);
1596
1617
  const startY = Math.round(deltaY < 0 ? height / n * (n - 1) : height / n);
1597
- const maxNegativeDeltaX = startX;
1598
- const maxPositiveDeltaX = Math.round(width / n * (n - 1));
1599
- const maxNegativeDeltaY = startY;
1600
- const maxPositiveDeltaY = Math.round(height / n * (n - 1));
1618
+ const maxPositiveDeltaX = startX;
1619
+ const maxNegativeDeltaX = width - startX;
1620
+ const maxPositiveDeltaY = startY;
1621
+ const maxNegativeDeltaY = height - startY;
1601
1622
  deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
1602
1623
  deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
1603
1624
  const endX = Math.round(startX - deltaX);
1604
1625
  const endY = Math.round(startY - deltaY);
1605
- const { x: adjustedStartX, y: adjustedStartY } = this.adjustCoordinates(startX, startY);
1606
- const { x: adjustedEndX, y: adjustedEndY } = this.adjustCoordinates(endX, endY);
1626
+ const { x: adjustedStartX, y: adjustedStartY } = await this.adjustCoordinates(startX, startY);
1627
+ const { x: adjustedEndX, y: adjustedEndY } = await this.adjustCoordinates(endX, endY);
1607
1628
  const adb = await this.getAdb();
1608
1629
  const swipeDuration = duration ?? defaultNormalScrollDuration;
1609
1630
  await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedStartX} ${adjustedStartY} ${adjustedEndX} ${adjustedEndY} ${swipeDuration}`);
@@ -1614,6 +1635,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1614
1635
  this.cachedPhysicalDisplayId = void 0;
1615
1636
  this.cachedScreenSize = null;
1616
1637
  this.cachedOrientation = null;
1638
+ this.scalingRatio = 1;
1617
1639
  if (this.scrcpyAdapter) {
1618
1640
  await this.scrcpyAdapter.disconnect();
1619
1641
  this.scrcpyAdapter = null;
@@ -1653,7 +1675,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1653
1675
  }
1654
1676
  async longPress(x, y, duration = 2000) {
1655
1677
  const adb = await this.getAdb();
1656
- const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
1678
+ const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
1657
1679
  await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
1658
1680
  }
1659
1681
  async pullDown(startPoint, distance, duration = 800) {
@@ -1675,8 +1697,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1675
1697
  }
1676
1698
  async pullDrag(from, to, duration) {
1677
1699
  const adb = await this.getAdb();
1678
- const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
1679
- const { x: toX, y: toY } = this.adjustCoordinates(to.x, to.y);
1700
+ const { x: fromX, y: fromY } = await this.adjustCoordinates(from.x, from.y);
1701
+ const { x: toX, y: toY } = await this.adjustCoordinates(to.x, to.y);
1680
1702
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
1681
1703
  }
1682
1704
  async pullUp(startPoint, distance, duration = 600) {
@@ -1763,7 +1785,6 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1763
1785
  device_define_property(this, "yadbPushed", false);
1764
1786
  device_define_property(this, "devicePixelRatio", 1);
1765
1787
  device_define_property(this, "devicePixelRatioInitialized", false);
1766
- device_define_property(this, "scalingRatio", 1);
1767
1788
  device_define_property(this, "adb", null);
1768
1789
  device_define_property(this, "connectingAdb", null);
1769
1790
  device_define_property(this, "destroyed", false);
@@ -1774,6 +1795,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1774
1795
  device_define_property(this, "cachedPhysicalDisplayId", void 0);
1775
1796
  device_define_property(this, "scrcpyAdapter", null);
1776
1797
  device_define_property(this, "appNameMapping", {});
1798
+ device_define_property(this, "scalingRatio", 1);
1799
+ device_define_property(this, "takeScreenshotFailCount", 0);
1777
1800
  device_define_property(this, "interfaceType", 'android');
1778
1801
  device_define_property(this, "uri", void 0);
1779
1802
  device_define_property(this, "options", void 0);
@@ -1783,6 +1806,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1783
1806
  this.customActions = options?.customActions;
1784
1807
  }
1785
1808
  }
1809
+ device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
1786
1810
  const runAdbShellParamSchema = core_namespaceObject.z.object({
1787
1811
  command: core_namespaceObject.z.string().describe('ADB shell command to execute')
1788
1812
  });
@@ -1795,6 +1819,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1795
1819
  description: 'Execute ADB shell command on Android device',
1796
1820
  interfaceAlias: 'runAdbShell',
1797
1821
  paramSchema: runAdbShellParamSchema,
1822
+ sample: {
1823
+ command: 'dumpsys window displays | grep -E "mCurrentFocus"'
1824
+ },
1798
1825
  call: async (param)=>{
1799
1826
  if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
1800
1827
  const adb = await device.getAdb();
@@ -1806,6 +1833,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1806
1833
  description: 'Launch an Android app or URL',
1807
1834
  interfaceAlias: 'launch',
1808
1835
  paramSchema: launchParamSchema,
1836
+ sample: {
1837
+ uri: 'com.example.app'
1838
+ },
1809
1839
  call: async (param)=>{
1810
1840
  if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
1811
1841
  await device.launch(param.uri);
@@ -1958,7 +1988,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1958
1988
  constructor(toolsManager){
1959
1989
  super({
1960
1990
  name: '@midscene/android-mcp',
1961
- version: "1.6.7",
1991
+ version: "1.6.9",
1962
1992
  description: 'Control the Android device using natural language commands'
1963
1993
  }, toolsManager);
1964
1994
  }
@@ -10,13 +10,10 @@ import { AndroidDeviceOpt } from '@midscene/core/device';
10
10
  import { BaseMidsceneTools } from '@midscene/shared/mcp';
11
11
  import { Device } from 'appium-adb';
12
12
  import { DeviceAction } from '@midscene/core';
13
- import { ElementCacheFeature } from '@midscene/core';
14
13
  import type { ElementInfo } from '@midscene/shared/extractor';
15
- import { IModelConfig } from '@midscene/shared/env';
16
14
  import { InterfaceType } from '@midscene/core';
17
15
  import { overrideAIConfig } from '@midscene/shared/env';
18
16
  import { Point } from '@midscene/core';
19
- import { Rect } from '@midscene/core';
20
17
  import { Size } from '@midscene/core';
21
18
  import { ToolDefinition } from '@midscene/shared/mcp';
22
19
 
@@ -68,7 +65,6 @@ export declare class AndroidDevice implements AbstractInterface {
68
65
  private yadbPushed;
69
66
  private devicePixelRatio;
70
67
  private devicePixelRatioInitialized;
71
- private scalingRatio;
72
68
  private adb;
73
69
  private connectingAdb;
74
70
  private destroyed;
@@ -79,6 +75,9 @@ export declare class AndroidDevice implements AbstractInterface {
79
75
  private cachedPhysicalDisplayId;
80
76
  private scrcpyAdapter;
81
77
  private appNameMapping;
78
+ private scalingRatio;
79
+ private takeScreenshotFailCount;
80
+ private static readonly TAKE_SCREENSHOT_FAIL_THRESHOLD;
82
81
  interfaceType: InterfaceType;
83
82
  uri: string | undefined;
84
83
  options?: AndroidDeviceOpt;
@@ -109,16 +108,6 @@ export declare class AndroidDevice implements AbstractInterface {
109
108
  private resolvePackageName;
110
109
  launch(uri: string): Promise<AndroidDevice>;
111
110
  execYadb(keyboardContent: string): Promise<void>;
112
- /**
113
- * 通过剪贴板输入文本(备用方案,当YADB不可用时)
114
- * 支持中文等非ASCII字符
115
- */
116
- private inputViaClipboard;
117
- /**
118
- * 逐字符输入文本(最后备用方案)
119
- * 通过模拟按键输入,支持中文(如果有对应输入法)
120
- */
121
- private inputCharByChar;
122
111
  getElementsInfo(): Promise<ElementInfo[]>;
123
112
  getElementsNodeTree(): Promise<any>;
124
113
  getScreenSize(): Promise<{
@@ -130,22 +119,38 @@ export declare class AndroidDevice implements AbstractInterface {
130
119
  private initializeDevicePixelRatio;
131
120
  getDisplayDensity(): Promise<number>;
132
121
  getDisplayOrientation(): Promise<number>;
133
- size(): Promise<Size>;
134
122
  /**
135
- * Generate cache feature for an element at given center point
136
- * Cache is based on test case ID + device ID, so screen size is guaranteed to be consistent
137
- * for same test case on same device. We store exact coordinates for maximum accuracy.
123
+ * Get physical screen dimensions adjusted for current orientation.
124
+ * Swaps width/height when the device is in landscape and the reported
125
+ * dimensions do not already reflect the current orientation.
138
126
  */
139
- cacheFeatureForPoint(center: [number, number], options?: {
140
- targetDescription?: string;
141
- modelConfig?: IModelConfig;
142
- }): Promise<ElementCacheFeature>;
127
+ private getOrientedPhysicalSize;
128
+ size(): Promise<Size>;
129
+ cacheFeatureForPoint(center: [number, number]): Promise<{
130
+ centerX: number;
131
+ centerY: number;
132
+ screenSize: {
133
+ width: number;
134
+ height: number;
135
+ };
136
+ }>;
137
+ rectMatchesCacheFeature(feature: {
138
+ centerX: number;
139
+ centerY: number;
140
+ screenSize: {
141
+ width: number;
142
+ height: number;
143
+ };
144
+ }): Promise<{
145
+ left: number;
146
+ top: number;
147
+ width: number;
148
+ height: number;
149
+ }>;
143
150
  /**
144
- * Find element rect from cache feature
145
- * Since cache is keyed by test case ID + device ID, screen size will be identical
146
- * We return the exact cached coordinates for maximum accuracy.
151
+ * Convert logical coordinates (from AI) back to physical coordinates (for ADB).
152
+ * The ratio is derived from size(), so overriding size() alone is sufficient.
147
153
  */
148
- rectMatchesCacheFeature(feature: ElementCacheFeature): Promise<Rect>;
149
154
  private adjustCoordinates;
150
155
  /**
151
156
  * Calculate the end point for scroll operations based on start point, scroll delta, and screen boundaries.
@@ -174,11 +179,15 @@ export declare class AndroidDevice implements AbstractInterface {
174
179
  scrollRight(distance?: number, startPoint?: Point): Promise<void>;
175
180
  ensureYadb(): Promise<void>;
176
181
  /**
177
- * Check if text contains characters that may cause issues with ADB inputText
178
- * This includes:
179
- * - Non-ASCII characters (Unicode characters like ö, é, ñ, Chinese, Japanese, etc.)
180
- * - Format specifiers that may be interpreted by shell (%, $)
181
- * - Special shell characters that need escaping
182
+ * Check if text contains characters that may cause issues with ADB inputText.
183
+ * appium-adb's inputText has known bugs with certain characters:
184
+ * - Backslash causes broken shell quoting
185
+ * - Backtick is not escaped at all
186
+ * - Text containing both " and ' throws an error
187
+ * - Dollar sign can cause variable expansion issues
188
+ *
189
+ * For these characters, we route through yadb which handles them correctly
190
+ * via escapeForShell + double-quoted shell context.
182
191
  */
183
192
  private shouldUseYadbForText;
184
193
  keyboardType(text: string, options?: AndroidDeviceInputOpt): Promise<void>;