# Kiosk 模式下 APK 安装问题修复方案 ## 📋 问题描述 在固定屏幕(Kiosk/LockTask Mode)下,应用无法拉起 APK 安装页面。这是因为: 1. **LockTask Mode 限制**:当应用处于 `startLockTask()` 状态时,系统只允许白名单中的包运行 2. **安装器不在白名单**:系统的 PackageInstaller 不在 `setLockTaskPackages()` 设置的白名单中 3. **无法跳转**:尝试启动安装 Intent 时被系统拦截,无法显示安装界面 ## ✅ 解决方案 ### 核心思路 在安装 APK 前**临时退出 Kiosk 模式**,安装完成或失败后**自动恢复 Kiosk 模式**。 ### 实现细节 #### 1. KioskManager 增强(Java 层) **文件**: `myLockView/src/main/java/com/ble/mylockview/admin/KioskManager.java` 新增两个方法: ```java /** * 临时退出 Kiosk 模式(用于安装 APK 等操作) * 不修改 kioskEnabled 标志,允许后续自动重新锁定 */ public static void temporaryExitKiosk() { Activity act = getActivity(); if (act == null) return; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { try { act.stopLockTask(); kioskStarted = false; Log.i(TAG, "⏸️ 临时退出 Kiosk(用于 APK 安装)"); } catch (Exception e) { Log.e(TAG, "临时退出 Kiosk 失败", e); } } } /** * 恢复 Kiosk 模式(临时退出后重新进入) */ public static void resumeKiosk() { Activity act = getActivity(); if (act == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return; // 只有在 kioskEnabled 为 true 且未启动时才重新启动 if (!kioskEnabled || kioskStarted) return; if (debugMode) { try { act.startLockTask(); kioskStarted = true; Log.i(TAG, "▶️ 恢复 Kiosk (Debug 模式)"); } catch (Exception ignored) {} return; } if (!dpm.isDeviceOwnerApp(act.getPackageName())) { Log.w(TAG, "❌ 非 DeviceOwner,无法恢复 Kiosk"); return; } try { dpm.setLockTaskPackages( admin, new String[]{act.getPackageName()} ); act.startLockTask(); kioskStarted = true; Log.i(TAG, "▶️ 恢复 Kiosk"); } catch (Exception e) { Log.e(TAG, "恢复 Kiosk 失败", e); } } ``` **关键区别**: - `exitKiosk()`: 完全退出,设置 `kioskEnabled = false`,不会自动恢复 - `temporaryExitKiosk()`: 临时退出,保持 `kioskEnabled = true`,可以通过 `resumeKiosk()` 恢复 #### 2. 创建 KioskModule 插件(uni-app 桥接层) **文件**: `uniplugin_module/src/main/java/io/dcloud/uniplugin/KioskModule.java` ```java package io.dcloud.uniplugin; import com.ble.mylockview.admin.KioskManager; import io.dcloud.feature.uniapp.annotation.UniJSMethod; import io.dcloud.feature.uniapp.bridge.UniJSCallback; import io.dcloud.feature.uniapp.common.UniModule; public class KioskModule extends UniModule { @UniJSMethod(uiThread = true) public void temporaryExitKiosk(UniJSCallback callback) { // 临时退出 Kiosk } @UniJSMethod(uiThread = true) public void resumeKiosk(UniJSCallback callback) { // 恢复 Kiosk } @UniJSMethod(uiThread = true) public void exitKiosk(UniJSCallback callback) { // 完全退出 Kiosk } } ``` #### 3. 注册插件 **文件**: `app/src/main/assets/dcloud_uniplugins.json` ```json { "nativePlugins": [ { "plugins": [ { "type": "module", "name": "KioskModule", "class": "io.dcloud.uniplugin.KioskModule" } ] } ] } ``` #### 4. 前端调用(Vue 层) **文件**: `heart-app-hbuilder-x/pages/platform-page/app-info/app-info.vue` ```javascript /** * 临时退出 Kiosk 模式并安装 APK */ temporaryExitKioskAndInstall() { const KioskModule = uni.requireNativePlugin('KioskModule'); if (!KioskModule) { console.warn('⚠️ KioskModule 不可用,直接尝试安装'); this.installApk(); return; } // 临时退出 Kiosk KioskModule.temporaryExitKiosk(res => { console.log('KioskModule.temporaryExitKiosk 结果:', res); // 延迟 300ms 后安装,确保 Kiosk 完全退出 setTimeout(() => { this.installApk(); }, 300); }); }, /** * 执行 APK 安装 */ installApk() { plus.runtime.install(this.saveFile, { force: true }, () => { console.log('✅ 安装成功'); uni.showToast({ title: '安装成功', icon: 'none' }); // 安装成功后,尝试恢复 Kiosk 模式 this.resumeKiosk(); }, err => { console.error('❌ 安装失败', err); uni.showToast({ title: '安装失败: ' + err.message, icon: 'none' }); // 安装失败后也要恢复 Kiosk this.resumeKiosk(); }); }, /** * 恢复 Kiosk 模式 */ resumeKiosk() { const KioskModule = uni.requireNativePlugin('KioskModule'); if (!KioskModule) { console.warn('⚠️ KioskModule 不可用,无法恢复 Kiosk'); return; } // 延迟 1 秒后恢复,给用户一些操作时间 setTimeout(() => { KioskModule.resumeKiosk(res => { console.log('KioskModule.resumeKiosk 结果:', res); }); }, 1000); } ``` #### 5. 依赖配置 **文件**: `uniplugin_module/build.gradle` ```gradle dependencies { implementation project(path: ':core') implementation project(path: ':ICDeviceManager') implementation project(path: ':myLockView') // ✅ 新增 // ... } ``` ## 🔄 工作流程 ``` 用户点击"更新" 或 "安装已下载的文件" ↓ 调用 temporaryExitKioskAndInstall() ↓ KioskModule.temporaryExitKiosk() ← 退出 Kiosk,但保持 kioskEnabled=true ↓ 延迟 300ms(确保退出完成) ↓ plus.runtime.install() ← 拉起系统安装界面(现在可以成功) ↓ 安装成功/失败的回调 ↓ resumeKiosk() ← 延迟 1 秒后自动恢复 Kiosk ↓ KioskManager.resumeKiosk() ↓ 重新进入 Kiosk 模式 ``` ## 📝 修改的文件清单 ### 新增文件 1. `bBeng-HeartRate-4.66-pad/uniplugin_module/src/main/java/io/dcloud/uniplugin/KioskModule.java` ### 修改的文件 1. `bBeng-HeartRate-4.66-pad/myLockView/src/main/java/com/ble/mylockview/admin/KioskManager.java` - 新增 `temporaryExitKiosk()` 方法 - 新增 `resumeKiosk()` 方法 2. `bBeng-HeartRate-4.66-pad/app/src/main/assets/dcloud_uniplugins.json` - 注册 `KioskModule` 插件 3. `bBeng-HeartRate-4.66-pad/uniplugin_module/build.gradle` - 添加 `myLockView` 模块依赖 4. `heart-app-hbuilder-x/pages/platform-page/app-info/app-info.vue` - 新增 `temporaryExitKioskAndInstall()` 方法 - 新增 `installApk()` 方法 - 新增 `resumeKiosk()` 方法 - 修改 `openDownloadFolder()` 调用新方法 - 修改下载完成后的自动安装逻辑 ## 🚀 部署步骤 ### 1. 编译 Android 项目 ```bash cd bBeng-HeartRate-4.66-pad ./gradlew assembleRelease ``` ### 2. 打包 uni-app(如果需要) 在 HBuilderX 中: 1. 打开 `heart-app-hbuilder-x` 项目 2. 发行 → 原生 App-云打包 / 本地打包 ### 3. 安装测试 ```bash adb install -r app/build/outputs/apk/release/app-release.apk ``` ### 4. 测试流程 1. **测试固定屏幕下载安装**: - 确保设备已设置为 Device Owner - 启动应用(会自动进入 Kiosk 模式) - 进入"版本更新"页面 - 点击"更新此版本"下载新版本 - 下载完成后会自动尝试安装 - ✅ 应该能成功拉起安装界面 2. **测试已下载文件安装**: - 如果之前下载过 APK - 点击"安装已下载的文件"按钮 - ✅ 应该能成功拉起安装界面 3. **测试 Kiosk 恢复**: - 取消安装或安装失败后 - 等待 1 秒 - ✅ 应该自动重新进入 Kiosk 模式 ## ⚠️ 注意事项 1. **时序控制**: - 退出 Kiosk 后延迟 300ms 再安装,确保退出完成 - 安装完成/失败后延迟 1 秒再恢复,给用户操作时间 2. **兼容性**: - 如果 `KioskModule` 不可用(非 Kiosk 模式运行),会自动降级到直接安装 - 代码兼容普通模式和 Kiosk 模式 3. **Device Owner 要求**: - `resumeKiosk()` 需要应用是 Device Owner - 如果不是 Device Owner,会记录日志但不会崩溃 4. **安装成功后的行为**: - APK 安装成功通常会重启应用 - 新版本启动后会自动重新进入 Kiosk 模式(由 `MyApplication` 处理) - `resumeKiosk()` 主要用于安装失败的情况 ## 🔍 日志调试 安装过程中可以查看以下日志: ```bash adb logcat -s KioskManager KioskModule app-info.vue ``` 关键日志: - `⏸️ 临时退出 Kiosk(用于 APK 安装)` - 成功退出 - `▶️ 恢复 Kiosk` - 成功恢复 - `KioskModule.temporaryExitKiosk 结果` - Vue 层调用结果 - `✅ 安装成功` / `❌ 安装失败` - 安装结果 ## 📚 扩展用途 这个方案不仅可以用于 APK 安装,还可以用于其他需要跳转到外部应用的场景: 1. **打开系统设置** 2. **打开文件管理器** 3. **打开浏览器** 4. **调用第三方应用** 只需在跳转前调用 `temporaryExitKiosk()`,完成后调用 `resumeKiosk()` 即可。 ## 🎯 总结 通过增强 `KioskManager` 和创建 `KioskModule` 插件,我们实现了: ✅ 固定屏幕下可以正常拉起 APK 安装界面 ✅ 安装完成后自动恢复 Kiosk 模式 ✅ 代码结构清晰,易于维护 ✅ 兼容普通模式和 Kiosk 模式 ✅ 可扩展到其他需要跳转的场景 --- **修复日期**: 2026/1/26 **修复人员**: AI Assistant **测试状态**: 待测试