@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.
@@ -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,61 +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
- await adb.shell(command);
969
- debugDevice(`YADB input completed: "${keyboardContent}"`);
970
- } catch (error) {
971
- const isAccessibilityConflict = error?.cause?.stderr?.includes('UiAutomationService') || error?.cause?.stderr?.includes('already registered') || error?.message?.includes('UiAutomationService');
972
- if (isAccessibilityConflict) {
973
- debugDevice("YADB failed due to AccessibilityService conflict (likely Appium running), falling back to clipboard method");
974
- await this.inputViaClipboard(keyboardContent);
975
- } else debugDevice(`YADB execution may have completed despite error: ${error}`);
976
- }
977
- }
978
- async inputViaClipboard(text) {
979
- const adb = await this.getAdb();
980
- try {
981
- debugDevice(`Inputting via clipboard: "${text}"`);
982
- const escapedText = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
983
- const setClipboardCmd = `
984
- content insert --uri content://settings/system --bind name:s:clipboard_text --bind value:s:"${escapedText}"
985
- `;
986
- await adb.shell(setClipboardCmd);
987
- await (0, core_utils_namespaceObject.sleep)(100);
988
- await adb.shell('input keyevent KEYCODE_PASTE');
989
- await (0, core_utils_namespaceObject.sleep)(100);
990
- debugDevice(`Clipboard input completed via content provider: "${text}"`);
991
- } catch (error1) {
992
- debugDevice(`Content provider clipboard failed, trying clipper app: ${error1}`);
993
- try {
994
- const base64Text = Buffer.from(text, 'utf-8').toString('base64');
995
- await adb.shell(`am broadcast -a clipper.set -e text "${base64Text}"`);
996
- await (0, core_utils_namespaceObject.sleep)(100);
997
- await adb.shell('input keyevent KEYCODE_PASTE');
998
- await (0, core_utils_namespaceObject.sleep)(100);
999
- debugDevice(`Clipboard input completed via clipper: "${text}"`);
1000
- } catch (error2) {
1001
- debugDevice(`All clipboard methods failed: ${error2}`);
1002
- const isPureAscii = /^[\x00-\x7F]*$/.test(text);
1003
- if (isPureAscii) {
1004
- debugDevice(`Using ADB inputText for ASCII text: "${text}"`);
1005
- await adb.inputText(text);
1006
- } else await this.inputCharByChar(text);
1007
- }
1008
- }
1009
- }
1010
- async inputCharByChar(text) {
1011
- const adb = await this.getAdb();
1012
- debugDevice(`Inputting character by character (slow method): "${text}"`);
1013
- const chars = Array.from(text);
1014
- for (const char of chars){
1015
- if (' ' === char) await adb.shell('input keyevent KEYCODE_SPACE');
1016
- else await adb.shell(`input text "${char}"`);
1017
- await (0, core_utils_namespaceObject.sleep)(50);
1018
- }
1019
- 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}'`);
1020
1009
  }
1021
1010
  async getElementsInfo() {
1022
1011
  return [];
@@ -1029,6 +1018,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1029
1018
  }
1030
1019
  async getScreenSize() {
1031
1020
  const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
1021
+ debugDevice(`getScreenSize: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedSize=${!!this.cachedScreenSize}`);
1032
1022
  if (shouldCache && this.cachedScreenSize) return this.cachedScreenSize;
1033
1023
  const adb = await this.getAdb();
1034
1024
  if ('number' == typeof this.options?.displayId) try {
@@ -1162,6 +1152,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1162
1152
  }
1163
1153
  async getDisplayOrientation() {
1164
1154
  const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
1155
+ debugDevice(`getDisplayOrientation: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedOrientation=${null !== this.cachedOrientation}`);
1165
1156
  if (shouldCache && null !== this.cachedOrientation) return this.cachedOrientation;
1166
1157
  const adb = await this.getAdb();
1167
1158
  let orientation = 0;
@@ -1187,6 +1178,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1187
1178
  if (shouldCache) this.cachedOrientation = orientation;
1188
1179
  return orientation;
1189
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
+ }
1190
1190
  async size() {
1191
1191
  const deviceInfo = await this.getDevicePhysicalInfo();
1192
1192
  const adapter = this.getScrcpyAdapter();
@@ -1213,7 +1213,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1213
1213
  height: logicalHeight
1214
1214
  };
1215
1215
  }
1216
- async cacheFeatureForPoint(center, options) {
1216
+ async cacheFeatureForPoint(center) {
1217
1217
  const { width, height } = await this.size();
1218
1218
  debugDevice('cacheFeatureForPoint: center=[%s,%s], screen=[%s,%s]', center[0], center[1], width, height);
1219
1219
  return {
@@ -1293,14 +1293,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1293
1293
  const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
1294
1294
  const useShellScreencap = 'number' == typeof this.options?.displayId;
1295
1295
  try {
1296
- if (useShellScreencap) throw new Error('Using shell screencap for displayId');
1297
- debugDevice('Taking screenshot via adb.takeScreenshot');
1298
- screenshotBuffer = await adb.takeScreenshot(null);
1299
- debugDevice('adb.takeScreenshot completed');
1300
- if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1301
- if (!(0, img_namespaceObject.isValidPNGImageBuffer)(screenshotBuffer)) {
1302
- debugDevice('Invalid image buffer detected: not a valid image format');
1303
- 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');
1304
1313
  }
1305
1314
  const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
1306
1315
  if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
@@ -1329,12 +1338,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1329
1338
  screenshotBuffer = await external_node_fs_default().promises.readFile(screenshotPath);
1330
1339
  const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
1331
1340
  if (!screenshotBuffer || validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) throw new Error(`Fallback screenshot validation failed: buffer size ${screenshotBuffer?.length || 0} bytes (minimum: ${validScreenshotBufferSize})`);
1332
- 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');
1333
1342
  debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
1334
1343
  } finally{
1335
- Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
1336
- 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);
1337
1354
  });
1355
+ child.unref();
1338
1356
  }
1339
1357
  }
1340
1358
  if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
@@ -1497,24 +1515,32 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1497
1515
  if (!this.yadbPushed) {
1498
1516
  const adb = await this.getAdb();
1499
1517
  const androidPkgJson = (0, external_node_module_.createRequire)(__rslib_import_meta_url__).resolve('@aiscene/android/package.json');
1500
- const yadbDir = external_node_path_default().join(external_node_path_default().dirname(androidPkgJson), 'bin');
1501
- 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');
1502
1520
  this.yadbPushed = true;
1503
1521
  }
1504
1522
  }
1505
1523
  shouldUseYadbForText(text) {
1506
1524
  const hasNonAscii = /[\x80-\uFFFF]/.test(text);
1507
1525
  const hasFormatSpecifiers = /%[a-zA-Z]/.test(text);
1508
- return hasNonAscii || hasFormatSpecifiers;
1526
+ const hasShellSpecialChars = /[\\`$]/.test(text);
1527
+ const hasBothQuotes = text.includes('"') && text.includes("'");
1528
+ return hasNonAscii || hasFormatSpecifiers || hasShellSpecialChars || hasBothQuotes;
1509
1529
  }
1510
1530
  async keyboardType(text, options) {
1511
1531
  if (!text) return;
1512
1532
  const adb = await this.getAdb();
1513
- const shouldUseYadb = this.shouldUseYadbForText(text);
1514
1533
  const IME_STRATEGY = (this.options?.imeStrategy || env_namespaceObject.globalConfigManager.getEnvConfigValue(env_namespaceObject.MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
1515
1534
  const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
1516
- if (IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && shouldUseYadb) await this.execYadb(text);
1517
- 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
+ }
1518
1544
  if (true === shouldAutoDismissKeyboard) await this.hideKeyboard(options);
1519
1545
  }
1520
1546
  normalizeKeyName(key) {
@@ -1562,12 +1588,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1562
1588
  }
1563
1589
  async mouseClick(x, y) {
1564
1590
  const adb = await this.getAdb();
1565
- const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
1591
+ const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
1566
1592
  await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} 150`);
1567
1593
  }
1568
1594
  async mouseDoubleClick(x, y) {
1569
1595
  const adb = await this.getAdb();
1570
- const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
1596
+ const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
1571
1597
  const tapCommand = `input${this.getDisplayArg()} tap ${adjustedX} ${adjustedY}`;
1572
1598
  await adb.shell(tapCommand);
1573
1599
  await (0, core_utils_namespaceObject.sleep)(50);
@@ -1578,8 +1604,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1578
1604
  }
1579
1605
  async mouseDrag(from, to, duration) {
1580
1606
  const adb = await this.getAdb();
1581
- const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
1582
- 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);
1583
1609
  const swipeDuration = duration ?? defaultNormalScrollDuration;
1584
1610
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
1585
1611
  }
@@ -1589,16 +1615,16 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1589
1615
  const n = 4;
1590
1616
  const startX = Math.round(deltaX < 0 ? width / n * (n - 1) : width / n);
1591
1617
  const startY = Math.round(deltaY < 0 ? height / n * (n - 1) : height / n);
1592
- const maxNegativeDeltaX = startX;
1593
- const maxPositiveDeltaX = Math.round(width / n * (n - 1));
1594
- const maxNegativeDeltaY = startY;
1595
- 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;
1596
1622
  deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
1597
1623
  deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
1598
1624
  const endX = Math.round(startX - deltaX);
1599
1625
  const endY = Math.round(startY - deltaY);
1600
- const { x: adjustedStartX, y: adjustedStartY } = this.adjustCoordinates(startX, startY);
1601
- 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);
1602
1628
  const adb = await this.getAdb();
1603
1629
  const swipeDuration = duration ?? defaultNormalScrollDuration;
1604
1630
  await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedStartX} ${adjustedStartY} ${adjustedEndX} ${adjustedEndY} ${swipeDuration}`);
@@ -1609,6 +1635,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1609
1635
  this.cachedPhysicalDisplayId = void 0;
1610
1636
  this.cachedScreenSize = null;
1611
1637
  this.cachedOrientation = null;
1638
+ this.scalingRatio = 1;
1612
1639
  if (this.scrcpyAdapter) {
1613
1640
  await this.scrcpyAdapter.disconnect();
1614
1641
  this.scrcpyAdapter = null;
@@ -1648,7 +1675,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1648
1675
  }
1649
1676
  async longPress(x, y, duration = 2000) {
1650
1677
  const adb = await this.getAdb();
1651
- const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
1678
+ const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
1652
1679
  await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
1653
1680
  }
1654
1681
  async pullDown(startPoint, distance, duration = 800) {
@@ -1670,8 +1697,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1670
1697
  }
1671
1698
  async pullDrag(from, to, duration) {
1672
1699
  const adb = await this.getAdb();
1673
- const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
1674
- 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);
1675
1702
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
1676
1703
  }
1677
1704
  async pullUp(startPoint, distance, duration = 600) {
@@ -1758,7 +1785,6 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1758
1785
  device_define_property(this, "yadbPushed", false);
1759
1786
  device_define_property(this, "devicePixelRatio", 1);
1760
1787
  device_define_property(this, "devicePixelRatioInitialized", false);
1761
- device_define_property(this, "scalingRatio", 1);
1762
1788
  device_define_property(this, "adb", null);
1763
1789
  device_define_property(this, "connectingAdb", null);
1764
1790
  device_define_property(this, "destroyed", false);
@@ -1769,6 +1795,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1769
1795
  device_define_property(this, "cachedPhysicalDisplayId", void 0);
1770
1796
  device_define_property(this, "scrcpyAdapter", null);
1771
1797
  device_define_property(this, "appNameMapping", {});
1798
+ device_define_property(this, "scalingRatio", 1);
1799
+ device_define_property(this, "takeScreenshotFailCount", 0);
1772
1800
  device_define_property(this, "interfaceType", 'android');
1773
1801
  device_define_property(this, "uri", void 0);
1774
1802
  device_define_property(this, "options", void 0);
@@ -1778,6 +1806,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1778
1806
  this.customActions = options?.customActions;
1779
1807
  }
1780
1808
  }
1809
+ device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
1781
1810
  const runAdbShellParamSchema = core_namespaceObject.z.object({
1782
1811
  command: core_namespaceObject.z.string().describe('ADB shell command to execute')
1783
1812
  });
@@ -1790,6 +1819,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1790
1819
  description: 'Execute ADB shell command on Android device',
1791
1820
  interfaceAlias: 'runAdbShell',
1792
1821
  paramSchema: runAdbShellParamSchema,
1822
+ sample: {
1823
+ command: 'dumpsys window displays | grep -E "mCurrentFocus"'
1824
+ },
1793
1825
  call: async (param)=>{
1794
1826
  if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
1795
1827
  const adb = await device.getAdb();
@@ -1801,6 +1833,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1801
1833
  description: 'Launch an Android app or URL',
1802
1834
  interfaceAlias: 'launch',
1803
1835
  paramSchema: launchParamSchema,
1836
+ sample: {
1837
+ uri: 'com.example.app'
1838
+ },
1804
1839
  call: async (param)=>{
1805
1840
  if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
1806
1841
  await device.launch(param.uri);
@@ -1953,7 +1988,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1953
1988
  constructor(toolsManager){
1954
1989
  super({
1955
1990
  name: '@midscene/android-mcp',
1956
- version: "1.6.6",
1991
+ version: "1.6.8",
1957
1992
  description: 'Control the Android device using natural language commands'
1958
1993
  }, toolsManager);
1959
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>;