ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android系统定制:包名白名单放开DEVICE_POWER权限实现应用主动灭屏

Android系统定制:包名白名单放开DEVICE_POWER权限实现应用主动灭屏 最近在折腾一台 Android 10 的定制设备做的是车载/工控类型的项目客户提了一个需求希望机器在特定场景下由应用主动触发灭屏做一个类似“一键休眠”的交互。听起来不就是调一下PowerManager.goToSleep()嘛结果一查资料心凉了半截Android 在电源管理这一块把权限卡得非常死应用想控制屏幕熄灭必须先持有android.permission.DEVICE_POWER。这是个 signature 级别权限普通三方 APK 根本没有机会拿到系统应用没有平台签名同样没戏。跑了几轮实验之后我决定从 framework 层入手直接改掉 Android 10 里对DEVICE_POWER权限的限制。这篇文章把完整思路、源码改动位置、周边联动问题和踩过的坑都记录下来给后面做 ROM 定制或者系统集成的兄弟一个参考。1. 先搞明白DEVICE_POWER 到底卡住了什么1.1 这个权限管着电源管理的命脉DEVICE_POWER定义的 protectionLevel 是signature|privileged也就是说它只发给两类进程用平台签名签名的系统应用或者放在/system/priv-app目录下的特权应用。普通应用哪怕在 Manifest 里写了也是白写安装阶段就会被权限管理器直接忽略。它控制的不是单一功能而是整个 PowerManagerService 的核心操作。随便列几个都是平时看起来非常普通的 APIPowerManager.goToSleep()让设备进入休眠屏幕熄灭CPU 可以进入低功耗。PowerManager.wakeUp()从睡眠中唤醒设备点亮屏幕。PowerManager.shutdown()/reboot()关机、重启。PowerManager.userActivity()上报用户活动影响自动灭屏计时。Android 这么设计不是没有道理。如果这些操作用任意安装的应用都能调用那随便一个恶意 App 就能让你的手机永远无法亮屏或者在你不知情时把机器关机体验直接变成灾难。所以系统把控制电源的权限收紧到系统级普通开发者平时做应用根本碰不到这一层。1.2 灭屏请求的真实调用链先看应用层是怎么走的。一个 App 想主动灭屏代码上会这样写PowerManager pm (PowerManager) getSystemService(Context.POWER_SERVICE); pm.goToSleep(SystemClock.uptimeMillis());但goToSleep()在 SDK 里是隐藏的 SystemApi普通编译方式根本调不到得靠反射。应用层方法最终通过 Binder 跨进程进入PowerManagerService对应的方法是Override public void goToSleep(long eventTime, int reason, int flags) { // 权限校验就在这一行 mContext.enforceCallingOrSelfPermission( android.Manifest.permission.DEVICE_POWER, null); ... }校验不通过直接抛SecurityException。这个异常在应用层看到的就是“Permission Denial: requires android.permission.DEVICE_POWER”Logcat 一查就能看到。所以我们真正要动的就是这个enforceCallingOrSelfPermission的判定逻辑。只要在校验前让特定调用者绕过或者让权限校验结果始终为通过问题就解决了。1.3 什么场景需要动这个开关不是所有人都会碰到这种需求但下面几类场景很常见第一类是设备管理类项目比如医院自助机、工控一体机、商显终端。这类设备通常做的是垂直应用不允许用户随便按电源键关机但需要应用在空闲时主动灭屏进入待机。应用没有 root 也没有系统签名只能在 framework 层开个口子。第二类是车机或座舱项目。车机上经常有“息屏听歌”、“夜间模式自动息屏”的需求需要系统进程或者上层应用直接控制屏幕状态但由于整车项目签名体系比较复杂应用拿不到DEVICE_POWER于是也需要在系统服务层做适配。第三类是定制 ROM 做节电功能、定时休眠策略。比如厂商想根据前台应用类型或者传感器状态自动休眠有时候需要放开给某个特殊包名调用。总之需求归需求关键是在不破坏安全边界的前提下开一个可控的口子。2. 方案怎么选三选一我建议这样做2.1 全局放行方案改动最小但风险最高最直接的做法就是把PowerManagerService.java里所有enforceCallingOrSelfPermission(Manifest.permission.DEVICE_POWER, null)直接注释掉。这样以来任何应用都可以调用goToSleep()、wakeUp()甚至可以尝试关机重启。我其实很不推荐在生产版本里这么干原因很简单安全边界被彻底打开。你会发现不只是你要的那个应用能灭屏所有安装到设备上的 App 都能控制电源。万一某个内置应用被恶意利用或者用户装了来路不明的软件它可以无限次让设备休眠干扰正常使用甚至配合其他漏洞搞出更严重的问题。这种方案只适合两类情况一是开发调试阶段为了快速验证链路二是自己私人设备上玩一玩。真要发布给客户千万别做一刀切。2.2 包名白名单方案安全和功能兼顾更好的做法是保留原有的权限校验但在校验前增加一个判断如果调用者的 UID 对应的包名在我们维护的白名单里就跳过enforce。这种做法的好处很明显默认行为和原生 Android 完全一致白名单为空时系统不会有任何额外风险。只给具体包名放行不会影响到其他应用。改动集中在一个方法里后续要拓展成配置文件或者动态设置都非常方便。我在项目里最终选的就是这个方案。它虽然比全局放行多写几个方法但安全收益是实打实的。2.3 自定义权限方案正规但实现麻烦还有一种理论上的做法是自定义一个权限比如com.demo.perm.CONTROL_DEVICE_POWER在系统 Manifest 里声明为signature|privileged然后把PowerManagerService里的权限校验改成校验这个自定义权限再把你自己的应用签成系统签名。这个方案从规范和代码结构上看是最“正统”的也确实可行。但问题在于实际操作中你要处理签名统一、系统应用权限声明、Manifest 权限升级等一系列事情。如果项目本身就是整体定制 ROM那可以接受如果只是在一个成熟系统上做一个增量定制为一个小功能去动签名体系和系统 Manifest成本非常不划算。所以我的建议很明确优先做包名白名单。如果你以后遇到同类问题也可以按这个优先级去选。方案改动量安全性维护成本推荐度全局放行小低低不推荐生产使用包名白名单中高中推荐自定义权限大高高整机 ROM 项目可选3. Android 10 源码级修改把灭屏控制权给指定包3.1 修改文件和核心位置在 Android 10 里PowerManagerService的源码路径是frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java我们需要确认的就是goToSleep、wakeUp方法里的权限检查点。我用的是 Android 10 的 AOSP 分支API 29代码里看到的检查逻辑是这样的Override public void goToSleep(long eventTime, int reason, int flags) { ... mContext.enforceCallingOrSelfPermission( android.Manifest.permission.DEVICE_POWER, null); ... }wakeUp的代码类似Override public void wakeUp(long eventTime, int reason, String details, String opPackageName) { ... mContext.enforceCallingOrSelfPermission( android.Manifest.permission.DEVICE_POWER, null); ... }我们要做的就是在这些enforce调用之前插入白名单判断命中的包直接跳过原始权限校验。3.2 具体代码改动在PowerManagerService.java里加一个私有工具方法和一个包名字符串常量。包名这里我建议直接定义成常量或者后续改成从SystemProperties读取两种方式我都试过本质是一样的。private static final String DEVICE_POWER_WHITE_PKG com.demo.autosleep; private boolean isDevicePowerAllowed(int callingUid) { if (callingUid Process.SYSTEM_UID || callingUid Process.ROOT_UID) { return true; } String[] packages mContext.getPackageManager() .getPackagesForUid(callingUid); if (packages ! null) { for (String pkg : packages) { if (DEVICE_POWER_WHITE_PKG.equals(pkg)) { return true; } } } return false; }然后在goToSleep里改成这样Override public void goToSleep(long eventTime, int reason, int flags) { ... if (!isDevicePowerAllowed(Binder.getCallingUid())) { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.DEVICE_POWER, null); } ... }wakeUp方法里同样处理。注意Binder.getCallingUid()在这里拿到的是发起 Binder 调用的进程 UID而不是PowerManagerService自己系统进程的 UID。因为 Binder 进入系统服务后getCallingUid()返回的是客户端 UID所以这个判断是准确的。如果你想支持多个包名可以把常量改成一个数组private static final String[] DEVICE_POWER_WHITE_PKGS { com.demo.autosleep, com.demo.autoscreen };然后改成遍历数组比对。这个写法在后续新增允许包时不用改逻辑只改数组内容就行。3.3 编译和烧录验证流程Android 10 的模块编译可以直接单编services模块也就是PowerManagerService所在的服务 jar。命令如下source build/envsetup.sh lunch 你的产品名 mmm frameworks/base/services编译完成后产物在out/target/product/产品名/system/framework/services.jar如果你的设备支持 adb remount可以这样快速验证adb root adb remount adb push out/target/product/产品名/system/framework/services.jar /system/framework/ adb reboot需要说明的是现在不少 Android 10 设备使用 dynamic partition 或者 system-as-root直接 push 可能需要额外处理或者干脆打整包刷机。不同平台差异比较大这点要按你手头设备的实际刷机方式操作我只是提供一个通用流程。重启后抓日志确认系统服务正常起来了adb logcat | grep PowerManagerService如果服务没有 crash说明改法基本没问题。接下来就是写一个测试应用通过反射调goToSleep看灭屏效果。3.4 应用侧反射调用方法因为goToSleep对普通 SDK 不可见所以应用里要用反射。我在测试项目里是这样写的public static void gotoSleep(Context context) { try { PowerManager pm (PowerManager) context.getSystemService(Context.POWER_SERVICE); Method method PowerManager.class.getMethod( goToSleep, long.class, int.class, int.class); method.invoke(pm, SystemClock.uptimeMillis(), PowerManager.GO_TO_SLEEP_REASON_APPLICATION, 0); } catch (Exception e) { Log.e(AutoSleep, gotoSleep failed, e); } }注意 Android 10 的goToSleep(long, int, int)是三个参数。不同 Android 版本参数可能不一样我记得早期版本是goToSleep(long)还有版本是goToSleep(long, int)。如果你适配不同系统版本最好先反射读出方法列表再适配别死磕某一个签名。改了 framework 并放权之后这个调用就能正常让设备灭屏了不再抛SecurityException。4. 改完别急着乐灭屏链路周边要一起调4.1 ShutdownThread 里还有一处检查我在调试过程中发现了一个比较隐蔽的问题goToSleep的权限放开后应用调shutdown或者reboot依然会被拦住。这是因为ShutdownThread.java里还有一次DEVICE_POWER检查。相关文件路径frameworks/base/services/core/java/com/android/server/power/ShutdownThread.java代码里大概长这样if (mContext.checkCallingOrSelfPermission( android.Manifest.permission.DEVICE_POWER) ! PackageManager.PERMISSION_GRANTED) { throw new SecurityException(Requires DEVICE_POWER permission); }如果客户的需求只是灭屏那这里不用动。但如果你接到的需求是“让应用能关机”或者“能重启到 recovery”那这里必须同步做白名单处理否则你修好了前面后面照样被 SecuritException 卡住。我在项目里遇到过这个情况当时只改了PowerManagerService以为都放开了结果测试同事反馈关机被拒绝查了一圈才发现是ShutdownThread挡了一道。4.2 Doze 模式会吞噬灭屏后的计划任务做纯灭屏动作没有问题但如果你还有一个想法是“灭屏后应用要继续跑任务”比如息屏后定时上报位置、定时播放多媒体那你一定绕不开 Android 10 的 Doze 机制。Android 10 对后台限制比 Android 8.0、9.0 更严格。灭屏之后设备会逐步进入 DozeAlarmManager 的闹钟会被延迟网络访问会被挂起WakeLock 长时间持有也会被系统拦截。你即便通过 framework 放开了灭屏权限也只是让 App 能主动把屏幕弄灭App 自身的后台存活能力仍然受制于电池优化策略。处理办法通常是在应用里申请REQUEST_IGNORE_BATTERY_OPTIMIZATIONS让用户或管理端把 App 加入电池优化白名单。在设备管理场景下如果应用是 DeviceOwner很多限制本来就会放宽。如果是车机或持续供电设备直接设置Settings.Global里关闭 Doze 相关开关一劳永逸。但这属于整体系统策略需评估功耗影响。4.3 WakeLock 和用户活动事件别打架还有一个容易被忽略的现象你明明成功调用了goToSleep屏幕灭了结果几秒钟后又亮了。排查半天发现是某个系统组件在灭屏前后上报了userActivity事件或者另一个应用持有亮屏 WakeLock系统把它当成一次“用户正在使用设备”的事件于是取消休眠状态。这里我踩过一次测试应用本身在调灭屏前拿过一个 PARTIAL_WAKE_LOCK一直没释放灭屏后这个锁还在系统以为还有高优先级任务要跑过一会儿就把屏幕拉回来了。后面把 WakeLock 在灭屏调用前主动释放问题就消失了。另外wakeUp如果被同样放开一些后台应用会在灭屏后乱唤醒屏幕这类问题也要在联调时反复确认。给白名单的应用发版本时务必要检查它们是否合理使用电源管理 API。5. 踩坑实录这些坑我替你趟过了5.1 改了权限但灭屏还是失败按上面方案改完权限编译烧录后发现测试应用调用goToSleep还是没反应。我当时第一反应是权限放开的代码没生效回头检查代码逻辑确认没错后来发现是反射参数写错了。在 Android 10 上PowerManager.GO_TO_SLEEP_REASON_APPLICATION是一个 int 常量值是 4。如果不小心把它当成 flag 传错位置或者把SystemClock.uptimeMillis()传成了System.currentTimeMillis()设备端就会因为 eventTime 过大而拒绝执行。这个接口对时间参数很敏感一定要用uptimeMillis()。排查方式也不难打开 Logcat过滤PowerManagerService。如果看到类似PowerManagerService: goToSleep PowerManagerService: Sleeping...说明调用进去了。如果只看到权限拒绝日志说明白名单没命中如果什么都没有可能是反射方法没匹配上需要先getDeclaredMethods()看方法签名。5.2 白名单包名在多用户或共享 UID 场景下的问题如果你做的是多用户设备比如一台设备上有多个 user你的白名单应用可能只在某个 user 下安装了。这时getPackagesForUid(callingUid)的返回值取决于该用户在当时的包管理视图正常情况下能正确返回包名因为 Binder 调用方的 UID 已经包含了 user id 信息。但如果应用通过android:sharedUserId和别的应用共享 UID那么getPackagesForUid会返回共享 UID 下的所有包名。只要共享组里有一个包名在白名单内这个组所有应用都能获得灭屏控制权。我遇到过测试同事把一个调试应用和正式应用配了同一个 sharedUserId结果调试应用直接把正式系统的行为也带偏了。所以白名单判断的代码里最好严格匹配完整包名不要用contains或startsWith这类模糊匹配避免别人用一个com.demo.autosleep.evil来蹭权限。这一点我在代码注释里写得特别清楚后面接手的人也不会犯低级错误。5.3 静态常量不好维护改成配置化更省心刚开始我把白名单包名写死在PowerManagerService.java里测试阶段够用。但到了项目后期产品经理说要在不同批次设备上用不同包名每次重新编译推送 framework 太痛苦了。后来我改成了动态读取方案。比如从Settings.Global读String configPkg Settings.Global.getString(mContext.getContentResolver(), device_power_white_pkg);再配合一个系统端配置项或者干脆读取/vendor/etc/device_power_config.xml改动都不大。这样运营人员直接改配置文件或者用系统设置接口写入不用碰代码。我自己的做法是在isDevicePowerAllowed里先查内存缓存再查设置项确保性能和灵活性兼顾。日常小型项目用静态常量完全够了但如果你判断这个需求会持续迭代强烈建议一步到位做成配置化。5.4 安全测试和 CTS 相关风险最后提醒一句任何对权限模型的修改都可能在 CTS/VTS 或者厂家安全测试中暴露问题。Android 原生测试用例里有不少是针对DEVICE_POWER的权限校验的如果全局放行测试必然失败。即使做白名单全局扫描工具也有可能发现系统行为与原生存在差异。我的处理方式是给改动加了编译开关private static final boolean ENABLE_DEVICE_POWER_WHITELIST true;将来要出安全合规版本直接把开关置为 false白名单逻辑完全不进入代码路径。这种增量改动风险最小也方便跟安全评审解释。另外在交付文档里我会把修改文件、修改点、白名单包名列表、编译开关位置全部列清楚方便后续有人接手时能快速定位。毕竟系统级改动不像应用层那样能随手回滚文档写得越细后面维护越省力。我自己做完这个改动的体会是Android 权限收紧的氛围越来越明显能通过系统正常渠道解决的问题尽量不要动 framework。但如果项目已经把路堵死了包名白名单配合编译开关是当前最可控的妥协方案。整个流程走下来核心工作量其实不在那几行权限代码而是在周边的调用链梳理和测试验证。希望这篇文章对正在做同类定制的朋友有帮助。
返回列表