KIOSK_APK_INSTALL_FIX.md 9.7 KB

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

新增两个方法:

/**
 * 临时退出 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

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

{
  "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

/**
 * 临时退出 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

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 项目

cd bBeng-HeartRate-4.66-pad
./gradlew assembleRelease

2. 打包 uni-app(如果需要)

在 HBuilderX 中:

  1. 打开 heart-app-hbuilder-x 项目
  2. 发行 → 原生 App-云打包 / 本地打包

3. 安装测试

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() 主要用于安装失败的情况

🔍 日志调试

安装过程中可以查看以下日志:

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
测试状态: 待测试