@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/bin/midscene-ime.apk +0 -0
- package/dist/es/cli.mjs +137 -25
- package/dist/es/index.mjs +136 -24
- package/dist/es/mcp-server.mjs +137 -25
- package/dist/lib/cli.js +137 -25
- package/dist/lib/index.js +136 -24
- package/dist/lib/mcp-server.js +137 -25
- package/dist/types/index.d.ts +12 -0
- package/dist/types/mcp-server.d.ts +12 -0
- package/package.json +5 -5
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
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');
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
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
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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",
|