做第三方平板管控时,管理员说的“锁定设备”,通常不是把屏幕熄灭,而是让设备立刻进入一个不可继续使用的页面:屏幕显示锁定原因,返回键无效;用户按 Home、打开最近任务或切到其他应用后,设备会重新回到锁定页;管理员解除后才恢复正常。
我在项目里处理这个功能时,最先踩到的坑是只做了一张全屏 Activity。它看起来像锁住了,但按一次 Home 就能离开,通知栏也还能下拉。真正能用的方案不是一个页面,而是下面这条闭环:
| 实现层 | 解决的问题 |
|---|---|
| 持久化锁定状态 | Activity 或进程重建后,仍然知道设备处于锁定中 |
| 全屏锁定页 | 展示锁定原因,消费返回键,并从最近任务中隐藏 |
| 无障碍前台监听 | 用户通过 Home、最近任务或其他入口离开后,重新拉起锁定页 |
| 顶部辅助功能窗口 | 在支持并验证过的设备上,限制从屏幕顶部下拉状态栏 |
| 有序解除 | 先清除状态,再移除窗口和关闭页面,避免旧事件把锁定页再次拉起 |

普通三方 APK 的设备锁定闭环
这里实现的是普通三方 APK 的业务锁定,不是 Android Keyguard,也不是 Device Owner 的 Lock Task/Kiosk。这个边界要先说清楚,但正文不展开权限体系,直接看功能怎么落地。
完整锁定还有一个硬前提:用户已经明确开启无障碍服务,而且服务当前处于连接状态。没有这个条件时只能展示一张页面,无法保证离开拉回和顶部限制,控制端应返回“能力未就绪”,不能返回锁定成功。
一、锁定状态必须独立于 Activity
锁定页是否还在屏幕上,只是界面状态;设备是否仍被锁定,才是业务事实。不能把一个静态的 isShowing 当成唯一依据,因为按 Home、旋转屏幕、系统回收 Activity 或进程重启,都可能让界面实例消失。
实际项目里,我会缓存当前设备控制状态,并在进程恢复后重新执行。下面把它缩成一个最小实现:只保存是否锁定和展示文案。
LockStateStore.kt
data class LockSnapshot(
val active: Boolean,
val reason: String
)
class LockStateStore(context: Context) {
private val prefs = context.applicationContext
.getSharedPreferences("managed_lock", Context.MODE_PRIVATE)
fun activate(reason: String): Boolean {
return prefs.edit()
.putBoolean("active", true)
.putString("reason", reason)
.commit()
}
fun deactivate(): Boolean {
return prefs.edit()
.putBoolean("active", false)
.remove("reason")
.commit()
}
fun current(): LockSnapshot {
return LockSnapshot(
active = prefs.getBoolean("active", false),
reason = prefs.getString("reason", null)
?: "设备已被管理员锁定"
)
}
}
这里刻意使用同步提交,是为了保证后续拉起界面前,磁盘中的锁定状态已经更新。正式项目也可以换成 DataStore,但顺序不能反:先写入锁定状态,再展示页面;解除时先清状态,再关闭页面。
二、全屏页只负责展示和拦截返回
锁定页本身不承担前台识别,也不决定设备是否仍被锁定。它只做三件事:读取锁定原因、隐藏系统栏、消费返回键。Manifest 中把它设为单任务,并从最近任务列表排除:
AndroidManifest.xml
<activity
android:name=".lock.ManagedLockActivity"
android:exported="false"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:theme="@style/Theme.App.Lock" />
ManagedLockActivity.kt
class ManagedLockActivity : AppCompatActivity() {
private val store by lazy { LockStateStore(this) }
companion object {
private val mainHandler = Handler(Looper.getMainLooper())


531

被折叠的 条评论
为什么被折叠?



