@midscene/android 1.10.8 → 1.10.9-beta-20260731103801.0

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/lib/cli.js CHANGED
@@ -87,20 +87,12 @@ var __webpack_modules__ = {
87
87
  try {
88
88
  this.isConnecting = true;
89
89
  debugScrcpy('Starting scrcpy connection...');
90
- const { AdbScrcpyClient, AdbScrcpyOptions3_3_3 } = await import("@yume-chan/adb-scrcpy");
90
+ const { AdbScrcpyClient } = await import("@yume-chan/adb-scrcpy");
91
91
  const { ReadableStream } = await import("@yume-chan/stream-extra");
92
92
  const { DefaultServerPath } = await import("@yume-chan/scrcpy");
93
93
  const serverBinPath = this.resolveServerBinPath();
94
94
  await AdbScrcpyClient.pushServer(this.adb, ReadableStream.from((0, node_fs__rspack_import_0.createReadStream)(serverBinPath)));
95
- const scrcpyOptions = new AdbScrcpyOptions3_3_3({
96
- audio: false,
97
- control: false,
98
- maxSize: this.options.maxSize,
99
- videoBitRate: this.options.videoBitRate,
100
- maxFps: 10,
101
- sendFrameMeta: true,
102
- videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
103
- });
95
+ const scrcpyOptions = await this.createScrcpyOptions();
104
96
  this.scrcpyClient = await AdbScrcpyClient.start(this.adb, DefaultServerPath, scrcpyOptions);
105
97
  serverOutputTask = this.collectServerOutput(this.scrcpyClient.output, serverOutput);
106
98
  const videoStreamPromise = this.scrcpyClient.videoStream;
@@ -128,6 +120,23 @@ var __webpack_modules__ = {
128
120
  this.isConnecting = false;
129
121
  }
130
122
  }
123
+ async createScrcpyOptions() {
124
+ const [{ AdbScrcpyOptions3_3_3 }, { ScrcpyInstanceId }] = await Promise.all([
125
+ import("@yume-chan/adb-scrcpy"),
126
+ import("@yume-chan/scrcpy")
127
+ ]);
128
+ return new AdbScrcpyOptions3_3_3({
129
+ audio: false,
130
+ control: false,
131
+ tunnelForward: true,
132
+ scid: ScrcpyInstanceId.random(),
133
+ maxSize: this.options.maxSize,
134
+ videoBitRate: this.options.videoBitRate,
135
+ maxFps: 10,
136
+ sendFrameMeta: true,
137
+ videoCodecOptions: 'i-frame-interval=0,bitrate-mode=2'
138
+ });
139
+ }
131
140
  async collectServerOutput(output, lines) {
132
141
  const reader = output.getReader();
133
142
  try {
@@ -895,6 +904,230 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
895
904
  this.retryAfter = null;
896
905
  }
897
906
  }
907
+ const TAG_RE = /<(\/?)([A-Za-z_][A-Za-z0-9_.\-:]*)([^>]*?)(\/?)>/g;
908
+ const ATTR_RE = /([A-Za-z_][A-Za-z0-9_.\-:]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
909
+ const BOUNDS_RE = /^\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]$/;
910
+ const ENTITY_TABLE = {
911
+ amp: '&',
912
+ lt: '<',
913
+ gt: '>',
914
+ quot: '"',
915
+ apos: "'"
916
+ };
917
+ function decodeEntities(value) {
918
+ if (!value.includes('&')) return value;
919
+ return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (full, body)=>{
920
+ if (body.startsWith('#x') || body.startsWith('#X')) {
921
+ const code = Number.parseInt(body.slice(2), 16);
922
+ return Number.isFinite(code) ? String.fromCodePoint(code) : full;
923
+ }
924
+ if (body.startsWith('#')) {
925
+ const code = Number.parseInt(body.slice(1), 10);
926
+ return Number.isFinite(code) ? String.fromCodePoint(code) : full;
927
+ }
928
+ return ENTITY_TABLE[body] ?? full;
929
+ });
930
+ }
931
+ function parseAttrs(raw) {
932
+ const attrs = {};
933
+ ATTR_RE.lastIndex = 0;
934
+ let match;
935
+ while(match = ATTR_RE.exec(raw))attrs[match[1]] = decodeEntities(match[3] ?? match[4] ?? '');
936
+ return attrs;
937
+ }
938
+ function parseXml(input) {
939
+ if (0 === input.trim().length) throw new Error('parseAndroidUITreeXml: input is empty');
940
+ const cleaned = input.replace(/<\?xml[\s\S]*?\?>/g, '').replace(/<!DOCTYPE[\s\S]*?>/gi, '').replace(/<!--[\s\S]*?-->/g, '');
941
+ TAG_RE.lastIndex = 0;
942
+ const stack = [];
943
+ let root;
944
+ let match;
945
+ while(match = TAG_RE.exec(cleaned)){
946
+ const isClose = '/' === match[1];
947
+ const name = match[2];
948
+ if (isClose) {
949
+ const current = stack.pop();
950
+ if (!current || current.name !== name) throw new Error(`parseAndroidUITreeXml: unbalanced close tag </${name}>`);
951
+ continue;
952
+ }
953
+ const element = {
954
+ name,
955
+ attrs: parseAttrs(match[3] ?? ''),
956
+ children: []
957
+ };
958
+ const parent = stack[stack.length - 1];
959
+ if (parent) parent.children.push(element);
960
+ else if (root) throw new Error('parseAndroidUITreeXml: more than one top-level element');
961
+ else root = element;
962
+ if ('/' !== match[4]) stack.push(element);
963
+ }
964
+ if (stack.length > 0) throw new Error('parseAndroidUITreeXml: unclosed element');
965
+ if (!root) throw new Error('parseAndroidUITreeXml: no root element');
966
+ return root;
967
+ }
968
+ function parseBounds(raw, devicePixelRatio) {
969
+ const match = raw ? BOUNDS_RE.exec(raw) : void 0;
970
+ if (!match) return {
971
+ left: 0,
972
+ top: 0,
973
+ width: 0,
974
+ height: 0
975
+ };
976
+ const left = Number.parseInt(match[1], 10);
977
+ const top = Number.parseInt(match[2], 10);
978
+ const right = Number.parseInt(match[3], 10);
979
+ const bottom = Number.parseInt(match[4], 10);
980
+ const ratio = devicePixelRatio > 0 ? devicePixelRatio : 1;
981
+ return {
982
+ left: left / ratio,
983
+ top: top / ratio,
984
+ width: Math.max(0, right - left) / ratio,
985
+ height: Math.max(0, bottom - top) / ratio
986
+ };
987
+ }
988
+ function toUiNode(element, devicePixelRatio) {
989
+ const attrs = {};
990
+ for (const [name, value] of Object.entries(element.attrs))if ('bounds' !== name) attrs[name] = value;
991
+ return {
992
+ type: element.attrs.class || element.name,
993
+ attrs,
994
+ bounds: parseBounds(element.attrs.bounds, devicePixelRatio),
995
+ children: element.children.map((child)=>toUiNode(child, devicePixelRatio))
996
+ };
997
+ }
998
+ function uiautomatorXmlToUiNode(xml, devicePixelRatio) {
999
+ const root = parseXml(xml);
1000
+ if ('hierarchy' === root.name && 1 === root.children.length) return toUiNode(root.children[0], devicePixelRatio);
1001
+ return toUiNode(root, devicePixelRatio);
1002
+ }
1003
+ const ACCESSIBILITY_DUMP_ATTEMPTS = 3;
1004
+ const ACCESSIBILITY_DUMP_TIMEOUT_MS = 5000;
1005
+ const ACCESSIBILITY_DUMP_RETRY_DELAY_MS = 250;
1006
+ const UIAUTOMATOR_LAYOUT_PATH = '/sdcard/midscene_window_dump.xml';
1007
+ const debugUITree = (0, logger_.getDebug)('android:ui-tree');
1008
+ function errorMessage(error) {
1009
+ return error instanceof Error ? error.message : String(error);
1010
+ }
1011
+ function validateHierarchyXml(xml) {
1012
+ if ('string' != typeof xml || !/<hierarchy(?:\s|>)/.test(xml) || !xml.includes('</hierarchy>')) throw new Error('uiautomator did not produce valid hierarchy XML');
1013
+ }
1014
+ function focusedPackageForLogicalDisplay(windowDump, logicalDisplayId) {
1015
+ const displayMatches = [
1016
+ ...windowDump.matchAll(/^\s*Display: mDisplayId=(\d+)\b.*$/gm)
1017
+ ];
1018
+ const displayIndex = displayMatches.findIndex((match)=>Number(match[1]) === logicalDisplayId);
1019
+ if (-1 === displayIndex) return null;
1020
+ const start = displayMatches[displayIndex].index ?? 0;
1021
+ const end = displayMatches[displayIndex + 1]?.index ?? windowDump.length;
1022
+ const displayBlock = windowDump.slice(start, end);
1023
+ for (const field of [
1024
+ 'mCurrentFocus',
1025
+ 'mFocusedApp'
1026
+ ]){
1027
+ const value = displayBlock.match(new RegExp(`^\\s*${field}=([^\\n]*)$`, 'm'))?.[1];
1028
+ const packageName = value?.match(/\bu\d+\s+([A-Za-z0-9_$.-]+)\//)?.[1];
1029
+ if (packageName) return packageName;
1030
+ }
1031
+ return null;
1032
+ }
1033
+ function firstTreePackage(root) {
1034
+ if (root.attrs.package) return root.attrs.package;
1035
+ for (const child of root.children){
1036
+ const packageName = firstTreePackage(child);
1037
+ if (packageName) return packageName;
1038
+ }
1039
+ return null;
1040
+ }
1041
+ function windowDisplayIdsForPackage(windowDump, packageName) {
1042
+ const windowMatches = [
1043
+ ...windowDump.matchAll(/^\s*Window #\d+ Window\{.*$/gm)
1044
+ ];
1045
+ const displayIds = new Set();
1046
+ for(let index = 0; index < windowMatches.length; index++){
1047
+ const start = windowMatches[index].index ?? 0;
1048
+ const end = windowMatches[index + 1]?.index ?? windowDump.length;
1049
+ const windowBlock = windowDump.slice(start, end);
1050
+ const ownerPackage = windowBlock.match(/^\s*mOwnerUid=.*\bpackage=([A-Za-z0-9_$.-]+)\b/m)?.[1];
1051
+ const headerPackage = windowMatches[index][0].match(/\bu\d+\s+([A-Za-z0-9_$.-]+)\//)?.[1];
1052
+ if (ownerPackage !== packageName && headerPackage !== packageName) continue;
1053
+ const displayId = windowBlock.match(/^\s*mDisplayId=(\d+)\b/m)?.[1];
1054
+ if (void 0 !== displayId) displayIds.add(Number(displayId));
1055
+ }
1056
+ return [
1057
+ ...displayIds
1058
+ ];
1059
+ }
1060
+ function uiTreeExtent(root) {
1061
+ let right = root.bounds.left + root.bounds.width;
1062
+ let bottom = root.bounds.top + root.bounds.height;
1063
+ for (const child of root.children){
1064
+ const childExtent = uiTreeExtent(child);
1065
+ right = Math.max(right, childExtent.right);
1066
+ bottom = Math.max(bottom, childExtent.bottom);
1067
+ }
1068
+ return {
1069
+ right,
1070
+ bottom
1071
+ };
1072
+ }
1073
+ async function dumpAccessibilityXml(adb) {
1074
+ await runAdbShellStdoutOrThrow(adb, `rm -f ${UIAUTOMATOR_LAYOUT_PATH}`, {
1075
+ timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
1076
+ });
1077
+ await runAdbShellStdoutOrThrow(adb, `uiautomator dump --compressed ${UIAUTOMATOR_LAYOUT_PATH}`, {
1078
+ timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
1079
+ });
1080
+ const xml = await runAdbShellStdoutOrThrow(adb, `cat ${UIAUTOMATOR_LAYOUT_PATH}`, {
1081
+ timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
1082
+ });
1083
+ validateHierarchyXml(xml);
1084
+ return xml;
1085
+ }
1086
+ async function validateUITreeDisplay(root, options) {
1087
+ if ('number' != typeof options.displayId) return;
1088
+ const { adb, displayId } = options;
1089
+ const displayWindowDump = await runAdbShellStdoutOrThrow(adb, 'dumpsys window displays', {
1090
+ timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
1091
+ });
1092
+ const expectedPackage = focusedPackageForLogicalDisplay(displayWindowDump, displayId);
1093
+ const treePackage = firstTreePackage(root);
1094
+ if (!expectedPackage) throw new Error(`Unable to verify Android UI tree display alignment for displayId=${displayId}: no focused package found on the target display`);
1095
+ if (!treePackage) throw new Error(`Unable to verify Android UI tree display alignment for displayId=${displayId}: accessibility hierarchy has no package`);
1096
+ const allWindowDump = await runAdbShellStdoutOrThrow(adb, 'dumpsys window windows', {
1097
+ timeout: ACCESSIBILITY_DUMP_TIMEOUT_MS
1098
+ });
1099
+ const treePackageDisplayIds = windowDisplayIdsForPackage(allWindowDump, treePackage);
1100
+ if (0 === treePackageDisplayIds.length) throw new Error(`Unable to verify Android UI tree display alignment for displayId=${displayId}: no window display ownership found for accessibility package ${treePackage}`);
1101
+ if (!treePackageDisplayIds.includes(displayId)) throw new Error(`Android UI tree display mismatch for displayId=${displayId}: expected focused package ${expectedPackage}, but accessibility hierarchy belongs to ${treePackage} on displayId=${treePackageDisplayIds.join(',')}`);
1102
+ const targetSize = await options.getDisplaySize();
1103
+ const extent = uiTreeExtent(root);
1104
+ const tolerance = 1;
1105
+ if (extent.right > targetSize.width + tolerance || extent.bottom > targetSize.height + tolerance) throw new Error(`Android UI tree bounds exceed displayId=${displayId}: tree ${Math.round(extent.right)}x${Math.round(extent.bottom)} is outside target ${targetSize.width}x${targetSize.height}`);
1106
+ }
1107
+ async function captureAndroidUITree(options) {
1108
+ const failures = [];
1109
+ for(let attempt = 1; attempt <= ACCESSIBILITY_DUMP_ATTEMPTS; attempt++){
1110
+ const startedAt = Date.now();
1111
+ try {
1112
+ const xml = await dumpAccessibilityXml(options.adb);
1113
+ const capturedAt = Date.now();
1114
+ const root = uiautomatorXmlToUiNode(xml, options.devicePixelRatio);
1115
+ await validateUITreeDisplay(root, options);
1116
+ debugUITree('capture source=%s attempt=%d durationMs=%d bytes=%d', 'uiautomator', attempt, Date.now() - startedAt, xml.length);
1117
+ return {
1118
+ platform: 'android',
1119
+ capturedAt,
1120
+ root
1121
+ };
1122
+ } catch (error) {
1123
+ const failure = `uiautomator attempt ${attempt}/${ACCESSIBILITY_DUMP_ATTEMPTS}: ${errorMessage(error)}`;
1124
+ failures.push(failure);
1125
+ debugUITree('capture failed source=%s attempt=%d/%d durationMs=%d error=%s', 'uiautomator', attempt, ACCESSIBILITY_DUMP_ATTEMPTS, Date.now() - startedAt, errorMessage(error));
1126
+ if (attempt < ACCESSIBILITY_DUMP_ATTEMPTS) await (0, core_utils_namespaceObject.sleep)(ACCESSIBILITY_DUMP_RETRY_DELAY_MS);
1127
+ }
1128
+ }
1129
+ throw new Error(`Unable to capture Android UI tree after ${failures.length} attempt(s): ${failures.join('; ')}`);
1130
+ }
898
1131
  function device_define_property(obj, key, value) {
899
1132
  if (key in obj) Object.defineProperty(obj, key, {
900
1133
  value: value,
@@ -914,6 +1147,35 @@ ${stdout ? truncateAdbShellStream(stdout, 'stdout') : EMPTY_ADB_SHELL_STDOUT}`;
914
1147
  const warnDevice = (0, logger_.getDebug)('android:device', {
915
1148
  console: true
916
1149
  });
1150
+ function displayViewportForLogicalDisplay(displayDump, logicalDisplayId) {
1151
+ for (const viewportMatch of displayDump.matchAll(/DisplayViewport\{([^}]*)\}/g)){
1152
+ const viewport = viewportMatch[1];
1153
+ const displayId = viewport.match(/\bdisplayId=(\d+)\b/)?.[1];
1154
+ if (Number(displayId) === logicalDisplayId) return viewportMatch[0];
1155
+ }
1156
+ return null;
1157
+ }
1158
+ function displayInfoLineForPhysicalDisplay(displayDump, physicalDisplayId) {
1159
+ const lineRegex = new RegExp(`^.*uniqueId ["']local:${physicalDisplayId}["'].*$`, 'm');
1160
+ return displayDump.match(lineRegex)?.[0] ?? null;
1161
+ }
1162
+ function physicalDisplayIdForLogicalDisplay(displayDump, logicalDisplayId) {
1163
+ const viewport = displayViewportForLogicalDisplay(displayDump, logicalDisplayId);
1164
+ if (viewport) {
1165
+ const physicalId = viewport.match(/\buniqueId=['"]local:(\d+)['"]/)?.[1];
1166
+ if (physicalId) return physicalId;
1167
+ }
1168
+ const lines = displayDump.split(/\r?\n/);
1169
+ for(let index = 0; index < lines.length; index++){
1170
+ const displayId = lines[index].match(/^\s*mDisplayId=(\d+)\b/)?.[1];
1171
+ if (Number(displayId) === logicalDisplayId) for(let cursor = index + 1; cursor < lines.length; cursor++){
1172
+ if (/^\s*mDisplayId=\d+\b/.test(lines[cursor])) break;
1173
+ const physicalId = lines[cursor].match(/mBaseDisplayInfo=.*\buniqueId "local:(\d+)"/)?.[1];
1174
+ if (physicalId) return physicalId;
1175
+ }
1176
+ }
1177
+ return null;
1178
+ }
917
1179
  function escapeForShell(text) {
918
1180
  return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
919
1181
  }
@@ -1201,6 +1463,15 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1201
1463
  children: []
1202
1464
  };
1203
1465
  }
1466
+ async getUITree() {
1467
+ await this.initializeDevicePixelRatio();
1468
+ return captureAndroidUITree({
1469
+ adb: await this.getAdb(),
1470
+ devicePixelRatio: this.devicePixelRatio,
1471
+ displayId: this.options?.displayId,
1472
+ getDisplaySize: ()=>this.size()
1473
+ });
1474
+ }
1204
1475
  async getScreenSize() {
1205
1476
  const shouldCache = !(this.options?.alwaysRefreshScreenInfo ?? false);
1206
1477
  debugDevice(`getScreenSize: alwaysRefreshScreenInfo=${this.options?.alwaysRefreshScreenInfo}, shouldCache=${shouldCache}, hasCachedSize=${!!this.cachedScreenSize}`);
@@ -1209,13 +1480,10 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1209
1480
  if ('number' == typeof this.options?.displayId) try {
1210
1481
  const stdout = await adb.shell('dumpsys display');
1211
1482
  if (this.options?.usePhysicalDisplayIdForDisplayLookup) {
1212
- const physicalDisplayId = await this.getPhysicalDisplayId();
1483
+ const physicalDisplayId = await this.resolvePhysicalDisplayId(stdout);
1213
1484
  if (physicalDisplayId) {
1214
- const lineRegex = new RegExp(`^.*uniqueId \"local:${physicalDisplayId}\".*$
1215
- `, 'm');
1216
- const lineMatch = stdout.match(lineRegex);
1217
- if (lineMatch) {
1218
- const targetLine = lineMatch[0];
1485
+ const targetLine = displayInfoLineForPhysicalDisplay(stdout, physicalDisplayId);
1486
+ if (targetLine) {
1219
1487
  const realMatch = targetLine.match(/real (\d+) x (\d+)/);
1220
1488
  const rotationMatch = targetLine.match(/rotation (\d+)/);
1221
1489
  if (realMatch && rotationMatch) {
@@ -1236,10 +1504,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1236
1504
  }
1237
1505
  }
1238
1506
  } else {
1239
- const viewportRegex = new RegExp(`DisplayViewport{[^}]*displayId=${this.options.displayId}[^}]*}`, 'g');
1240
- const match = stdout.match(viewportRegex);
1241
- if (match) {
1242
- const targetLine = match[0];
1507
+ const targetLine = displayViewportForLogicalDisplay(stdout, this.options.displayId);
1508
+ if (targetLine) {
1243
1509
  const physicalFrameMatch = targetLine.match(/physicalFrame=Rect\(\d+, \d+ - (\d+), (\d+)\)/);
1244
1510
  const orientationMatch = targetLine.match(/orientation=(\d+)/);
1245
1511
  if (physicalFrameMatch && orientationMatch) {
@@ -1305,13 +1571,10 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1305
1571
  if ('number' == typeof this.options?.displayId) try {
1306
1572
  const stdout = await adb.shell('dumpsys display');
1307
1573
  if (this.options?.usePhysicalDisplayIdForDisplayLookup) {
1308
- const physicalDisplayId = await this.getPhysicalDisplayId();
1574
+ const physicalDisplayId = await this.resolvePhysicalDisplayId(stdout);
1309
1575
  if (physicalDisplayId) {
1310
- const lineRegex = new RegExp(`^.*uniqueId \"local:${physicalDisplayId}\".*$
1311
- `, 'm');
1312
- const lineMatch = stdout.match(lineRegex);
1313
- if (lineMatch) {
1314
- const targetLine = lineMatch[0];
1576
+ const targetLine = displayInfoLineForPhysicalDisplay(stdout, physicalDisplayId);
1577
+ if (targetLine) {
1315
1578
  const densityMatch = targetLine.match(/density (\d+)/);
1316
1579
  if (densityMatch) {
1317
1580
  const density = Number(densityMatch[1]);
@@ -1449,7 +1712,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1449
1712
  debugDevice('screenshotBase64 end (scrcpy mode)');
1450
1713
  return result;
1451
1714
  } catch (error) {
1452
- debugDevice(`Scrcpy screenshot failed, falling back to standard ADB method.\nError: ${error}`);
1715
+ warnDevice(`Scrcpy screenshot failed, falling back to standard ADB method.\nError: ${error}`);
1453
1716
  }
1454
1717
  const adb = await this.getAdb();
1455
1718
  let screenshotBuffer;
@@ -1936,6 +2199,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1936
2199
  if ('number' == typeof this.options?.displayId && 0 !== this.options.displayId) warnDevice(`yadb ${operation} cannot target display ${this.options.displayId}; it will act on the default display.`);
1937
2200
  }
1938
2201
  async getPhysicalDisplayId() {
2202
+ return this.resolvePhysicalDisplayId();
2203
+ }
2204
+ async resolvePhysicalDisplayId(displayDump) {
1939
2205
  if (void 0 !== this.cachedPhysicalDisplayId) return this.cachedPhysicalDisplayId;
1940
2206
  if ('number' != typeof this.options?.displayId) {
1941
2207
  this.cachedPhysicalDisplayId = null;
@@ -1943,6 +2209,13 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1943
2209
  }
1944
2210
  const adb = await this.getAdb();
1945
2211
  try {
2212
+ const resolvedDisplayDump = displayDump ?? await adb.shell('dumpsys display');
2213
+ const logicalDisplayPhysicalId = physicalDisplayIdForLogicalDisplay(resolvedDisplayDump, this.options.displayId);
2214
+ if (logicalDisplayPhysicalId) {
2215
+ this.cachedPhysicalDisplayId = logicalDisplayPhysicalId;
2216
+ debugDevice(`Found and cached physical display ID: ${logicalDisplayPhysicalId} for logical display ID: ${this.options.displayId}`);
2217
+ return this.cachedPhysicalDisplayId;
2218
+ }
1946
2219
  const stdout = await adb.shell(`dumpsys SurfaceFlinger --display-id ${this.options.displayId}`);
1947
2220
  const regex = new RegExp(`Display (\\d+) \\(HWC display ${this.options.displayId}\\):`);
1948
2221
  const displayMatch = stdout.match(regex);
@@ -2327,7 +2600,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
2327
2600
  const tools = new AndroidMidsceneTools();
2328
2601
  (0, cli_namespaceObject.runToolsCLI)(tools, 'midscene-android', {
2329
2602
  stripPrefix: 'android_',
2330
- version: "1.10.8",
2603
+ version: "1.10.9-beta-20260731103801.0",
2331
2604
  extraCommands: (0, core_namespaceObject.createReportCliCommands)()
2332
2605
  }).catch((e)=>{
2333
2606
  process.exit((0, cli_namespaceObject.reportCLIError)(e));