@midscene/android 1.6.4 → 1.7.1-beta-20260408073050.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.
Binary file
package/dist/es/cli.mjs CHANGED
@@ -675,6 +675,18 @@ const defaultFastScrollDuration = 100;
675
675
  const defaultNormalScrollDuration = 1000;
676
676
  const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
677
677
  const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
678
+ const MIDSCENE_IME_PACKAGE = 'com.midscene.ime';
679
+ const MIDSCENE_IME_ID = `${MIDSCENE_IME_PACKAGE}/.MidsceneIME`;
680
+ const MIDSCENE_IME_DISMISS_ACTION = `${MIDSCENE_IME_PACKAGE}.DISMISS`;
681
+ const AUTO_DISMISS_KEYBOARD_SCHEMA = z.union([
682
+ z.boolean(),
683
+ z["enum"]([
684
+ 'back',
685
+ 'esc',
686
+ 'midscene-ime',
687
+ 'midscene-ime-auto-install'
688
+ ])
689
+ ]).optional().describe("Dismiss the keyboard after input. Use `true` for the default ESC-based behavior, `false` to leave the keyboard open, `'back'`/`'esc'` for explicit key-event strategies, or `'midscene-ime'` / `'midscene-ime-auto-install'` for the zero-key-event helper IME flow.");
678
690
  const debugDevice = (0, logger_.getDebug)('android:device');
679
691
  function escapeForShell(text) {
680
692
  return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
@@ -698,7 +710,7 @@ class AndroidDevice {
698
710
  interfaceAlias: 'aiInput',
699
711
  paramSchema: z.object({
700
712
  value: z.string().describe('The text to input. Provide the final content for replace/append modes, or an empty string when using clear mode to remove existing text.'),
701
- autoDismissKeyboard: z.boolean().optional().describe('If true, the keyboard will be dismissed after the input is completed. Do not set it unless the user asks you to do so.'),
713
+ autoDismissKeyboard: AUTO_DISMISS_KEYBOARD_SCHEMA,
702
714
  mode: z.preprocess((val)=>'append' === val ? 'typeOnly' : val, z["enum"]([
703
715
  'replace',
704
716
  'clear',
@@ -944,6 +956,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
944
956
  setAppNameMapping(mapping) {
945
957
  this.appNameMapping = mapping;
946
958
  }
959
+ setInstallApprovalHandler(handler) {
960
+ this.installApprovalHandler = handler;
961
+ }
947
962
  resolvePackageName(appName) {
948
963
  const normalizedAppName = normalizeForComparison(appName);
949
964
  return this.appNameMapping[normalizedAppName];
@@ -1482,7 +1497,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1482
1497
  if (!text) return;
1483
1498
  const adb = await this.getAdb();
1484
1499
  const IME_STRATEGY = (this.options?.imeStrategy || globalConfigManager.getEnvConfigValue(MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
1485
- const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
1500
+ const autoDismiss = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? 'esc';
1501
+ const shouldAutoDismissKeyboard = false !== autoDismiss;
1486
1502
  const useYadb = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && this.shouldUseYadbForText(text);
1487
1503
  if (useYadb) await this.execYadb(escapeForShell(text), {
1488
1504
  overwrite: options?.overwrite
@@ -1600,6 +1616,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1600
1616
  }
1601
1617
  this.connectingAdb = null;
1602
1618
  this.yadbPushed = false;
1619
+ this.midsceneImeInstalled = false;
1603
1620
  }
1604
1621
  async getTimestamp() {
1605
1622
  const adb = await this.getAdb();
@@ -1699,43 +1716,135 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1699
1716
  return null;
1700
1717
  }
1701
1718
  }
1719
+ resolveAutoDismissKeyboard(options) {
1720
+ const raw = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard;
1721
+ if (true === raw) ;
1722
+ else if (false === raw) return false;
1723
+ else if (void 0 !== raw) return raw;
1724
+ const legacy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy;
1725
+ if ('back-first' === legacy) return 'back';
1726
+ return 'esc';
1727
+ }
1728
+ async waitForKeyboardHidden(adb, timeoutMs) {
1729
+ const startTime = Date.now();
1730
+ const intervalMs = 100;
1731
+ while(Date.now() - startTime < timeoutMs){
1732
+ await sleep(intervalMs);
1733
+ const currentStatus = await adb.isSoftKeyboardPresent();
1734
+ const isStillShown = 'boolean' == typeof currentStatus ? currentStatus : currentStatus?.isKeyboardShown;
1735
+ if (!isStillShown) return true;
1736
+ }
1737
+ return false;
1738
+ }
1739
+ async ensureMidsceneImeInstalled(adb, autoApprove = false) {
1740
+ if (this.midsceneImeInstalled) return;
1741
+ try {
1742
+ const output = await adb.shell(`pm list packages ${MIDSCENE_IME_PACKAGE}`);
1743
+ if (output.includes(MIDSCENE_IME_PACKAGE)) {
1744
+ await adb.shell(`ime enable ${MIDSCENE_IME_ID}`);
1745
+ this.midsceneImeInstalled = true;
1746
+ return;
1747
+ }
1748
+ } catch {}
1749
+ const androidPkgJson = (0, external_node_module_.createRequire)(import.meta.url).resolve('@midscene/android/package.json');
1750
+ const apkPath = external_node_path_["default"].join(external_node_path_["default"].dirname(androidPkgJson), 'bin', 'midscene-ime.apk');
1751
+ if (!external_node_fs_["default"].existsSync(apkPath)) throw new Error(`MidsceneIME APK not found at ${apkPath}. Run the @midscene/android prepack/build step to generate bin/midscene-ime.apk before using the MidsceneIME keyboard-dismiss strategy.`);
1752
+ try {
1753
+ await adb.install(apkPath);
1754
+ await adb.shell(`ime enable ${MIDSCENE_IME_ID}`);
1755
+ this.midsceneImeInstalled = true;
1756
+ return;
1757
+ } catch (e) {
1758
+ const errorMsg = String(e);
1759
+ if (!errorMsg.includes('INSTALL_FAILED_USER_RESTRICTED')) throw e;
1760
+ if (!autoApprove || !this.installApprovalHandler) throw new Error(`APK installation was blocked by the device (INSTALL_FAILED_USER_RESTRICTED). On some ROMs (e.g. MIUI), enable "Install via USB" in Developer Options, or use autoDismissKeyboard: 'midscene-ime-auto-install' with an agent to auto-approve. Original error: ${e}`);
1761
+ debugDevice('Install blocked by device, retrying with approval handler...');
1762
+ }
1763
+ await adb.push(apkPath, '/data/local/tmp/midscene-ime.apk');
1764
+ const installPromise = adb.shell('pm install -r -t /data/local/tmp/midscene-ime.apk');
1765
+ await sleep(2000);
1766
+ try {
1767
+ await this.installApprovalHandler();
1768
+ } catch (approvalError) {
1769
+ debugDevice(`Install approval handler failed: ${approvalError}`);
1770
+ }
1771
+ try {
1772
+ const result = await installPromise;
1773
+ if (result.includes('Success')) {
1774
+ await adb.shell(`ime enable ${MIDSCENE_IME_ID}`);
1775
+ this.midsceneImeInstalled = true;
1776
+ debugDevice('MidsceneIME installed via auto-approval');
1777
+ return;
1778
+ }
1779
+ throw new Error(`Install failed: ${result}`);
1780
+ } catch (e) {
1781
+ throw new Error(`Failed to install MidsceneIME even with auto-approval. Please manually enable "Install via USB" in Developer Options. Original error: ${e}`);
1782
+ }
1783
+ }
1784
+ async hideKeyboardViaMidsceneIme(adb, timeoutMs) {
1785
+ const originalIme = await adb.shell('settings get secure default_input_method');
1786
+ const trimmedOriginalIme = originalIme.trim();
1787
+ try {
1788
+ await adb.shell(`ime set ${MIDSCENE_IME_ID}`);
1789
+ await sleep(500);
1790
+ await adb.shell(`am broadcast -a ${MIDSCENE_IME_DISMISS_ACTION}`);
1791
+ await sleep(500);
1792
+ if (trimmedOriginalIme && trimmedOriginalIme !== MIDSCENE_IME_ID) await adb.shell(`ime set ${trimmedOriginalIme}`);
1793
+ if (await this.waitForKeyboardHidden(adb, timeoutMs)) {
1794
+ debugDevice('Keyboard hidden successfully with MidsceneIME');
1795
+ return true;
1796
+ }
1797
+ debugDevice('MidsceneIME did not dismiss keyboard');
1798
+ return false;
1799
+ } catch (e) {
1800
+ if (trimmedOriginalIme && trimmedOriginalIme !== MIDSCENE_IME_ID) try {
1801
+ await adb.shell(`ime set ${trimmedOriginalIme}`);
1802
+ } catch {}
1803
+ throw e;
1804
+ }
1805
+ }
1702
1806
  async hideKeyboard(options, timeoutMs = 1000) {
1703
1807
  const adb = await this.getAdb();
1704
- const keyboardDismissStrategy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy ?? 'esc-first';
1808
+ const strategy = this.resolveAutoDismissKeyboard(options);
1809
+ if (false === strategy) return false;
1705
1810
  const keyboardStatus = await adb.isSoftKeyboardPresent();
1706
1811
  const isKeyboardShown = 'boolean' == typeof keyboardStatus ? keyboardStatus : keyboardStatus?.isKeyboardShown;
1707
1812
  if (!isKeyboardShown) {
1708
1813
  debugDevice('Keyboard has no UI; no closing necessary');
1709
1814
  return false;
1710
1815
  }
1711
- const keyCodes = 'back-first' === keyboardDismissStrategy ? [
1712
- 4,
1713
- 111
1714
- ] : [
1715
- 111,
1716
- 4
1717
- ];
1718
- for (const keyCode of keyCodes){
1719
- await adb.keyevent(keyCode);
1720
- const startTime = Date.now();
1721
- const intervalMs = 100;
1722
- while(Date.now() - startTime < timeoutMs){
1723
- await sleep(intervalMs);
1724
- const currentStatus = await adb.isSoftKeyboardPresent();
1725
- const isStillShown = 'boolean' == typeof currentStatus ? currentStatus : currentStatus?.isKeyboardShown;
1726
- if (!isStillShown) {
1727
- debugDevice(`Keyboard hidden successfully with keycode ${keyCode}`);
1728
- return true;
1729
- }
1816
+ if ('midscene-ime' === strategy || 'midscene-ime-auto-install' === strategy) {
1817
+ const autoApprove = 'midscene-ime-auto-install' === strategy;
1818
+ try {
1819
+ await this.ensureMidsceneImeInstalled(adb, autoApprove);
1820
+ const hidden = await this.hideKeyboardViaMidsceneIme(adb, timeoutMs);
1821
+ if (!hidden) console.warn('Warning: MidsceneIME was invoked but the software keyboard is still visible');
1822
+ return hidden;
1823
+ } catch (e) {
1824
+ const installHint = autoApprove ? 'Failed to use MidsceneIME auto-install for keyboard dismissal. MidsceneIME will not fall back to ESC/BACK because that can clear user input. Please enable USB installs on the device or preinstall the helper APK manually: adb install <path-to-midscene-ime.apk>.' : "Failed to use MidsceneIME for keyboard dismissal. Please install the APK manually: adb install <path-to-midscene-ime.apk>, or use autoDismissKeyboard: 'midscene-ime-auto-install' with an AndroidAgent to auto-approve installation prompts.";
1825
+ throw new Error(`${installHint} Original error: ${e}`);
1730
1826
  }
1731
- debugDevice(`Keyboard still shown after keycode ${keyCode}, trying next key`);
1732
1827
  }
1733
- console.warn('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1828
+ const keyCode = 'back' === strategy ? 4 : 111;
1829
+ await adb.keyevent(keyCode);
1830
+ if (await this.waitForKeyboardHidden(adb, timeoutMs)) {
1831
+ debugDevice(`Keyboard hidden successfully with keycode ${keyCode} (${strategy})`);
1832
+ return true;
1833
+ }
1834
+ const fallbackKeyCode = 'back' === strategy ? 111 : 4;
1835
+ await adb.keyevent(fallbackKeyCode);
1836
+ if (await this.waitForKeyboardHidden(adb, timeoutMs)) {
1837
+ debugDevice(`Keyboard hidden successfully with fallback keycode ${fallbackKeyCode}`);
1838
+ return true;
1839
+ }
1840
+ console.warn('Warning: Failed to hide the software keyboard after trying all strategies');
1734
1841
  return false;
1735
1842
  }
1736
1843
  constructor(deviceId, options){
1737
1844
  device_define_property(this, "deviceId", void 0);
1738
1845
  device_define_property(this, "yadbPushed", false);
1846
+ device_define_property(this, "midsceneImeInstalled", false);
1847
+ device_define_property(this, "installApprovalHandler", void 0);
1739
1848
  device_define_property(this, "devicePixelRatio", 1);
1740
1849
  device_define_property(this, "devicePixelRatioInitialized", false);
1741
1850
  device_define_property(this, "adb", null);
@@ -1883,6 +1992,9 @@ class AndroidAgent extends Agent {
1883
1992
  super(device, opts), agent_define_property(this, "back", void 0), agent_define_property(this, "home", void 0), agent_define_property(this, "recentApps", void 0), agent_define_property(this, "appNameMapping", void 0);
1884
1993
  this.appNameMapping = mergeAndNormalizeAppNameMapping(defaultAppNameMapping, opts?.appNameMapping);
1885
1994
  device.setAppNameMapping(this.appNameMapping);
1995
+ device.setInstallApprovalHandler(async ()=>{
1996
+ await this.aiAct('A system dialog is asking to approve USB installation. Click the "Continue Install", "Allow", "Install" or similar approval button.');
1997
+ });
1886
1998
  this.back = this.createActionWrapper('AndroidBackButton');
1887
1999
  this.home = this.createActionWrapper('AndroidHomeButton');
1888
2000
  this.recentApps = this.createActionWrapper('AndroidRecentAppsButton');
@@ -1956,7 +2068,7 @@ class AndroidMidsceneTools extends BaseMidsceneTools {
1956
2068
  const tools = new AndroidMidsceneTools();
1957
2069
  runToolsCLI(tools, 'midscene-android', {
1958
2070
  stripPrefix: 'android_',
1959
- version: "1.6.4",
2071
+ version: "1.7.1-beta-20260408073050.0",
1960
2072
  extraCommands: createReportCliCommands()
1961
2073
  }).catch((e)=>{
1962
2074
  if (!(e instanceof CLIError)) console.error(e);
package/dist/es/index.mjs CHANGED
@@ -578,6 +578,18 @@ const defaultFastScrollDuration = 100;
578
578
  const defaultNormalScrollDuration = 1000;
579
579
  const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
580
580
  const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
581
+ const MIDSCENE_IME_PACKAGE = 'com.midscene.ime';
582
+ const MIDSCENE_IME_ID = `${MIDSCENE_IME_PACKAGE}/.MidsceneIME`;
583
+ const MIDSCENE_IME_DISMISS_ACTION = `${MIDSCENE_IME_PACKAGE}.DISMISS`;
584
+ const AUTO_DISMISS_KEYBOARD_SCHEMA = z.union([
585
+ z.boolean(),
586
+ z["enum"]([
587
+ 'back',
588
+ 'esc',
589
+ 'midscene-ime',
590
+ 'midscene-ime-auto-install'
591
+ ])
592
+ ]).optional().describe("Dismiss the keyboard after input. Use `true` for the default ESC-based behavior, `false` to leave the keyboard open, `'back'`/`'esc'` for explicit key-event strategies, or `'midscene-ime'` / `'midscene-ime-auto-install'` for the zero-key-event helper IME flow.");
581
593
  const debugDevice = (0, logger_.getDebug)('android:device');
582
594
  function escapeForShell(text) {
583
595
  return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
@@ -601,7 +613,7 @@ class AndroidDevice {
601
613
  interfaceAlias: 'aiInput',
602
614
  paramSchema: z.object({
603
615
  value: z.string().describe('The text to input. Provide the final content for replace/append modes, or an empty string when using clear mode to remove existing text.'),
604
- autoDismissKeyboard: z.boolean().optional().describe('If true, the keyboard will be dismissed after the input is completed. Do not set it unless the user asks you to do so.'),
616
+ autoDismissKeyboard: AUTO_DISMISS_KEYBOARD_SCHEMA,
605
617
  mode: z.preprocess((val)=>'append' === val ? 'typeOnly' : val, z["enum"]([
606
618
  'replace',
607
619
  'clear',
@@ -847,6 +859,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
847
859
  setAppNameMapping(mapping) {
848
860
  this.appNameMapping = mapping;
849
861
  }
862
+ setInstallApprovalHandler(handler) {
863
+ this.installApprovalHandler = handler;
864
+ }
850
865
  resolvePackageName(appName) {
851
866
  const normalizedAppName = normalizeForComparison(appName);
852
867
  return this.appNameMapping[normalizedAppName];
@@ -1385,7 +1400,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1385
1400
  if (!text) return;
1386
1401
  const adb = await this.getAdb();
1387
1402
  const IME_STRATEGY = (this.options?.imeStrategy || globalConfigManager.getEnvConfigValue(MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
1388
- const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
1403
+ const autoDismiss = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? 'esc';
1404
+ const shouldAutoDismissKeyboard = false !== autoDismiss;
1389
1405
  const useYadb = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && this.shouldUseYadbForText(text);
1390
1406
  if (useYadb) await this.execYadb(escapeForShell(text), {
1391
1407
  overwrite: options?.overwrite
@@ -1503,6 +1519,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1503
1519
  }
1504
1520
  this.connectingAdb = null;
1505
1521
  this.yadbPushed = false;
1522
+ this.midsceneImeInstalled = false;
1506
1523
  }
1507
1524
  async getTimestamp() {
1508
1525
  const adb = await this.getAdb();
@@ -1602,43 +1619,135 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1602
1619
  return null;
1603
1620
  }
1604
1621
  }
1622
+ resolveAutoDismissKeyboard(options) {
1623
+ const raw = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard;
1624
+ if (true === raw) ;
1625
+ else if (false === raw) return false;
1626
+ else if (void 0 !== raw) return raw;
1627
+ const legacy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy;
1628
+ if ('back-first' === legacy) return 'back';
1629
+ return 'esc';
1630
+ }
1631
+ async waitForKeyboardHidden(adb, timeoutMs) {
1632
+ const startTime = Date.now();
1633
+ const intervalMs = 100;
1634
+ while(Date.now() - startTime < timeoutMs){
1635
+ await sleep(intervalMs);
1636
+ const currentStatus = await adb.isSoftKeyboardPresent();
1637
+ const isStillShown = 'boolean' == typeof currentStatus ? currentStatus : currentStatus?.isKeyboardShown;
1638
+ if (!isStillShown) return true;
1639
+ }
1640
+ return false;
1641
+ }
1642
+ async ensureMidsceneImeInstalled(adb, autoApprove = false) {
1643
+ if (this.midsceneImeInstalled) return;
1644
+ try {
1645
+ const output = await adb.shell(`pm list packages ${MIDSCENE_IME_PACKAGE}`);
1646
+ if (output.includes(MIDSCENE_IME_PACKAGE)) {
1647
+ await adb.shell(`ime enable ${MIDSCENE_IME_ID}`);
1648
+ this.midsceneImeInstalled = true;
1649
+ return;
1650
+ }
1651
+ } catch {}
1652
+ const androidPkgJson = (0, external_node_module_.createRequire)(import.meta.url).resolve('@midscene/android/package.json');
1653
+ const apkPath = external_node_path_["default"].join(external_node_path_["default"].dirname(androidPkgJson), 'bin', 'midscene-ime.apk');
1654
+ if (!external_node_fs_["default"].existsSync(apkPath)) throw new Error(`MidsceneIME APK not found at ${apkPath}. Run the @midscene/android prepack/build step to generate bin/midscene-ime.apk before using the MidsceneIME keyboard-dismiss strategy.`);
1655
+ try {
1656
+ await adb.install(apkPath);
1657
+ await adb.shell(`ime enable ${MIDSCENE_IME_ID}`);
1658
+ this.midsceneImeInstalled = true;
1659
+ return;
1660
+ } catch (e) {
1661
+ const errorMsg = String(e);
1662
+ if (!errorMsg.includes('INSTALL_FAILED_USER_RESTRICTED')) throw e;
1663
+ if (!autoApprove || !this.installApprovalHandler) throw new Error(`APK installation was blocked by the device (INSTALL_FAILED_USER_RESTRICTED). On some ROMs (e.g. MIUI), enable "Install via USB" in Developer Options, or use autoDismissKeyboard: 'midscene-ime-auto-install' with an agent to auto-approve. Original error: ${e}`);
1664
+ debugDevice('Install blocked by device, retrying with approval handler...');
1665
+ }
1666
+ await adb.push(apkPath, '/data/local/tmp/midscene-ime.apk');
1667
+ const installPromise = adb.shell('pm install -r -t /data/local/tmp/midscene-ime.apk');
1668
+ await sleep(2000);
1669
+ try {
1670
+ await this.installApprovalHandler();
1671
+ } catch (approvalError) {
1672
+ debugDevice(`Install approval handler failed: ${approvalError}`);
1673
+ }
1674
+ try {
1675
+ const result = await installPromise;
1676
+ if (result.includes('Success')) {
1677
+ await adb.shell(`ime enable ${MIDSCENE_IME_ID}`);
1678
+ this.midsceneImeInstalled = true;
1679
+ debugDevice('MidsceneIME installed via auto-approval');
1680
+ return;
1681
+ }
1682
+ throw new Error(`Install failed: ${result}`);
1683
+ } catch (e) {
1684
+ throw new Error(`Failed to install MidsceneIME even with auto-approval. Please manually enable "Install via USB" in Developer Options. Original error: ${e}`);
1685
+ }
1686
+ }
1687
+ async hideKeyboardViaMidsceneIme(adb, timeoutMs) {
1688
+ const originalIme = await adb.shell('settings get secure default_input_method');
1689
+ const trimmedOriginalIme = originalIme.trim();
1690
+ try {
1691
+ await adb.shell(`ime set ${MIDSCENE_IME_ID}`);
1692
+ await sleep(500);
1693
+ await adb.shell(`am broadcast -a ${MIDSCENE_IME_DISMISS_ACTION}`);
1694
+ await sleep(500);
1695
+ if (trimmedOriginalIme && trimmedOriginalIme !== MIDSCENE_IME_ID) await adb.shell(`ime set ${trimmedOriginalIme}`);
1696
+ if (await this.waitForKeyboardHidden(adb, timeoutMs)) {
1697
+ debugDevice('Keyboard hidden successfully with MidsceneIME');
1698
+ return true;
1699
+ }
1700
+ debugDevice('MidsceneIME did not dismiss keyboard');
1701
+ return false;
1702
+ } catch (e) {
1703
+ if (trimmedOriginalIme && trimmedOriginalIme !== MIDSCENE_IME_ID) try {
1704
+ await adb.shell(`ime set ${trimmedOriginalIme}`);
1705
+ } catch {}
1706
+ throw e;
1707
+ }
1708
+ }
1605
1709
  async hideKeyboard(options, timeoutMs = 1000) {
1606
1710
  const adb = await this.getAdb();
1607
- const keyboardDismissStrategy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy ?? 'esc-first';
1711
+ const strategy = this.resolveAutoDismissKeyboard(options);
1712
+ if (false === strategy) return false;
1608
1713
  const keyboardStatus = await adb.isSoftKeyboardPresent();
1609
1714
  const isKeyboardShown = 'boolean' == typeof keyboardStatus ? keyboardStatus : keyboardStatus?.isKeyboardShown;
1610
1715
  if (!isKeyboardShown) {
1611
1716
  debugDevice('Keyboard has no UI; no closing necessary');
1612
1717
  return false;
1613
1718
  }
1614
- const keyCodes = 'back-first' === keyboardDismissStrategy ? [
1615
- 4,
1616
- 111
1617
- ] : [
1618
- 111,
1619
- 4
1620
- ];
1621
- for (const keyCode of keyCodes){
1622
- await adb.keyevent(keyCode);
1623
- const startTime = Date.now();
1624
- const intervalMs = 100;
1625
- while(Date.now() - startTime < timeoutMs){
1626
- await sleep(intervalMs);
1627
- const currentStatus = await adb.isSoftKeyboardPresent();
1628
- const isStillShown = 'boolean' == typeof currentStatus ? currentStatus : currentStatus?.isKeyboardShown;
1629
- if (!isStillShown) {
1630
- debugDevice(`Keyboard hidden successfully with keycode ${keyCode}`);
1631
- return true;
1632
- }
1719
+ if ('midscene-ime' === strategy || 'midscene-ime-auto-install' === strategy) {
1720
+ const autoApprove = 'midscene-ime-auto-install' === strategy;
1721
+ try {
1722
+ await this.ensureMidsceneImeInstalled(adb, autoApprove);
1723
+ const hidden = await this.hideKeyboardViaMidsceneIme(adb, timeoutMs);
1724
+ if (!hidden) console.warn('Warning: MidsceneIME was invoked but the software keyboard is still visible');
1725
+ return hidden;
1726
+ } catch (e) {
1727
+ const installHint = autoApprove ? 'Failed to use MidsceneIME auto-install for keyboard dismissal. MidsceneIME will not fall back to ESC/BACK because that can clear user input. Please enable USB installs on the device or preinstall the helper APK manually: adb install <path-to-midscene-ime.apk>.' : "Failed to use MidsceneIME for keyboard dismissal. Please install the APK manually: adb install <path-to-midscene-ime.apk>, or use autoDismissKeyboard: 'midscene-ime-auto-install' with an AndroidAgent to auto-approve installation prompts.";
1728
+ throw new Error(`${installHint} Original error: ${e}`);
1633
1729
  }
1634
- debugDevice(`Keyboard still shown after keycode ${keyCode}, trying next key`);
1635
1730
  }
1636
- console.warn('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1731
+ const keyCode = 'back' === strategy ? 4 : 111;
1732
+ await adb.keyevent(keyCode);
1733
+ if (await this.waitForKeyboardHidden(adb, timeoutMs)) {
1734
+ debugDevice(`Keyboard hidden successfully with keycode ${keyCode} (${strategy})`);
1735
+ return true;
1736
+ }
1737
+ const fallbackKeyCode = 'back' === strategy ? 111 : 4;
1738
+ await adb.keyevent(fallbackKeyCode);
1739
+ if (await this.waitForKeyboardHidden(adb, timeoutMs)) {
1740
+ debugDevice(`Keyboard hidden successfully with fallback keycode ${fallbackKeyCode}`);
1741
+ return true;
1742
+ }
1743
+ console.warn('Warning: Failed to hide the software keyboard after trying all strategies');
1637
1744
  return false;
1638
1745
  }
1639
1746
  constructor(deviceId, options){
1640
1747
  device_define_property(this, "deviceId", void 0);
1641
1748
  device_define_property(this, "yadbPushed", false);
1749
+ device_define_property(this, "midsceneImeInstalled", false);
1750
+ device_define_property(this, "installApprovalHandler", void 0);
1642
1751
  device_define_property(this, "devicePixelRatio", 1);
1643
1752
  device_define_property(this, "devicePixelRatioInitialized", false);
1644
1753
  device_define_property(this, "adb", null);
@@ -1949,6 +2058,9 @@ class AndroidAgent extends Agent {
1949
2058
  super(device, opts), agent_define_property(this, "back", void 0), agent_define_property(this, "home", void 0), agent_define_property(this, "recentApps", void 0), agent_define_property(this, "appNameMapping", void 0);
1950
2059
  this.appNameMapping = mergeAndNormalizeAppNameMapping(defaultAppNameMapping, opts?.appNameMapping);
1951
2060
  device.setAppNameMapping(this.appNameMapping);
2061
+ device.setInstallApprovalHandler(async ()=>{
2062
+ await this.aiAct('A system dialog is asking to approve USB installation. Click the "Continue Install", "Allow", "Install" or similar approval button.');
2063
+ });
1952
2064
  this.back = this.createActionWrapper('AndroidBackButton');
1953
2065
  this.home = this.createActionWrapper('AndroidHomeButton');
1954
2066
  this.recentApps = this.createActionWrapper('AndroidRecentAppsButton');