@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 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 { defineAction, defineActionClearInput, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionScroll, defineActionTap } from "@midscene/core/device";
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, isValidPNGImageBuffer } from "@midscene/shared/img";
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
- try {
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, options) {
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) throw new Error('Using shell screencap for displayId');
1272
- debugDevice('Taking screenshot via adb.takeScreenshot');
1273
- screenshotBuffer = await adb.takeScreenshot(null);
1274
- debugDevice('adb.takeScreenshot completed');
1275
- if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1276
- if (!isValidPNGImageBuffer(screenshotBuffer)) {
1277
- debugDevice('Invalid image buffer detected: not a valid image format');
1278
- throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
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 (!isValidPNGImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
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
- Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
1311
- debugDevice(`Failed to delete remote screenshot: ${error}`);
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 yadbDir = external_node_path_["default"].join(external_node_path_["default"].dirname(androidPkgJson), 'bin');
1476
- await adb.push(yadbDir, '/data/local/tmp');
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
- return hasNonAscii || hasFormatSpecifiers;
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
- if (IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && shouldUseYadb) await this.execYadb(text);
1492
- else await adb.inputText(text);
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 maxNegativeDeltaX = startX;
1568
- const maxPositiveDeltaX = Math.round(width / n * (n - 1));
1569
- const maxNegativeDeltaY = startY;
1570
- const maxPositiveDeltaY = Math.round(height / n * (n - 1));
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.7"
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);