@midscene/android 1.7.0 → 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.
package/dist/lib/index.js CHANGED
@@ -612,6 +612,18 @@ var __webpack_exports__ = {};
612
612
  const defaultNormalScrollDuration = 1000;
613
613
  const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
614
614
  const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
615
+ const MIDSCENE_IME_PACKAGE = 'com.midscene.ime';
616
+ const MIDSCENE_IME_ID = `${MIDSCENE_IME_PACKAGE}/.MidsceneIME`;
617
+ const MIDSCENE_IME_DISMISS_ACTION = `${MIDSCENE_IME_PACKAGE}.DISMISS`;
618
+ const AUTO_DISMISS_KEYBOARD_SCHEMA = core_namespaceObject.z.union([
619
+ core_namespaceObject.z.boolean(),
620
+ core_namespaceObject.z["enum"]([
621
+ 'back',
622
+ 'esc',
623
+ 'midscene-ime',
624
+ 'midscene-ime-auto-install'
625
+ ])
626
+ ]).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.");
615
627
  const debugDevice = (0, logger_.getDebug)('android:device');
616
628
  function escapeForShell(text) {
617
629
  return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
@@ -635,7 +647,7 @@ var __webpack_exports__ = {};
635
647
  interfaceAlias: 'aiInput',
636
648
  paramSchema: core_namespaceObject.z.object({
637
649
  value: core_namespaceObject.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.'),
638
- autoDismissKeyboard: core_namespaceObject.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.'),
650
+ autoDismissKeyboard: AUTO_DISMISS_KEYBOARD_SCHEMA,
639
651
  mode: core_namespaceObject.z.preprocess((val)=>'append' === val ? 'typeOnly' : val, core_namespaceObject.z["enum"]([
640
652
  'replace',
641
653
  'clear',
@@ -881,6 +893,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
881
893
  setAppNameMapping(mapping) {
882
894
  this.appNameMapping = mapping;
883
895
  }
896
+ setInstallApprovalHandler(handler) {
897
+ this.installApprovalHandler = handler;
898
+ }
884
899
  resolvePackageName(appName) {
885
900
  const normalizedAppName = (0, shared_utils_namespaceObject.normalizeForComparison)(appName);
886
901
  return this.appNameMapping[normalizedAppName];
@@ -1419,7 +1434,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1419
1434
  if (!text) return;
1420
1435
  const adb = await this.getAdb();
1421
1436
  const IME_STRATEGY = (this.options?.imeStrategy || env_namespaceObject.globalConfigManager.getEnvConfigValue(env_namespaceObject.MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
1422
- const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
1437
+ const autoDismiss = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? 'esc';
1438
+ const shouldAutoDismissKeyboard = false !== autoDismiss;
1423
1439
  const useYadb = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && this.shouldUseYadbForText(text);
1424
1440
  if (useYadb) await this.execYadb(escapeForShell(text), {
1425
1441
  overwrite: options?.overwrite
@@ -1537,6 +1553,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1537
1553
  }
1538
1554
  this.connectingAdb = null;
1539
1555
  this.yadbPushed = false;
1556
+ this.midsceneImeInstalled = false;
1540
1557
  }
1541
1558
  async getTimestamp() {
1542
1559
  const adb = await this.getAdb();
@@ -1636,43 +1653,135 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1636
1653
  return null;
1637
1654
  }
1638
1655
  }
1656
+ resolveAutoDismissKeyboard(options) {
1657
+ const raw = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard;
1658
+ if (true === raw) ;
1659
+ else if (false === raw) return false;
1660
+ else if (void 0 !== raw) return raw;
1661
+ const legacy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy;
1662
+ if ('back-first' === legacy) return 'back';
1663
+ return 'esc';
1664
+ }
1665
+ async waitForKeyboardHidden(adb, timeoutMs) {
1666
+ const startTime = Date.now();
1667
+ const intervalMs = 100;
1668
+ while(Date.now() - startTime < timeoutMs){
1669
+ await (0, utils_namespaceObject.sleep)(intervalMs);
1670
+ const currentStatus = await adb.isSoftKeyboardPresent();
1671
+ const isStillShown = 'boolean' == typeof currentStatus ? currentStatus : currentStatus?.isKeyboardShown;
1672
+ if (!isStillShown) return true;
1673
+ }
1674
+ return false;
1675
+ }
1676
+ async ensureMidsceneImeInstalled(adb, autoApprove = false) {
1677
+ if (this.midsceneImeInstalled) return;
1678
+ try {
1679
+ const output = await adb.shell(`pm list packages ${MIDSCENE_IME_PACKAGE}`);
1680
+ if (output.includes(MIDSCENE_IME_PACKAGE)) {
1681
+ await adb.shell(`ime enable ${MIDSCENE_IME_ID}`);
1682
+ this.midsceneImeInstalled = true;
1683
+ return;
1684
+ }
1685
+ } catch {}
1686
+ const androidPkgJson = (0, external_node_module_.createRequire)(__rslib_import_meta_url__).resolve('@midscene/android/package.json');
1687
+ const apkPath = external_node_path_default().join(external_node_path_default().dirname(androidPkgJson), 'bin', 'midscene-ime.apk');
1688
+ 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.`);
1689
+ try {
1690
+ await adb.install(apkPath);
1691
+ await adb.shell(`ime enable ${MIDSCENE_IME_ID}`);
1692
+ this.midsceneImeInstalled = true;
1693
+ return;
1694
+ } catch (e) {
1695
+ const errorMsg = String(e);
1696
+ if (!errorMsg.includes('INSTALL_FAILED_USER_RESTRICTED')) throw e;
1697
+ 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}`);
1698
+ debugDevice('Install blocked by device, retrying with approval handler...');
1699
+ }
1700
+ await adb.push(apkPath, '/data/local/tmp/midscene-ime.apk');
1701
+ const installPromise = adb.shell('pm install -r -t /data/local/tmp/midscene-ime.apk');
1702
+ await (0, utils_namespaceObject.sleep)(2000);
1703
+ try {
1704
+ await this.installApprovalHandler();
1705
+ } catch (approvalError) {
1706
+ debugDevice(`Install approval handler failed: ${approvalError}`);
1707
+ }
1708
+ try {
1709
+ const result = await installPromise;
1710
+ if (result.includes('Success')) {
1711
+ await adb.shell(`ime enable ${MIDSCENE_IME_ID}`);
1712
+ this.midsceneImeInstalled = true;
1713
+ debugDevice('MidsceneIME installed via auto-approval');
1714
+ return;
1715
+ }
1716
+ throw new Error(`Install failed: ${result}`);
1717
+ } catch (e) {
1718
+ throw new Error(`Failed to install MidsceneIME even with auto-approval. Please manually enable "Install via USB" in Developer Options. Original error: ${e}`);
1719
+ }
1720
+ }
1721
+ async hideKeyboardViaMidsceneIme(adb, timeoutMs) {
1722
+ const originalIme = await adb.shell('settings get secure default_input_method');
1723
+ const trimmedOriginalIme = originalIme.trim();
1724
+ try {
1725
+ await adb.shell(`ime set ${MIDSCENE_IME_ID}`);
1726
+ await (0, utils_namespaceObject.sleep)(500);
1727
+ await adb.shell(`am broadcast -a ${MIDSCENE_IME_DISMISS_ACTION}`);
1728
+ await (0, utils_namespaceObject.sleep)(500);
1729
+ if (trimmedOriginalIme && trimmedOriginalIme !== MIDSCENE_IME_ID) await adb.shell(`ime set ${trimmedOriginalIme}`);
1730
+ if (await this.waitForKeyboardHidden(adb, timeoutMs)) {
1731
+ debugDevice('Keyboard hidden successfully with MidsceneIME');
1732
+ return true;
1733
+ }
1734
+ debugDevice('MidsceneIME did not dismiss keyboard');
1735
+ return false;
1736
+ } catch (e) {
1737
+ if (trimmedOriginalIme && trimmedOriginalIme !== MIDSCENE_IME_ID) try {
1738
+ await adb.shell(`ime set ${trimmedOriginalIme}`);
1739
+ } catch {}
1740
+ throw e;
1741
+ }
1742
+ }
1639
1743
  async hideKeyboard(options, timeoutMs = 1000) {
1640
1744
  const adb = await this.getAdb();
1641
- const keyboardDismissStrategy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy ?? 'esc-first';
1745
+ const strategy = this.resolveAutoDismissKeyboard(options);
1746
+ if (false === strategy) return false;
1642
1747
  const keyboardStatus = await adb.isSoftKeyboardPresent();
1643
1748
  const isKeyboardShown = 'boolean' == typeof keyboardStatus ? keyboardStatus : keyboardStatus?.isKeyboardShown;
1644
1749
  if (!isKeyboardShown) {
1645
1750
  debugDevice('Keyboard has no UI; no closing necessary');
1646
1751
  return false;
1647
1752
  }
1648
- const keyCodes = 'back-first' === keyboardDismissStrategy ? [
1649
- 4,
1650
- 111
1651
- ] : [
1652
- 111,
1653
- 4
1654
- ];
1655
- for (const keyCode of keyCodes){
1656
- await adb.keyevent(keyCode);
1657
- const startTime = Date.now();
1658
- const intervalMs = 100;
1659
- while(Date.now() - startTime < timeoutMs){
1660
- await (0, utils_namespaceObject.sleep)(intervalMs);
1661
- const currentStatus = await adb.isSoftKeyboardPresent();
1662
- const isStillShown = 'boolean' == typeof currentStatus ? currentStatus : currentStatus?.isKeyboardShown;
1663
- if (!isStillShown) {
1664
- debugDevice(`Keyboard hidden successfully with keycode ${keyCode}`);
1665
- return true;
1666
- }
1753
+ if ('midscene-ime' === strategy || 'midscene-ime-auto-install' === strategy) {
1754
+ const autoApprove = 'midscene-ime-auto-install' === strategy;
1755
+ try {
1756
+ await this.ensureMidsceneImeInstalled(adb, autoApprove);
1757
+ const hidden = await this.hideKeyboardViaMidsceneIme(adb, timeoutMs);
1758
+ if (!hidden) console.warn('Warning: MidsceneIME was invoked but the software keyboard is still visible');
1759
+ return hidden;
1760
+ } catch (e) {
1761
+ 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.";
1762
+ throw new Error(`${installHint} Original error: ${e}`);
1667
1763
  }
1668
- debugDevice(`Keyboard still shown after keycode ${keyCode}, trying next key`);
1669
1764
  }
1670
- console.warn('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1765
+ const keyCode = 'back' === strategy ? 4 : 111;
1766
+ await adb.keyevent(keyCode);
1767
+ if (await this.waitForKeyboardHidden(adb, timeoutMs)) {
1768
+ debugDevice(`Keyboard hidden successfully with keycode ${keyCode} (${strategy})`);
1769
+ return true;
1770
+ }
1771
+ const fallbackKeyCode = 'back' === strategy ? 111 : 4;
1772
+ await adb.keyevent(fallbackKeyCode);
1773
+ if (await this.waitForKeyboardHidden(adb, timeoutMs)) {
1774
+ debugDevice(`Keyboard hidden successfully with fallback keycode ${fallbackKeyCode}`);
1775
+ return true;
1776
+ }
1777
+ console.warn('Warning: Failed to hide the software keyboard after trying all strategies');
1671
1778
  return false;
1672
1779
  }
1673
1780
  constructor(deviceId, options){
1674
1781
  device_define_property(this, "deviceId", void 0);
1675
1782
  device_define_property(this, "yadbPushed", false);
1783
+ device_define_property(this, "midsceneImeInstalled", false);
1784
+ device_define_property(this, "installApprovalHandler", void 0);
1676
1785
  device_define_property(this, "devicePixelRatio", 1);
1677
1786
  device_define_property(this, "devicePixelRatioInitialized", false);
1678
1787
  device_define_property(this, "adb", null);
@@ -1984,6 +2093,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1984
2093
  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);
1985
2094
  this.appNameMapping = (0, shared_utils_namespaceObject.mergeAndNormalizeAppNameMapping)(defaultAppNameMapping, opts?.appNameMapping);
1986
2095
  device.setAppNameMapping(this.appNameMapping);
2096
+ device.setInstallApprovalHandler(async ()=>{
2097
+ await this.aiAct('A system dialog is asking to approve USB installation. Click the "Continue Install", "Allow", "Install" or similar approval button.');
2098
+ });
1987
2099
  this.back = this.createActionWrapper('AndroidBackButton');
1988
2100
  this.home = this.createActionWrapper('AndroidHomeButton');
1989
2101
  this.recentApps = this.createActionWrapper('AndroidRecentAppsButton');
@@ -705,6 +705,18 @@ var __webpack_exports__ = {};
705
705
  const defaultNormalScrollDuration = 1000;
706
706
  const IME_STRATEGY_ALWAYS_YADB = 'always-yadb';
707
707
  const IME_STRATEGY_YADB_FOR_NON_ASCII = 'yadb-for-non-ascii';
708
+ const MIDSCENE_IME_PACKAGE = 'com.midscene.ime';
709
+ const MIDSCENE_IME_ID = `${MIDSCENE_IME_PACKAGE}/.MidsceneIME`;
710
+ const MIDSCENE_IME_DISMISS_ACTION = `${MIDSCENE_IME_PACKAGE}.DISMISS`;
711
+ const AUTO_DISMISS_KEYBOARD_SCHEMA = core_namespaceObject.z.union([
712
+ core_namespaceObject.z.boolean(),
713
+ core_namespaceObject.z["enum"]([
714
+ 'back',
715
+ 'esc',
716
+ 'midscene-ime',
717
+ 'midscene-ime-auto-install'
718
+ ])
719
+ ]).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.");
708
720
  const debugDevice = (0, logger_.getDebug)('android:device');
709
721
  function escapeForShell(text) {
710
722
  return text.replace(/'/g, "'\\''").replace(/\n/g, '\\n');
@@ -728,7 +740,7 @@ var __webpack_exports__ = {};
728
740
  interfaceAlias: 'aiInput',
729
741
  paramSchema: core_namespaceObject.z.object({
730
742
  value: core_namespaceObject.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.'),
731
- autoDismissKeyboard: core_namespaceObject.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.'),
743
+ autoDismissKeyboard: AUTO_DISMISS_KEYBOARD_SCHEMA,
732
744
  mode: core_namespaceObject.z.preprocess((val)=>'append' === val ? 'typeOnly' : val, core_namespaceObject.z["enum"]([
733
745
  'replace',
734
746
  'clear',
@@ -974,6 +986,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
974
986
  setAppNameMapping(mapping) {
975
987
  this.appNameMapping = mapping;
976
988
  }
989
+ setInstallApprovalHandler(handler) {
990
+ this.installApprovalHandler = handler;
991
+ }
977
992
  resolvePackageName(appName) {
978
993
  const normalizedAppName = (0, utils_namespaceObject.normalizeForComparison)(appName);
979
994
  return this.appNameMapping[normalizedAppName];
@@ -1512,7 +1527,8 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1512
1527
  if (!text) return;
1513
1528
  const adb = await this.getAdb();
1514
1529
  const IME_STRATEGY = (this.options?.imeStrategy || env_namespaceObject.globalConfigManager.getEnvConfigValue(env_namespaceObject.MIDSCENE_ANDROID_IME_STRATEGY)) ?? IME_STRATEGY_YADB_FOR_NON_ASCII;
1515
- const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
1530
+ const autoDismiss = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? 'esc';
1531
+ const shouldAutoDismissKeyboard = false !== autoDismiss;
1516
1532
  const useYadb = IME_STRATEGY === IME_STRATEGY_ALWAYS_YADB || IME_STRATEGY === IME_STRATEGY_YADB_FOR_NON_ASCII && this.shouldUseYadbForText(text);
1517
1533
  if (useYadb) await this.execYadb(escapeForShell(text), {
1518
1534
  overwrite: options?.overwrite
@@ -1630,6 +1646,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1630
1646
  }
1631
1647
  this.connectingAdb = null;
1632
1648
  this.yadbPushed = false;
1649
+ this.midsceneImeInstalled = false;
1633
1650
  }
1634
1651
  async getTimestamp() {
1635
1652
  const adb = await this.getAdb();
@@ -1729,43 +1746,135 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1729
1746
  return null;
1730
1747
  }
1731
1748
  }
1749
+ resolveAutoDismissKeyboard(options) {
1750
+ const raw = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard;
1751
+ if (true === raw) ;
1752
+ else if (false === raw) return false;
1753
+ else if (void 0 !== raw) return raw;
1754
+ const legacy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy;
1755
+ if ('back-first' === legacy) return 'back';
1756
+ return 'esc';
1757
+ }
1758
+ async waitForKeyboardHidden(adb, timeoutMs) {
1759
+ const startTime = Date.now();
1760
+ const intervalMs = 100;
1761
+ while(Date.now() - startTime < timeoutMs){
1762
+ await (0, core_utils_namespaceObject.sleep)(intervalMs);
1763
+ const currentStatus = await adb.isSoftKeyboardPresent();
1764
+ const isStillShown = 'boolean' == typeof currentStatus ? currentStatus : currentStatus?.isKeyboardShown;
1765
+ if (!isStillShown) return true;
1766
+ }
1767
+ return false;
1768
+ }
1769
+ async ensureMidsceneImeInstalled(adb, autoApprove = false) {
1770
+ if (this.midsceneImeInstalled) return;
1771
+ try {
1772
+ const output = await adb.shell(`pm list packages ${MIDSCENE_IME_PACKAGE}`);
1773
+ if (output.includes(MIDSCENE_IME_PACKAGE)) {
1774
+ await adb.shell(`ime enable ${MIDSCENE_IME_ID}`);
1775
+ this.midsceneImeInstalled = true;
1776
+ return;
1777
+ }
1778
+ } catch {}
1779
+ const androidPkgJson = (0, external_node_module_.createRequire)(__rslib_import_meta_url__).resolve('@midscene/android/package.json');
1780
+ const apkPath = external_node_path_default().join(external_node_path_default().dirname(androidPkgJson), 'bin', 'midscene-ime.apk');
1781
+ 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.`);
1782
+ try {
1783
+ await adb.install(apkPath);
1784
+ await adb.shell(`ime enable ${MIDSCENE_IME_ID}`);
1785
+ this.midsceneImeInstalled = true;
1786
+ return;
1787
+ } catch (e) {
1788
+ const errorMsg = String(e);
1789
+ if (!errorMsg.includes('INSTALL_FAILED_USER_RESTRICTED')) throw e;
1790
+ 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}`);
1791
+ debugDevice('Install blocked by device, retrying with approval handler...');
1792
+ }
1793
+ await adb.push(apkPath, '/data/local/tmp/midscene-ime.apk');
1794
+ const installPromise = adb.shell('pm install -r -t /data/local/tmp/midscene-ime.apk');
1795
+ await (0, core_utils_namespaceObject.sleep)(2000);
1796
+ try {
1797
+ await this.installApprovalHandler();
1798
+ } catch (approvalError) {
1799
+ debugDevice(`Install approval handler failed: ${approvalError}`);
1800
+ }
1801
+ try {
1802
+ const result = await installPromise;
1803
+ if (result.includes('Success')) {
1804
+ await adb.shell(`ime enable ${MIDSCENE_IME_ID}`);
1805
+ this.midsceneImeInstalled = true;
1806
+ debugDevice('MidsceneIME installed via auto-approval');
1807
+ return;
1808
+ }
1809
+ throw new Error(`Install failed: ${result}`);
1810
+ } catch (e) {
1811
+ throw new Error(`Failed to install MidsceneIME even with auto-approval. Please manually enable "Install via USB" in Developer Options. Original error: ${e}`);
1812
+ }
1813
+ }
1814
+ async hideKeyboardViaMidsceneIme(adb, timeoutMs) {
1815
+ const originalIme = await adb.shell('settings get secure default_input_method');
1816
+ const trimmedOriginalIme = originalIme.trim();
1817
+ try {
1818
+ await adb.shell(`ime set ${MIDSCENE_IME_ID}`);
1819
+ await (0, core_utils_namespaceObject.sleep)(500);
1820
+ await adb.shell(`am broadcast -a ${MIDSCENE_IME_DISMISS_ACTION}`);
1821
+ await (0, core_utils_namespaceObject.sleep)(500);
1822
+ if (trimmedOriginalIme && trimmedOriginalIme !== MIDSCENE_IME_ID) await adb.shell(`ime set ${trimmedOriginalIme}`);
1823
+ if (await this.waitForKeyboardHidden(adb, timeoutMs)) {
1824
+ debugDevice('Keyboard hidden successfully with MidsceneIME');
1825
+ return true;
1826
+ }
1827
+ debugDevice('MidsceneIME did not dismiss keyboard');
1828
+ return false;
1829
+ } catch (e) {
1830
+ if (trimmedOriginalIme && trimmedOriginalIme !== MIDSCENE_IME_ID) try {
1831
+ await adb.shell(`ime set ${trimmedOriginalIme}`);
1832
+ } catch {}
1833
+ throw e;
1834
+ }
1835
+ }
1732
1836
  async hideKeyboard(options, timeoutMs = 1000) {
1733
1837
  const adb = await this.getAdb();
1734
- const keyboardDismissStrategy = options?.keyboardDismissStrategy ?? this.options?.keyboardDismissStrategy ?? 'esc-first';
1838
+ const strategy = this.resolveAutoDismissKeyboard(options);
1839
+ if (false === strategy) return false;
1735
1840
  const keyboardStatus = await adb.isSoftKeyboardPresent();
1736
1841
  const isKeyboardShown = 'boolean' == typeof keyboardStatus ? keyboardStatus : keyboardStatus?.isKeyboardShown;
1737
1842
  if (!isKeyboardShown) {
1738
1843
  debugDevice('Keyboard has no UI; no closing necessary');
1739
1844
  return false;
1740
1845
  }
1741
- const keyCodes = 'back-first' === keyboardDismissStrategy ? [
1742
- 4,
1743
- 111
1744
- ] : [
1745
- 111,
1746
- 4
1747
- ];
1748
- for (const keyCode of keyCodes){
1749
- await adb.keyevent(keyCode);
1750
- const startTime = Date.now();
1751
- const intervalMs = 100;
1752
- while(Date.now() - startTime < timeoutMs){
1753
- await (0, core_utils_namespaceObject.sleep)(intervalMs);
1754
- const currentStatus = await adb.isSoftKeyboardPresent();
1755
- const isStillShown = 'boolean' == typeof currentStatus ? currentStatus : currentStatus?.isKeyboardShown;
1756
- if (!isStillShown) {
1757
- debugDevice(`Keyboard hidden successfully with keycode ${keyCode}`);
1758
- return true;
1759
- }
1846
+ if ('midscene-ime' === strategy || 'midscene-ime-auto-install' === strategy) {
1847
+ const autoApprove = 'midscene-ime-auto-install' === strategy;
1848
+ try {
1849
+ await this.ensureMidsceneImeInstalled(adb, autoApprove);
1850
+ const hidden = await this.hideKeyboardViaMidsceneIme(adb, timeoutMs);
1851
+ if (!hidden) console.warn('Warning: MidsceneIME was invoked but the software keyboard is still visible');
1852
+ return hidden;
1853
+ } catch (e) {
1854
+ 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.";
1855
+ throw new Error(`${installHint} Original error: ${e}`);
1760
1856
  }
1761
- debugDevice(`Keyboard still shown after keycode ${keyCode}, trying next key`);
1762
1857
  }
1763
- console.warn('Warning: Failed to hide the software keyboard after trying both ESC and BACK keys');
1858
+ const keyCode = 'back' === strategy ? 4 : 111;
1859
+ await adb.keyevent(keyCode);
1860
+ if (await this.waitForKeyboardHidden(adb, timeoutMs)) {
1861
+ debugDevice(`Keyboard hidden successfully with keycode ${keyCode} (${strategy})`);
1862
+ return true;
1863
+ }
1864
+ const fallbackKeyCode = 'back' === strategy ? 111 : 4;
1865
+ await adb.keyevent(fallbackKeyCode);
1866
+ if (await this.waitForKeyboardHidden(adb, timeoutMs)) {
1867
+ debugDevice(`Keyboard hidden successfully with fallback keycode ${fallbackKeyCode}`);
1868
+ return true;
1869
+ }
1870
+ console.warn('Warning: Failed to hide the software keyboard after trying all strategies');
1764
1871
  return false;
1765
1872
  }
1766
1873
  constructor(deviceId, options){
1767
1874
  device_define_property(this, "deviceId", void 0);
1768
1875
  device_define_property(this, "yadbPushed", false);
1876
+ device_define_property(this, "midsceneImeInstalled", false);
1877
+ device_define_property(this, "installApprovalHandler", void 0);
1769
1878
  device_define_property(this, "devicePixelRatio", 1);
1770
1879
  device_define_property(this, "devicePixelRatioInitialized", false);
1771
1880
  device_define_property(this, "adb", null);
@@ -1913,6 +2022,9 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1913
2022
  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);
1914
2023
  this.appNameMapping = (0, utils_namespaceObject.mergeAndNormalizeAppNameMapping)(defaultAppNameMapping, opts?.appNameMapping);
1915
2024
  device.setAppNameMapping(this.appNameMapping);
2025
+ device.setInstallApprovalHandler(async ()=>{
2026
+ await this.aiAct('A system dialog is asking to approve USB installation. Click the "Continue Install", "Allow", "Install" or similar approval button.');
2027
+ });
1916
2028
  this.back = this.createActionWrapper('AndroidBackButton');
1917
2029
  this.home = this.createActionWrapper('AndroidHomeButton');
1918
2030
  this.recentApps = this.createActionWrapper('AndroidRecentAppsButton');
@@ -1990,7 +2102,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
1990
2102
  constructor(toolsManager){
1991
2103
  super({
1992
2104
  name: '@midscene/android-mcp',
1993
- version: "1.7.0",
2105
+ version: "1.7.1-beta-20260408073050.0",
1994
2106
  description: 'Control the Android device using natural language commands'
1995
2107
  }, toolsManager);
1996
2108
  }
@@ -75,6 +75,8 @@ export declare interface AndroidConnectedDevice extends Device {
75
75
  export declare class AndroidDevice implements AbstractInterface {
76
76
  private deviceId;
77
77
  private yadbPushed;
78
+ private midsceneImeInstalled;
79
+ private installApprovalHandler?;
78
80
  private devicePixelRatio;
79
81
  private devicePixelRatioInitialized;
80
82
  private adb;
@@ -111,6 +113,12 @@ export declare class AndroidDevice implements AbstractInterface {
111
113
  * Set the app name to package name mapping
112
114
  */
113
115
  setAppNameMapping(mapping: Record<string, string>): void;
116
+ /**
117
+ * Set a handler that will be called when APK installation needs user approval
118
+ * (e.g. on MIUI devices). The handler should use aiAct or similar to click
119
+ * the approval button on screen.
120
+ */
121
+ setInstallApprovalHandler(handler: () => Promise<void>): void;
114
122
  /**
115
123
  * Resolve app name to package name using the mapping
116
124
  * Comparison is case-insensitive and ignores spaces, dashes, and underscores.
@@ -233,6 +241,10 @@ export declare class AndroidDevice implements AbstractInterface {
233
241
  pullUp(startPoint?: Point, distance?: number, duration?: number): Promise<void>;
234
242
  private getDisplayArg;
235
243
  getPhysicalDisplayId(): Promise<string | null>;
244
+ private resolveAutoDismissKeyboard;
245
+ private waitForKeyboardHidden;
246
+ private ensureMidsceneImeInstalled;
247
+ private hideKeyboardViaMidsceneIme;
236
248
  hideKeyboard(options?: AndroidDeviceInputOpt, timeoutMs?: number): Promise<boolean>;
237
249
  }
238
250
 
@@ -67,6 +67,8 @@ declare type AndroidAgentOpt = AgentOpt & {
67
67
  declare class AndroidDevice implements AbstractInterface {
68
68
  private deviceId;
69
69
  private yadbPushed;
70
+ private midsceneImeInstalled;
71
+ private installApprovalHandler?;
70
72
  private devicePixelRatio;
71
73
  private devicePixelRatioInitialized;
72
74
  private adb;
@@ -103,6 +105,12 @@ declare class AndroidDevice implements AbstractInterface {
103
105
  * Set the app name to package name mapping
104
106
  */
105
107
  setAppNameMapping(mapping: Record<string, string>): void;
108
+ /**
109
+ * Set a handler that will be called when APK installation needs user approval
110
+ * (e.g. on MIUI devices). The handler should use aiAct or similar to click
111
+ * the approval button on screen.
112
+ */
113
+ setInstallApprovalHandler(handler: () => Promise<void>): void;
106
114
  /**
107
115
  * Resolve app name to package name using the mapping
108
116
  * Comparison is case-insensitive and ignores spaces, dashes, and underscores.
@@ -225,6 +233,10 @@ declare class AndroidDevice implements AbstractInterface {
225
233
  pullUp(startPoint?: Point, distance?: number, duration?: number): Promise<void>;
226
234
  private getDisplayArg;
227
235
  getPhysicalDisplayId(): Promise<string | null>;
236
+ private resolveAutoDismissKeyboard;
237
+ private waitForKeyboardHidden;
238
+ private ensureMidsceneImeInstalled;
239
+ private hideKeyboardViaMidsceneIme;
228
240
  hideKeyboard(options?: AndroidDeviceInputOpt, timeoutMs?: number): Promise<boolean>;
229
241
  }
230
242
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midscene/android",
3
- "version": "1.7.0",
3
+ "version": "1.7.1-beta-20260408073050.0",
4
4
  "description": "Android automation library for Midscene",
5
5
  "keywords": [
6
6
  "Android UI automation",
@@ -41,8 +41,8 @@
41
41
  "@yume-chan/stream-extra": "2.1.0",
42
42
  "appium-adb": "12.12.1",
43
43
  "sharp": "^0.34.3",
44
- "@midscene/core": "1.7.0",
45
- "@midscene/shared": "1.7.0"
44
+ "@midscene/core": "1.7.1-beta-20260408073050.0",
45
+ "@midscene/shared": "1.7.1-beta-20260408073050.0"
46
46
  },
47
47
  "optionalDependencies": {
48
48
  "@ffmpeg-installer/ffmpeg": "^1.1.0"
@@ -56,12 +56,12 @@
56
56
  "tsx": "^4.19.2",
57
57
  "vitest": "3.0.5",
58
58
  "zod": "3.24.3",
59
- "@midscene/playground": "1.7.0"
59
+ "@midscene/playground": "1.7.1-beta-20260408073050.0"
60
60
  },
61
61
  "license": "MIT",
62
62
  "scripts": {
63
63
  "dev": "npm run build:watch",
64
- "prebuild": "node scripts/download-scrcpy-server.mjs && node scripts/download-yadb.mjs",
64
+ "prebuild": "node scripts/download-scrcpy-server.mjs && node scripts/download-yadb.mjs && node scripts/build-midscene-ime.mjs",
65
65
  "build": "rslib build",
66
66
  "build:watch": "rslib build --watch --no-clean",
67
67
  "playground": "DEBUG=midscene:* tsx demo/playground.ts",