Android Kiosk 模式与应用保活完整方案
概述
本文档提供一套完整的 Android 应用保活与 Kiosk 模式实现方案,适用于专用设备(班牌、定制平板、信息屏等)场景,确保应用持续稳定运行,防止被关闭、绕过和卸载。
目录
方案架构
┌─────────────────────────────────────────────────────────────────┐
│ Android 专用设备 │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Device Owner 层(最强防护) │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │
│ │ │ 锁定任务模式 │ │ 禁用状态栏 │ │ 防止卸载/修改设置 │ │ │
│ │ └─────────────┘ └──────────────┘ └─────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ 双进程守护层(中等防护) │ │
│ │ ┌──────────────────┐ ┌────────────────────────┐ │ │
│ │ │ KioskService │ 互相守护 │ KioskMonitorService │ │ │
│ │ │ (前台服务A) │◄──────►│ (前台服务B) │ │ │
│ │ └──────────────────┘ └────────────────────────┘ │ │
│ │ ↓ ↓ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ AppForegroundMonitor │ │ │
│ │ │ - 检测当前前台应用 │ │ │
│ │ │ - 检测到非本应用时自动拉回 │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ 辅助防护层(基础防护) │ │
│ │ ┌────────────────┐ ┌──────────────┐ ┌───────────────┐ │ │
│ │ │ 无障碍服务 │ │ Overlay 状态栏 │ │ 开机自启广播 │ │ │
│ │ └────────────────┘ └──────────────┘ └───────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
前置条件
设备要求
| 要求 | 说明 |
|---|---|
| Android 7.0+ | 支持锁定任务模式 |
| 推荐 Android 9.0+ | 更好的 Device Owner 支持 |
| 设备未配置(可选) | 设置 Device Owner 需要 |
权限要求
<!-- 前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- 悬浮窗(状态栏拦截) -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 无障碍服务 -->
<!-- 需要用户手动开启 -->
核心功能实现
1. 前台服务保活
功能说明
通过前台服务提高应用进程优先级,防止被系统回收。
实现代码
class KioskService : Service() {
companion object {
private const val NOTIFICATION_ID = 1000
private const val CHANNEL_ID = "kiosk_service_channel"
private const val ACTION_START = "com.example.ACTION_START_KIOSK"
private const val ACTION_STOP = "com.example.ACTION_STOP_KIOSK"
fun start(context: Context) {
val intent = Intent(context, KioskService::class.java).apply {
action = ACTION_START
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
fun stop(context: Context) {
val intent = Intent(context, KioskService::class.java).apply {
action = ACTION_STOP
}
context.startService(intent)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> startKioskMode()
ACTION_STOP -> {
stopKioskMode()
stopSelf()
return START_NOT_STICKY
}
}
// 关键:返回 START_STICKY,服务被杀后自动重启
return START_STICKY
}
private fun startKioskMode() {
startForeground(NOTIFICATION_ID, buildNotification())
// ... 其他初始化逻辑
}
private fun buildNotification(): Notification {
// 构建常驻通知,确保服务不被系统回收
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Kiosk 保护服务",
NotificationManager.IMPORTANCE_LOW
)
val nm = getSystemService(NotificationManager::class.java)
nm.createNotificationChannel(channel)
return Notification.Builder(this, CHANNEL_ID)
.setContentTitle("应用保护中")
.setContentText("Kiosk 模式运行中")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setOngoing(true) // 常驻通知
.setPriority(Notification.PRIORITY_LOW)
.build()
} else {
@Suppress("DEPRECATION")
return Notification.Builder(this)
.setContentTitle("应用保护中")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setOngoing(true)
.build()
}
}
override fun onBind(intent: Intent?): IBinder? = null
}
配置文件
<!-- AndroidManifest.xml -->
<service
android:name=".KioskService"
android:exported="false">
</service>
2. 锁定任务模式 (Kiosk)
功能说明
限制用户只能在指定应用间切换,无法退出到桌面或其他应用。
前提条件
- 应用必须是 Device Owner
- 设备处于锁定任务模式
实现代码
object HomeLauncherHelper {
// 设置允许锁定的应用包名
fun configureLockTask(context: Context): Boolean {
return try {
val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
val adminComponent = ComponentName(context, HomeDeviceAdminReceiver::class.java)
// 设置允许锁定任务的包名白名单
dpm.setLockTaskPackages(adminComponent, arrayOf(context.packageName))
true
} catch (e: Exception) {
false
}
}
// 启动锁定任务模式
fun startLockTask(context: Context): Boolean {
return try {
val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
if (!am.isInLockTaskMode) {
am.startLockTask()
true
} else {
true // 已在锁定模式
}
} catch (e: Exception) {
false
}
}
// 检查是否可以启用锁定任务
fun canEnableLockTask(context: Context): Boolean {
val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
val adminComponent = ComponentName(context, HomeDeviceAdminReceiver::class.java)
return dpm.isDeviceOwnerApp(context.packageName) &&
dpm.isLockTaskPermitted(context.packageName)
}
// 检查是否是 Device Owner
fun isDeviceOwner(context: Context): Boolean {
val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
return dpm.isDeviceOwnerApp(context.packageName)
}
}
Device Admin 配置
<!-- res/xml/device_admin.xml -->
<?xml version="1.0" encoding="utf-8"?>
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
<uses-policies>
<wipe-data />
<reset-password />
<force-lock />
<lock-task />
</uses-policies>
</device-admin>
<!-- AndroidManifest.xml -->
<receiver
android:name=".HomeDeviceAdminReceiver"
android:description="@string/device_admin_description"
android:label="@string/device_admin_label"
android:permission="android.permission.BIND_DEVICE_ADMIN"
android:exported="true">
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
DeviceAdminReceiver
class HomeDeviceAdminReceiver : DeviceAdminReceiver() {
override fun onEnabled(context: Context, intent: Intent) {
super.onEnabled(context, intent)
Log.i("DeviceAdmin", "设备管理员已启用")
}
}
3. 防止卸载
功能说明
禁止用户卸载当前应用。
前提条件
- 应用必须是 Device Owner
实现代码
fun setUninstallBlocked(context: Context, blocked: Boolean): Boolean {
return try {
if (!isDeviceOwner(context)) {
return false
}
val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
val adminComponent = ComponentName(context, HomeDeviceAdminReceiver::class.java)
// 禁止/允许卸载
dpm.setUninstallBlocked(adminComponent, context.packageName, blocked)
true
} catch (e: Exception) {
false
}
}
4. 状态栏保护
功能说明
禁用状态栏和通知面板,防止用户下拉进入系统设置。
实现方式对比
| 方式 | 要求 | 防护强度 |
|---|---|---|
| Device Owner API | Device Owner | ⭐⭐⭐⭐⭐ |
| Overlay 拦截 | 悬浮窗权限 | ⭐⭐⭐ |
方式一:Device Owner API
fun disableStatusBar(context: Context, disabled: Boolean): Boolean {
return try {
if (!isDeviceOwner(context)) return false
val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
val adminComponent = ComponentName(context, HomeDeviceAdminReceiver::class.java)
// 禁用状态栏
dpm.setStatusBarDisabled(adminComponent, disabled)
true
} catch (e: Exception) {
false
}
}
方式二:WindowManager Overlay
object StatusBarBlocker {
private var statusBarView: View? = null
private var windowManager: WindowManager? = null
fun disableStatusBar(context: Context) {
// 如果是 Device Owner,使用官方 API
if (HomeLauncherHelper.isDeviceOwner(context)) {
HomeLauncherHelper.disableStatusBar(context, true)
return
}
// 否则使用 Overlay 方式
if (statusBarView == null) {
createStatusBarBlockerView(context)
}
}
private fun createStatusBarBlockerView(context: Context) {
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
// 创建透明 View 覆盖在状态栏上
statusBarView = View(context).apply {
setBackgroundColor(Color.TRANSPARENT)
}
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
getStatusBarHeight(context),
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.TOP
}
try {
windowManager?.addView(statusBarView, params)
} catch (e: Exception) {
Log.e("StatusBarBlocker", "添加 View 失败: ${e.message}")
}
}
private fun getStatusBarHeight(context: Context): Int {
val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
return if (resourceId > 0) {
context.resources.getDimensionPixelSize(resourceId)
} else {
24 * context.resources.displayMetrics.density.toInt()
}
}
fun removeStatusBarBlocker() {
try {
statusBarView?.let { view ->
windowManager?.removeView(view)
}
} catch (e: Exception) {
}
statusBarView = null
}
}
5. 无障碍服务增强
功能说明
通过无障碍服务检测用户操作,自动阻止跳出 Kiosk 模式。
实现代码
class KioskAccessibilityService : AccessibilityService() {
companion object {
var instance: KioskAccessibilityService? = null
fun isRunning(): Boolean = instance != null
fun start(context: Context) {
val intent = Intent(context, KioskAccessibilityService::class.java)
context.startService(intent)
}
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
if (event == null) return
val kioskEnabled = MySharedPreferences.read(Constance.KIOSK_MODE_ENABLED, false)
val statusBarBlockEnabled = MySharedPreferences.read(Constance.STATUS_BAR_BLOCK_ENABLED, true)
if (!kioskEnabled && !statusBarBlockEnabled) return
val packageName = event.packageName?.toString() ?: return
when (event.eventType) {
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
// 检测到切换到其他应用,自动返回
if (isSystemPackage(packageName)) {
handler.postDelayed({
startActivity(Intent(this, InputActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
}, 500)
}
}
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED -> {
// 关闭通知面板
if (statusBarBlockEnabled) {
performGlobalAction(GLOBAL_ACTION_BACK)
}
}
}
}
private fun isSystemPackage(packageName: String): Boolean {
return packageName in setOf(
"com.android.settings",
"com.android.systemui",
"com.miui.home"
)
}
override fun onInterrupt() {}
override fun onServiceConnected() {
super.onServiceConnected()
instance = this
}
override fun onDestroy() {
super.onDestroy()
instance = null
}
}
服务配置
<!-- res/xml/kiosk_accessibility_service.xml -->
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
android:description="班牌应用保护服务"
android:notificationTimeout="100" />
6. 双进程守护
功能说明
使用两个前台服务互相守护,一个被杀后另一个自动重启它。
架构图
┌─────────────────────────────────────────────────────────┐
│ 主应用进程 │
│ │
│ ┌──────────────────┐ ┌────────────────────┐ │
│ │ KioskService │◄────────►│ KioskMonitorService│ │
│ │ (前台服务A) │ 互相守护 │ (前台服务B) │ │
│ └────────┬─────────┘ └─────────┬──────────┘ │
│ │ │ │
│ │ │ │
│ │ ┌────────────────────┐ │ │
│ └───►│ 守护检查逻辑 │◄───┘ │
│ │ 1. 检查对方服务 │ │
│ │ 2. 检查主Activity │ │
│ │ 3. 检查前台状态 │ │
│ │ 4. 自动恢复 │ │
│ └────────────────────┘ │
└─────────────────────────────────────────────────────────┘
KioskMonitorService 实现
class KioskMonitorService : Service() {
companion object {
private const val GUARD_INTERVAL = 3000L // 守护检查间隔
private const val FOREGROUND_CHECK_INTERVAL = 2000L // 前台检查间隔
fun start(context: Context) {
val intent = Intent(context, KioskMonitorService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
}
private val handler = Handler(Looper.getMainLooper())
private var isRunning = false
// 守护检查:检查其他服务和 Activity 是否存活
private val guardRunnable = object : Runnable {
override fun run() {
if (!isRunning) return
// 1. 检查 KioskService 是否存活
if (!KioskService.isServiceRunning(this@KioskMonitorService)) {
KioskService.start(this@KioskMonitorService)
}
// 2. 检查主 Activity 是否存活
if (!ActivityLifecycleTracker.isMainActivityAlive()) {
restartMainActivity()
}
// 3. 检查锁定任务模式
val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
if (HomeLauncherHelper.isDeviceOwner(this@KioskMonitorService) && !am.isInLockTaskMode) {
// 通知 Activity 重新启动锁定
sendBroadcast(Intent("com.example.ACTION_RESTART_LOCK_TASK"))
}
handler.postDelayed(this, GUARD_INTERVAL)
}
}
// 前台检查:检测用户是否切换到其他应用
private val foregroundCheckRunnable = object : Runnable {
override fun run() {
if (!isRunning) return
// 检查应用是否在前台
if (!AppForegroundMonitor.checkIfAppInForeground(this@KioskMonitorService)) {
val currentPkg = AppForegroundMonitor.getCurrentForegroundPackage(this@KioskMonitorService)
// 允许用户在系统设置中短暂停留(配置权限)
if (currentPkg !in setOf("com.android.settings")) {
// 立即拉回应用
AppForegroundMonitor.bringAppToFront(this@KioskMonitorService)
}
}
handler.postDelayed(this, FOREGROUND_CHECK_INTERVAL)
}
}
override fun onCreate() {
super.onCreate()
isRunning = true
startForeground(NOTIFICATION_ID, buildNotification())
// 启动守护和前台检查
handler.postDelayed(guardRunnable, GUARD_INTERVAL)
handler.postDelayed(foregroundCheckRunnable, FOREGROUND_CHECK_INTERVAL)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY // 关键:被杀后自动重启
}
override fun onDestroy() {
super.onDestroy()
isRunning = false
handler.removeCallbacksAndMessages(null)
}
private fun restartMainActivity() {
startActivity(Intent(this, InputActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
})
}
override fun onBind(intent: Intent?): IBinder? = null
}
主服务启动监控服务
// 在 KioskService.onStartCommand 中
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> {
startKioskMode()
// 启动监控服务实现双进程守护
KioskMonitorService.start(this)
}
ACTION_STOP -> {
KioskMonitorService.stop(this)
stopSelf()
}
}
return START_STICKY
}
7. Activity 生命周期跟踪
功能说明
通过 Application.ActivityLifecycleCallbacks 精确跟踪 Activity 状态,比 getRunningTasks() 更可靠。
实现代码
object ActivityLifecycleTracker {
private var activeActivityCount = 0
private var isMainActivityResumed = false
private val callbacks = object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {
activeActivityCount++
}
override fun onActivityResumed(activity: Activity) {
if (activity is InputActivity) {
isMainActivityResumed = true
}
}
override fun onActivityPaused(activity: Activity) {
if (activity is InputActivity) {
isMainActivityResumed = false
}
}
override fun onActivityStopped(activity: Activity) {
activeActivityCount = (activeActivityCount - 1).coerceAtLeast(0)
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {
activeActivityCount = (activeActivityCount - 1).coerceAtLeast(0)
if (activity is InputActivity) {
isMainActivityResumed = false
}
}
}
fun register(app: Application) {
app.registerActivityLifecycleCallbacks(callbacks)
}
fun isMainActivityAlive(): Boolean {
// 主 Activity 在前台,或有其他 Activity 存活
return isMainActivityResumed || activeActivityCount > 0
}
fun isMainActivityInForeground(): Boolean {
return isMainActivityResumed
}
fun getActiveActivityCount(): Int {
return activeActivityCount
}
}
在 Application 中注册
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 注册 Activity 生命周期跟踪器
ActivityLifecycleTracker.register(this)
}
}
权限与配置
AndroidManifest.xml 完整配置
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 权限声明 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application>
<!-- 主 Activity -->
<activity
android:name=".InputActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- 前台服务 A -->
<service
android:name=".KioskService"
android:exported="false" />
<!-- 前台服务 B(守护服务) -->
<service
android:name=".KioskMonitorService"
android:exported="false" />
<!-- Device Admin Receiver -->
<receiver
android:name=".HomeDeviceAdminReceiver"
android:permission="android.permission.BIND_DEVICE_ADMIN"
android:exported="true">
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
<!-- 开机自启广播 -->
<receiver
android:name=".BootBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
<!-- 无障碍服务 -->
<service
android:name=".KioskAccessibilityService"
android:label="班牌保护服务"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/kiosk_accessibility_service" />
</service>
</application>
</manifest>
开机自启恢复
class BootBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED,
Intent.ACTION_LOCKED_BOOT_COMPLETED -> {
// 恢复 Kiosk 保护状态
val kioskEnabled = MySharedPreferences.read(Constance.KIOSK_MODE_ENABLED, false)
val foregroundEnabled = MySharedPreferences.read(Constance.FOREGROUND_SERVICE_ENABLED, true)
if (foregroundEnabled) {
KioskService.start(context)
}
if (kioskEnabled) {
// 延迟启动锁定任务模式
Handler(Looper.getMainLooper()).postDelayed({
HomeLauncherHelper.startLockTask(context)
}, 3000)
}
}
}
}
}
设置页面设计
功能开关列表
| 功能 | 默认值 | 是否需要密码 |
|---|---|---|
| 前台服务保活 | ✅ 开启 | 关闭时需要 |
| Kiosk 锁定模式 | ❌ 关闭 | 关闭时需要 |
| 状态栏保护 | ✅ 开启 | 关闭时需要 |
| 禁止卸载 | ❌ 关闭 | 设置时需要 |
密码保护逻辑
// 开启操作:无需密码
// 关闭/清除操作:需要密码验证
private fun showPasswordDialog(callback: (Boolean) -> Unit) {
val input = EditText(this).apply {
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
hint = "请输入管理员密码"
}
AlertDialog.Builder(this)
.setTitle("安全验证")
.setView(input)
.setPositiveButton("确定") { _, _ ->
val password = input.text.toString()
if (password == "admin123" || password == "1234") {
callback(true)
} else {
"密码错误".showToast(this)
callback(false)
}
}
.setNegativeButton("取消") { _, _ -> callback(false) }
.setCancelable(false)
.show()
}
Device Owner 设置指南
什么是 Device Owner
Device Owner 是 Android 5.0+ 引入的设备管理角色,比 Device Admin 拥有更高权限,可以实现:
- 锁定任务模式(Kiosk)
- 禁用状态栏
- 防止卸载
- 持久化默认桌面
- 限制用户修改系统设置
设置步骤
方法一:ADB 命令(推荐)
前提条件:设备必须处于未配置状态
# 1. 恢复出厂设置(如果设备已配置)
adb shell reboot recovery
# 手动选择恢复出厂设置
# 2. 等待设备重启,跳过所有设置向导
# 不要登录任何账户,不要连接 WiFi
# 3. 安装应用
adb install your_app.apk
# 4. 设置 Device Owner
adb shell dpm set-device-owner com.your.package/.YourDeviceAdminReceiver
# 5. 验证设置
adb shell dumpsys device_policy | grep "Device Owner"
方法二:Root Shell(设备已配置但有 Root)
fun setDeviceOwnerViaRoot(context: Context): Boolean {
return try {
val command = "dpm set-device-owner ${context.packageName}/.HomeDeviceAdminReceiver"
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command))
val exitCode = process.waitFor()
exitCode == 0
} catch (e: Exception) {
false
}
}
方法三:代码中引导用户
fun showAdbInstructionDialog(context: Context) {
val command = "adb shell dpm set-device-owner ${context.packageName}/.HomeDeviceAdminReceiver"
AlertDialog.Builder(context)
.setTitle("设置 Device Owner")
.setMessage("请在电脑上执行以下命令:\n\n$command\n\n注意:设备必须处于未配置状态")
.setPositiveButton("我已完成") { _, _ -> }
.show()
}
注意事项
- Device Owner 只能设置一次,设置后无法通过普通方式清除
- 设备必须处于未配置状态:如果之前设置过其他 Device Owner,需要恢复出厂
-
清除 Device Owner(需要 Root):
adb shell dpm remove-active-admin com.your.package/.HomeDeviceAdminReceiver
常见问题与解决方案
Q1: 前台服务被杀后没有自动重启?
解决方案:
- 确保
onStartCommand()返回START_STICKY - 使用双进程守护(两个前台服务互相守护)
- 检查设备厂商是否有后台限制(如 MIUI 的省电策略)
Q2: 锁定任务模式启动失败?
检查清单:
- 应用是否是 Device Owner?
- 是否调用了
setLockTaskPackages()? - 是否在支持的 Activity 中调用
startLockTask()? -
AndroidManifest.xml中是否声明了HomeDeviceAdminReceiver?
Q3: 无障碍服务没有触发事件?
排查步骤:
- 检查
kiosk_accessibility_service.xml配置是否正确 - 确认服务已开启:
adb shell settings get secure enabled_accessibility_services - 某些 ROM 会限制无障碍事件,尝试在系统设置中手动开启
Q4: 如何检测应用是否被切换到后台?
推荐方式:
// 方式1:ActivityLifecycleTracker(最可靠)
val isAlive = ActivityLifecycleTracker.isMainActivityAlive()
// 方式2:AppForegroundMonitor(轮询检测)
val isForeground = AppForegroundMonitor.checkIfAppInForeground(context)
// 方式3:AccessibilityService(被动监听)
// 在 onAccessibilityEvent 中检测 TYPE_WINDOW_STATE_CHANGED
Q5: 如何防止用户强制停止应用?
方案:
- Device Owner:禁止用户强制停止(某些系统支持)
- 双进程守护:一个服务被杀,另一个立即重启
- 增加恢复机制:检测到应用被杀后自动拉起
Q6: 状态栏 Overlay 没有生效?
检查清单:
- 是否有
SYSTEM_ALERT_WINDOW权限? - 是否在正确的时候添加了 View?
- View 的
WindowManager.LayoutParams参数是否正确?
文件结构参考
app/src/main/
├── java/com/example/yourapp/
│ ├── Utils/home/
│ │ ├── KioskService.kt # 前台服务 A
│ │ ├── KioskMonitorService.kt # 前台服务 B(守护)
│ │ ├── KioskAccessibilityService.kt # 无障碍服务
│ │ ├── StatusBarBlocker.kt # 状态栏拦截
│ │ ├── AppForegroundMonitor.kt # 前台状态检测
│ │ ├── ActivityLifecycleTracker.kt # Activity 生命周期跟踪
│ │ └── HomeLauncherHelper.kt # Kiosk 功能核心逻辑
│ ├── receiver/
│ │ ├── HomeDeviceAdminReceiver.kt # 设备管理员
│ │ └── BootBroadcastReceiver.kt # 开机自启
│ └── MyApplication.kt
├── res/
│ ├── xml/
│ │ ├── device_admin.xml # 设备管理员配置
│ │ └── kiosk_accessibility_service.xml # 无障碍服务配置
│ └── layout/
│ └── activity_set.xml # 设置页面
└── AndroidManifest.xml
安全建议
- 密码保护:关键设置必须通过密码验证
- 日志审计:记录所有安全相关操作
- 远程配置:支持远程更新 Kiosk 策略
- 防调试:在生产环境中禁用调试模式
- 签名验证:验证应用签名,防止被篡改
版本兼容性
| 功能 | 最低版本 | 说明 |
|---|---|---|
| 前台服务 | API 1 | 全版本支持 |
| 锁定任务模式 | API 21 | 需要 Device Owner |
| Device Owner | API 21 | Android 5.0+ |
| 禁用状态栏 | API 24 | Device Owner API |
| 防止卸载 | API 21 | Device Owner API |
| Overlay 拦截 | API 23 | SYSTEM_ALERT_WINDOW |
| 无障碍服务 | API 4 | 全版本支持 |
许可说明
本文档有本人原创,禁止任何商用或者转载。仅供学习和参考使用。在实际项目中使用时,请根据具体需求进行调整。