@aiscene/android 1.6.6 → 1.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/es/index.mjs CHANGED
@@ -3,11 +3,12 @@ import * as __rspack_external_node_fs_5ea92f0c from "node:fs";
3
3
  import * as __rspack_external_node_module_ab9f2194 from "node:module";
4
4
  import * as __rspack_external_node_path_c5b9b54f from "node:path";
5
5
  import node_assert from "node:assert";
6
+ import { execFile } from "node:child_process";
6
7
  import { getMidsceneLocationSchema, z } from "@midscene/core";
7
- import { defineAction, defineActionClearInput, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionScroll, defineActionTap } from "@midscene/core/device";
8
+ import { defineAction, defineActionClearInput, defineActionCursorMove, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionPinch, defineActionScroll, defineActionSwipe, defineActionTap, normalizeMobileSwipeParam, normalizePinchParam } from "@midscene/core/device";
8
9
  import { getTmpFile, sleep } from "@midscene/core/utils";
9
10
  import { MIDSCENE_ADB_PATH, MIDSCENE_ADB_REMOTE_HOST, MIDSCENE_ADB_REMOTE_PORT, MIDSCENE_ANDROID_IME_STRATEGY, globalConfigManager, overrideAIConfig } from "@midscene/shared/env";
10
- import { createImgBase64ByFormat, isValidPNGImageBuffer } from "@midscene/shared/img";
11
+ import { createImgBase64ByFormat, isValidImageBuffer } from "@midscene/shared/img";
11
12
  import { mergeAndNormalizeAppNameMapping, normalizeForComparison, repeat } from "@midscene/shared/utils";
12
13
  import { ADB } from "appium-adb";
13
14
  import { Agent } from "@midscene/core/agent";
@@ -578,6 +579,9 @@ const defaultNormalScrollDuration = 1000;
578
579
  const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
579
580
  const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
580
581
  const debugDevice = (0, logger_.getDebug)('android:device');
582
+ function escapeForShell(text) {
583
+ return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
584
+ }
581
585
  class AndroidDevice {
582
586
  actionSpace() {
583
587
  const defaultActions = [
@@ -605,6 +609,12 @@ class AndroidDevice {
605
609
  ]).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.')),
606
610
  locate: getMidsceneLocationSchema().describe('The input field to be filled').optional()
607
611
  }),
612
+ sample: {
613
+ value: 'test@example.com',
614
+ locate: {
615
+ prompt: 'the email input field'
616
+ }
617
+ },
608
618
  call: async (param)=>{
609
619
  const element = param.locate;
610
620
  if ('typeOnly' !== param.mode) await this.clearInput(element);
@@ -650,9 +660,21 @@ class AndroidDevice {
650
660
  y: to.center[1]
651
661
  });
652
662
  }),
663
+ defineActionSwipe(async (param)=>{
664
+ const { startPoint, endPoint, duration, repeatCount } = normalizeMobileSwipeParam(param, await this.size());
665
+ for(let i = 0; i < repeatCount; i++)await this.mouseDrag(startPoint, endPoint, duration);
666
+ }),
653
667
  defineActionKeyboardPress(async (param)=>{
654
668
  await this.keyboardPress(param.keyName);
655
669
  }),
670
+ defineActionCursorMove(async (param)=>{
671
+ const arrowKey = 'left' === param.direction ? 'ArrowLeft' : 'ArrowRight';
672
+ const times = param.times ?? 1;
673
+ for(let i = 0; i < times; i++){
674
+ await this.keyboardPress(arrowKey);
675
+ await sleep(100);
676
+ }
677
+ }),
656
678
  defineAction({
657
679
  name: 'LongPress',
658
680
  description: 'Trigger a long press on the screen at specified element',
@@ -660,6 +682,11 @@ class AndroidDevice {
660
682
  duration: z.number().optional().describe('The duration of the long press in milliseconds'),
661
683
  locate: getMidsceneLocationSchema().describe('The element to be long pressed')
662
684
  }),
685
+ sample: {
686
+ locate: {
687
+ prompt: 'the message bubble'
688
+ }
689
+ },
663
690
  call: async (param)=>{
664
691
  const element = param.locate;
665
692
  if (!element) throw new Error('LongPress requires an element to be located');
@@ -679,6 +706,12 @@ class AndroidDevice {
679
706
  duration: z.number().optional().describe('The duration of the pull (in milliseconds)'),
680
707
  locate: getMidsceneLocationSchema().optional().describe('The element to start the pull from (optional)')
681
708
  }),
709
+ sample: {
710
+ direction: 'down',
711
+ locate: {
712
+ prompt: 'the center of the content list area'
713
+ }
714
+ },
682
715
  call: async (param)=>{
683
716
  const element = param.locate;
684
717
  const startPoint = element ? {
@@ -691,6 +724,16 @@ class AndroidDevice {
691
724
  else throw new Error(`Unknown pull direction: ${param.direction}`);
692
725
  }
693
726
  }),
727
+ defineActionPinch(async (param)=>{
728
+ const { centerX, centerY, startDistance, endDistance, duration } = normalizePinchParam(param, await this.size());
729
+ const { x: adjCenterX, y: adjCenterY } = await this.adjustCoordinates(centerX, centerY);
730
+ const ratio = 0 !== adjCenterX && 0 !== centerX ? adjCenterX / centerX : 1;
731
+ const adjStartDist = Math.round(startDistance * ratio);
732
+ const adjEndDist = Math.round(endDistance * ratio);
733
+ await this.ensureYadb();
734
+ const adb = await this.getAdb();
735
+ 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}`);
736
+ }),
694
737
  defineActionClearInput(async (param)=>{
695
738
  await this.clearInput(param.locate);
696
739
  })
@@ -835,61 +878,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
835
878
  async execYadb(keyboardContent) {
836
879
  await this.ensureYadb();
837
880
  const adb = await this.getAdb();
838
- try {
839
- const command = `app_process${this.getDisplayArg()} -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard "${keyboardContent}"`;
840
- debugDevice(`Executing YADB input: "${keyboardContent}"`);
841
- await adb.shell(command);
842
- debugDevice(`YADB input completed: "${keyboardContent}"`);
843
- } catch (error) {
844
- const isAccessibilityConflict = error?.cause?.stderr?.includes('UiAutomationService') || error?.cause?.stderr?.includes('already registered') || error?.message?.includes('UiAutomationService');
845
- if (isAccessibilityConflict) {
846
- debugDevice("YADB failed due to AccessibilityService conflict (likely Appium running), falling back to clipboard method");
847
- await this.inputViaClipboard(keyboardContent);
848
- } else debugDevice(`YADB execution may have completed despite error: ${error}`);
849
- }
850
- }
851
- async inputViaClipboard(text) {
852
- const adb = await this.getAdb();
853
- try {
854
- debugDevice(`Inputting via clipboard: "${text}"`);
855
- const escapedText = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
856
- const setClipboardCmd = `
857
- content insert --uri content://settings/system --bind name:s:clipboard_text --bind value:s:"${escapedText}"
858
- `;
859
- await adb.shell(setClipboardCmd);
860
- await sleep(100);
861
- await adb.shell('input keyevent KEYCODE_PASTE');
862
- await sleep(100);
863
- debugDevice(`Clipboard input completed via content provider: "${text}"`);
864
- } catch (error1) {
865
- debugDevice(`Content provider clipboard failed, trying clipper app: ${error1}`);
866
- try {
867
- const base64Text = Buffer.from(text, 'utf-8').toString('base64');
868
- await adb.shell(`am broadcast -a clipper.set -e text "${base64Text}"`);
869
- await sleep(100);
870
- await adb.shell('input keyevent KEYCODE_PASTE');
871
- await sleep(100);
872
- debugDevice(`Clipboard input completed via clipper: "${text}"`);
873
- } catch (error2) {
874
- debugDevice(`All clipboard methods failed: ${error2}`);
875
- const isPureAscii = /^[\x00-\x7F]*$/.test(text);
876
- if (isPureAscii) {
877
- debugDevice(`Using ADB inputText for ASCII text: "${text}"`);
878
- await adb.inputText(text);
879
- } else await this.inputCharByChar(text);
880
- }
881
- }
882
- }
883
- async inputCharByChar(text) {
884
- const adb = await this.getAdb();
885
- debugDevice(`Inputting character by character (slow method): "${text}"`);
886
- const chars = Array.from(text);
887
- for (const char of chars){
888
- if (' ' === char) await adb.shell('input keyevent KEYCODE_SPACE');
889
- else await adb.shell(`input text "${char}"`);
890
- await sleep(50);
891
- }
892
- debugDevice("Character-by-character input completed");
881
+ await adb.shell(`app_process${this.getDisplayArg()} -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard '${keyboardContent}'`);
893
882
  }
894
883
  async getElementsInfo() {
895
884
  return [];
@@ -902,6 +891,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
902
891
  }
903
892
  async getScreenSize() {
904
893
  const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
894
+ debugDevice(`getScreenSize: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedSize=${!!this.cachedScreenSize}`);
905
895
  if (shouldCache && this.cachedScreenSize) return this.cachedScreenSize;
906
896
  const adb = await this.getAdb();
907
897
  if ('number' == typeof this.options?.displayId) try {
@@ -1035,6 +1025,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1035
1025
  }
1036
1026
  async getDisplayOrientation() {
1037
1027
  const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
1028
+ debugDevice(`getDisplayOrientation: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedOrientation=${null !== this.cachedOrientation}`);
1038
1029
  if (shouldCache && null !== this.cachedOrientation) return this.cachedOrientation;
1039
1030
  const adb = await this.getAdb();
1040
1031
  let orientation = 0;
@@ -1060,6 +1051,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1060
1051
  if (shouldCache) this.cachedOrientation = orientation;
1061
1052
  return orientation;
1062
1053
  }
1054
+ async getOrientedPhysicalSize() {
1055
+ const info = await this.getDevicePhysicalInfo();
1056
+ const isLandscape = 1 === info.orientation || 3 === info.orientation;
1057
+ const shouldSwap = true !== info.isCurrentOrientation && isLandscape;
1058
+ return {
1059
+ width: shouldSwap ? info.physicalHeight : info.physicalWidth,
1060
+ height: shouldSwap ? info.physicalWidth : info.physicalHeight
1061
+ };
1062
+ }
1063
1063
  async size() {
1064
1064
  const deviceInfo = await this.getDevicePhysicalInfo();
1065
1065
  const adapter = this.getScrcpyAdapter();
@@ -1086,7 +1086,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1086
1086
  height: logicalHeight
1087
1087
  };
1088
1088
  }
1089
- async cacheFeatureForPoint(center, options) {
1089
+ async cacheFeatureForPoint(center) {
1090
1090
  const { width, height } = await this.size();
1091
1091
  debugDevice('cacheFeatureForPoint: center=[%s,%s], screen=[%s,%s]', center[0], center[1], width, height);
1092
1092
  return {
@@ -1166,14 +1166,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1166
1166
  const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
1167
1167
  const useShellScreencap = 'number' == typeof this.options?.displayId;
1168
1168
  try {
1169
- if (useShellScreencap) throw new Error('Using shell screencap for displayId');
1170
- debugDevice('Taking screenshot via adb.takeScreenshot');
1171
- screenshotBuffer = await adb.takeScreenshot(null);
1172
- debugDevice('adb.takeScreenshot completed');
1173
- if (!screenshotBuffer) throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1174
- if (!isValidPNGImageBuffer(screenshotBuffer)) {
1175
- debugDevice('Invalid image buffer detected: not a valid image format');
1176
- throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1169
+ if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
1170
+ debugDevice('Taking screenshot via adb.takeScreenshot');
1171
+ screenshotBuffer = await adb.takeScreenshot(null);
1172
+ debugDevice('adb.takeScreenshot completed');
1173
+ if (!screenshotBuffer) {
1174
+ this.takeScreenshotFailCount++;
1175
+ throw new Error('Failed to capture screenshot: screenshotBuffer is null');
1176
+ }
1177
+ if (!isValidImageBuffer(screenshotBuffer)) {
1178
+ debugDevice('Invalid image buffer detected: not a valid image format');
1179
+ this.takeScreenshotFailCount++;
1180
+ throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
1181
+ }
1182
+ this.takeScreenshotFailCount = 0;
1183
+ } else {
1184
+ if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
1185
+ throw new Error('Using shell screencap directly');
1177
1186
  }
1178
1187
  const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
1179
1188
  if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
@@ -1202,12 +1211,21 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1202
1211
  screenshotBuffer = await external_node_fs_["default"].promises.readFile(screenshotPath);
1203
1212
  const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
1204
1213
  if (!screenshotBuffer || validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) throw new Error(`Fallback screenshot validation failed: buffer size ${screenshotBuffer?.length || 0} bytes (minimum: ${validScreenshotBufferSize})`);
1205
- if (!isValidPNGImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
1214
+ if (!isValidImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
1206
1215
  debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
1207
1216
  } finally{
1208
- Promise.resolve().then(()=>adb.shell(`rm ${androidScreenshotPath}`)).catch((error)=>{
1209
- debugDevice(`Failed to delete remote screenshot: ${error}`);
1217
+ const adbPath = adb.executable?.path ?? 'adb';
1218
+ const child = execFile(adbPath, [
1219
+ '-s',
1220
+ this.deviceId,
1221
+ 'shell',
1222
+ `rm ${androidScreenshotPath}`
1223
+ ], {
1224
+ timeout: 3000
1225
+ }, (err)=>{
1226
+ if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
1210
1227
  });
1228
+ child.unref();
1211
1229
  }
1212
1230
  }
1213
1231
  if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
@@ -1370,24 +1388,32 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1370
1388
  if (!this.yadbPushed) {
1371
1389
  const adb = await this.getAdb();
1372
1390
  const androidPkgJson = (0, external_node_module_.createRequire)(import.meta.url).resolve('@aiscene/android/package.json');
1373
- const yadbDir = external_node_path_["default"].join(external_node_path_["default"].dirname(androidPkgJson), 'bin');
1374
- await adb.push(yadbDir, '/data/local/tmp');
1391
+ const yadbBin = external_node_path_["default"].join(external_node_path_["default"].dirname(androidPkgJson), 'bin', 'yadb');
1392
+ await adb.push(yadbBin, '/data/local/tmp');
1375
1393
  this.yadbPushed = true;
1376
1394
  }
1377
1395
  }
1378
1396
  shouldUseYadbForText(text) {
1379
1397
  const hasNonAscii = /[\x80-\uFFFF]/.test(text);
1380
1398
  const hasFormatSpecifiers = /%[a-zA-Z]/.test(text);
1381
- return hasNonAscii || hasFormatSpecifiers;
1399
+ const hasShellSpecialChars = /[\\`$]/.test(text);
1400
+ const hasBothQuotes = text.includes('"') && text.includes("'");
1401
+ return hasNonAscii || hasFormatSpecifiers || hasShellSpecialChars || hasBothQuotes;
1382
1402
  }
1383
1403
  async keyboardType(text, options) {
1384
1404
  if (!text) return;
1385
1405
  const adb = await this.getAdb();
1386
- const shouldUseYadb = this.shouldUseYadbForText(text);
1387
1406
  const IME_STRATEGY = (this.options?.imeStrategy || globalConfigManager.getEnvConfigValue(MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
1388
1407
  const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
1389
- if (IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && shouldUseYadb) await this.execYadb(text);
1390
- else await adb.inputText(text);
1408
+ const useYadb = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && this.shouldUseYadbForText(text);
1409
+ if (useYadb) await this.execYadb(escapeForShell(text));
1410
+ else {
1411
+ const segments = text.split('\n');
1412
+ for(let i = 0; i < segments.length; i++){
1413
+ if (segments[i].length > 0) await adb.inputText(segments[i]);
1414
+ if (i < segments.length - 1) await adb.keyevent(66);
1415
+ }
1416
+ }
1391
1417
  if (true === shouldAutoDismissKeyboard) await this.hideKeyboard(options);
1392
1418
  }
1393
1419
  normalizeKeyName(key) {
@@ -1435,12 +1461,12 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1435
1461
  }
1436
1462
  async mouseClick(x, y) {
1437
1463
  const adb = await this.getAdb();
1438
- const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
1464
+ const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
1439
1465
  await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} 150`);
1440
1466
  }
1441
1467
  async mouseDoubleClick(x, y) {
1442
1468
  const adb = await this.getAdb();
1443
- const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
1469
+ const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
1444
1470
  const tapCommand = `input${this.getDisplayArg()} tap ${adjustedX} ${adjustedY}`;
1445
1471
  await adb.shell(tapCommand);
1446
1472
  await sleep(50);
@@ -1451,8 +1477,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1451
1477
  }
1452
1478
  async mouseDrag(from, to, duration) {
1453
1479
  const adb = await this.getAdb();
1454
- const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
1455
- const { x: toX, y: toY } = this.adjustCoordinates(to.x, to.y);
1480
+ const { x: fromX, y: fromY } = await this.adjustCoordinates(from.x, from.y);
1481
+ const { x: toX, y: toY } = await this.adjustCoordinates(to.x, to.y);
1456
1482
  const swipeDuration = duration ?? defaultNormalScrollDuration;
1457
1483
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${swipeDuration}`);
1458
1484
  }
@@ -1462,16 +1488,16 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1462
1488
  const n = 4;
1463
1489
  const startX = Math.round(deltaX < 0 ? width / n * (n - 1) : width / n);
1464
1490
  const startY = Math.round(deltaY < 0 ? height / n * (n - 1) : height / n);
1465
- const maxNegativeDeltaX = startX;
1466
- const maxPositiveDeltaX = Math.round(width / n * (n - 1));
1467
- const maxNegativeDeltaY = startY;
1468
- const maxPositiveDeltaY = Math.round(height / n * (n - 1));
1491
+ const maxPositiveDeltaX = startX;
1492
+ const maxNegativeDeltaX = width - startX;
1493
+ const maxPositiveDeltaY = startY;
1494
+ const maxNegativeDeltaY = height - startY;
1469
1495
  deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
1470
1496
  deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
1471
1497
  const endX = Math.round(startX - deltaX);
1472
1498
  const endY = Math.round(startY - deltaY);
1473
- const { x: adjustedStartX, y: adjustedStartY } = this.adjustCoordinates(startX, startY);
1474
- const { x: adjustedEndX, y: adjustedEndY } = this.adjustCoordinates(endX, endY);
1499
+ const { x: adjustedStartX, y: adjustedStartY } = await this.adjustCoordinates(startX, startY);
1500
+ const { x: adjustedEndX, y: adjustedEndY } = await this.adjustCoordinates(endX, endY);
1475
1501
  const adb = await this.getAdb();
1476
1502
  const swipeDuration = duration ?? defaultNormalScrollDuration;
1477
1503
  await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedStartX} ${adjustedStartY} ${adjustedEndX} ${adjustedEndY} ${swipeDuration}`);
@@ -1482,6 +1508,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1482
1508
  this.cachedPhysicalDisplayId = void 0;
1483
1509
  this.cachedScreenSize = null;
1484
1510
  this.cachedOrientation = null;
1511
+ this.scalingRatio = 1;
1485
1512
  if (this.scrcpyAdapter) {
1486
1513
  await this.scrcpyAdapter.disconnect();
1487
1514
  this.scrcpyAdapter = null;
@@ -1521,7 +1548,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1521
1548
  }
1522
1549
  async longPress(x, y, duration = 2000) {
1523
1550
  const adb = await this.getAdb();
1524
- const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
1551
+ const { x: adjustedX, y: adjustedY } = await this.adjustCoordinates(x, y);
1525
1552
  await adb.shell(`input${this.getDisplayArg()} swipe ${adjustedX} ${adjustedY} ${adjustedX} ${adjustedY} ${duration}`);
1526
1553
  }
1527
1554
  async pullDown(startPoint, distance, duration = 800) {
@@ -1543,8 +1570,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1543
1570
  }
1544
1571
  async pullDrag(from, to, duration) {
1545
1572
  const adb = await this.getAdb();
1546
- const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
1547
- const { x: toX, y: toY } = this.adjustCoordinates(to.x, to.y);
1573
+ const { x: fromX, y: fromY } = await this.adjustCoordinates(from.x, from.y);
1574
+ const { x: toX, y: toY } = await this.adjustCoordinates(to.x, to.y);
1548
1575
  await adb.shell(`input${this.getDisplayArg()} swipe ${fromX} ${fromY} ${toX} ${toY} ${duration}`);
1549
1576
  }
1550
1577
  async pullUp(startPoint, distance, duration = 600) {
@@ -1631,7 +1658,6 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1631
1658
  device_define_property(this, "yadbPushed", false);
1632
1659
  device_define_property(this, "devicePixelRatio", 1);
1633
1660
  device_define_property(this, "devicePixelRatioInitialized", false);
1634
- device_define_property(this, "scalingRatio", 1);
1635
1661
  device_define_property(this, "adb", null);
1636
1662
  device_define_property(this, "connectingAdb", null);
1637
1663
  device_define_property(this, "destroyed", false);
@@ -1642,6 +1668,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1642
1668
  device_define_property(this, "cachedPhysicalDisplayId", void 0);
1643
1669
  device_define_property(this, "scrcpyAdapter", null);
1644
1670
  device_define_property(this, "appNameMapping", {});
1671
+ device_define_property(this, "scalingRatio", 1);
1672
+ device_define_property(this, "takeScreenshotFailCount", 0);
1645
1673
  device_define_property(this, "interfaceType", 'android');
1646
1674
  device_define_property(this, "uri", void 0);
1647
1675
  device_define_property(this, "options", void 0);
@@ -1651,6 +1679,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1651
1679
  this.customActions = options?.customActions;
1652
1680
  }
1653
1681
  }
1682
+ device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
1654
1683
  const runAdbShellParamSchema = z.object({
1655
1684
  command: z.string().describe('ADB shell command to execute')
1656
1685
  });
@@ -1663,6 +1692,9 @@ const createPlatformActions = (device)=>({
1663
1692
  description: 'Execute ADB shell command on Android device',
1664
1693
  interfaceAlias: 'runAdbShell',
1665
1694
  paramSchema: runAdbShellParamSchema,
1695
+ sample: {
1696
+ command: 'dumpsys window displays | grep -E "mCurrentFocus"'
1697
+ },
1666
1698
  call: async (param)=>{
1667
1699
  if (!param.command || '' === param.command.trim()) throw new Error('RunAdbShell requires a non-empty command parameter');
1668
1700
  const adb = await device.getAdb();
@@ -1674,6 +1706,9 @@ const createPlatformActions = (device)=>({
1674
1706
  description: 'Launch an Android app or URL',
1675
1707
  interfaceAlias: 'launch',
1676
1708
  paramSchema: launchParamSchema,
1709
+ sample: {
1710
+ uri: 'com.example.app'
1711
+ },
1677
1712
  call: async (param)=>{
1678
1713
  if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
1679
1714
  await device.launch(param.uri);