@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.
- 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
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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
|
-
|
|
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');
|