Android Kiosk 模式与应用保活完整方案

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()
}

注意事项

  1. Device Owner 只能设置一次,设置后无法通过普通方式清除
  2. 设备必须处于未配置状态:如果之前设置过其他 Device Owner,需要恢复出厂
  3. 清除 Device Owner(需要 Root):
    adb shell dpm remove-active-admin com.your.package/.HomeDeviceAdminReceiver
    

常见问题与解决方案

Q1: 前台服务被杀后没有自动重启?

解决方案

  1. 确保 onStartCommand() 返回 START_STICKY
  2. 使用双进程守护(两个前台服务互相守护)
  3. 检查设备厂商是否有后台限制(如 MIUI 的省电策略)

Q2: 锁定任务模式启动失败?

检查清单

  • 应用是否是 Device Owner?
  • 是否调用了 setLockTaskPackages()
  • 是否在支持的 Activity 中调用 startLockTask()
  • AndroidManifest.xml 中是否声明了 HomeDeviceAdminReceiver

Q3: 无障碍服务没有触发事件?

排查步骤

  1. 检查 kiosk_accessibility_service.xml 配置是否正确
  2. 确认服务已开启:adb shell settings get secure enabled_accessibility_services
  3. 某些 ROM 会限制无障碍事件,尝试在系统设置中手动开启

Q4: 如何检测应用是否被切换到后台?

推荐方式

// 方式1:ActivityLifecycleTracker(最可靠)
val isAlive = ActivityLifecycleTracker.isMainActivityAlive()

// 方式2:AppForegroundMonitor(轮询检测)
val isForeground = AppForegroundMonitor.checkIfAppInForeground(context)

// 方式3:AccessibilityService(被动监听)
// 在 onAccessibilityEvent 中检测 TYPE_WINDOW_STATE_CHANGED

Q5: 如何防止用户强制停止应用?

方案

  1. Device Owner:禁止用户强制停止(某些系统支持)
  2. 双进程守护:一个服务被杀,另一个立即重启
  3. 增加恢复机制:检测到应用被杀后自动拉起

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

安全建议

  1. 密码保护:关键设置必须通过密码验证
  2. 日志审计:记录所有安全相关操作
  3. 远程配置:支持远程更新 Kiosk 策略
  4. 防调试:在生产环境中禁用调试模式
  5. 签名验证:验证应用签名,防止被篡改

版本兼容性

功能 最低版本 说明
前台服务 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 全版本支持

许可说明

本文档有本人原创,禁止任何商用或者转载。仅供学习和参考使用。在实际项目中使用时,请根据具体需求进行调整。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容