☰
Android 10灭屏调用DEVICE_POWER权限被拒?全面解决方案
2026/10/5 3:34:09 网站建设 项目流程

你们做系统定制或者深度ROM改造时,大概率遇到过这种场景:App在亮屏状态下跑得好好的,一旦灭屏,想自己点亮屏幕亮个通知、弹个提醒,或者反过来想自动把屏幕合上,结果系统直接抛SecurityException,Logcat里明晃晃一行“requires android.permission.DEVICE_POWER”。做Framework的同事一看就懂,这是Android 10上PowerManagerService对灭屏相关接口的调用卡死了权限。

这篇文章就把这个问题彻底捋一遍。我会从权限定义、调用链检查位置、解除限制的几种思路讲起,再给一套我自己在Android 10源码上验证过的白名单式修改方案,最后整理几个新手容易踩的坑。内容面向做ROM定制、系统应用开发、Framework二次开发的人群,普通App开发者也可以看看,了解系统权限的边界。

1. 为什么会有“灭屏调用DEVICE_POWER”这个坎

1.1 先搞清楚调用链:谁在拦你

Android系统里,“灭屏”相关的控制权高度集中。你写个App,调用PowerManager的wakeUp方法试图点亮屏幕,最终会通过Binder进入系统服务PowerManagerService。这个服务位于framework层,对任何调用的身份都会做权限校验。Android 10的PowerManagerService源码里,wakeUp、goToSleep、userActivity这三个关键方法,都有对应的权限检查逻辑。

问题就出在这儿。这三个方法恰恰是灭屏、亮屏、延长亮屏时间最常用的入口。如果你的App不是系统签名应用,没有DEVICE_POWER权限,在灭屏状态下调用wakeUp,直接会被enforceWakeUpPermission拦住,异常还没走到真正的逻辑就被丢回去了。所以很多第三方自动化工具、远程控制App在锁屏状态下想干点事,往往连第一步都迈不出去。

谷歌这么设计是有道理的。如果任何App都能随意点亮别人的屏幕、快速熄灭屏幕,整个系统的电源管理就乱套了,耗电和用户体验都会失控。但到了ROM定制领域,这个限制却经常成为障碍——比如你要做一款深度定制的行业终端,某个前台服务需要在灭屏状态下来电亮屏,或者配合传感器自动熄屏,这时候DEVICE_POWER权限就成了必须跨过去的坎。

1.2 权限定义到底长什么样

DEVICE_POWER这个权限,在AOSP源码里定义在frameworks/base/core/res/AndroidManifest.xml中,声明如下:

<permission android:name="android.permission.DEVICE_POWER" android:protectionLevel="signature|privileged" />

关键就在protectionLevel。signature表示只有和系统使用同一签名密钥的应用才能申请到;privileged表示放在/system/priv-app目录下的特权应用也可以获批。也就是说,常规第三方App在AndroidManifest里写了这个权限,安装时也不会真正拿到,PackageManager会直接忽略或者拒绝。

Android 10上系统服务端还有一层强制检查。PowerManagerService里有个内部方法叫enforceWakeUpPermission,里面就是一句话:

mContext.enforceCallingOrSelfPermission( android.Manifest.permission.DEVICE_POWER, null);

这句话的意思是:谁调用我,我就去查他的UID有没有被授予DEVICE_POWER权限。没有?那不好意思,SecurityException安排上。类似的检查在goToSleep、userActivity里也存在,只是名称和调用时机略有差异。除此之外,PowerManagerService内部还会区分调用来源是system、shell还是普通uid,部分场景下system uid和shell uid有一定放宽,但普通应用就是严格管控。

知道了拦截点在哪儿,接下来的事情就好办了——要么改权限定义,要么改检查逻辑,要么让目标App直接变成“自己人”。

2. 解除限制的四条路,怎么选

2.1 方案A:直接改权限等级,最简单也最危险

第一种思路非常直白:把frameworks/base/core/res/AndroidManifest.xml里DEVICE_POWER的protectionLevel改成normal或者dangerous。

<permission android:name="android.permission.DEVICE_POWER" android:protectionLevel="normal" />

改完之后重新编译framework-res.apk,刷进系统,你会发现任何App都能申请这个权限,调用wakeUp不再被拦截。这个方案改动量最小、见效最快,但我个人非常不建议在生产环境这么搞。

原因不复杂:DEVICE_POWER权限不只是控制屏幕亮灭,还关联着PowerManager里一系列其他操作,比如强制休眠、设置电源状态、修改WakeLock策略。把这种权限开放给所有App,等于让任何应用都能随意掌控电源行为,恶意应用可以直接让用户手机无限次亮灭屏、快速耗尽电量、干扰正常使用。系统定制的底线是功能可用且安全可控,粗暴放开权限属于拆了承重墙。

2.2 方案B:白名单式修改PowerManagerService,推荐路线

第二种思路,在PowerManagerService的权限检查环节做“定向放行”。核心逻辑是:保留DEVICE_POWER权限的整体保护等级不变,但在enforceWakeUpPermission等检查方法里,增加一个条件判断——如果调用方UID属于你指定的白名单包名,就直接跳过权限检查。

好处非常明显:其余99.9%的App仍然拿不到这个权限,系统整体的权限边界没有被破坏。你只需要为目标应用开一个口子,安全影响面小,也方便后期维护。风险在于你需要维护一份白名单列表,且改的是framework核心代码,编译周期比改个XML要长。

如果只是给一两个系统内置App用,白名单方案是成本和安全性最均衡的选择。如果给一个大型行业系统用,里面有几十个受控应用都要操作电源,那我会建议你把白名单做成可配置的,通过系统属性或者配置文件动态读取,别把名单焊死在代码里。

2.3 方案C:让应用变成系统应用,用platform签名一劳永逸

这是很多定制团队在用的一条路:把目标App的APK用系统的platform签名重新签一遍。因为DEVICE_POWER是signature级别权限,同一个签名下,这个App自然就被PackageManager认定为“可信应用”,安装时就能拿到权限。

操作上,编译环境里一般有platform.pk8和platform.x509.pem两个签名文件,用签名工具把APK重签后,直接push到/system/app或/system/priv-app,重启即可。这套方案的好处是无需改动framework源码,升级App时只要签名一致就能保持权限;劣势是签名密钥泄露等于整个系统权限防线崩溃,另外一旦App被用户提取出来装在别的手机上,因为签名不匹配,是无法正常安装的。

如果你的项目本身就是自有设备、行业终端,整个系统都是你签名的,那方案C往往是最省事的。如果只是需要在某个第三方App上做验证,方案B更灵活。

2.4 加一条:特权应用白名单privapp-permissions.xml

Android 9之后,系统加强了priv-app权限管理。即使你的App放在了/system/priv-app目录下,如果想要获得signature|privileged权限,还必须在/etc/permissions/目录下给这个App配一个privapp-permissions.xml白名单,否则PackageManager会拒绝授予privileged权限。

Android 10继续了这一机制,所以即便你把APK重签、放到priv-app,也千万别忘记这一步。配置文件写起来很直观:

<permissions> <privapp-permissions package="com.example.yourpackage"> <permission name="android.permission.DEVICE_POWER"/> </privapp-permissions> </permissions>

这个文件需要放进源码的frameworks/base/data/etc/或者直接打包进system分区,不同厂商的目录略有差异,务必按项目实际路径调整。我见过不少开发者在方案C上卡住,不是签名问题,而是少了这张白名单。

3. 实操:白名单方案的一个可落地改法

3.1 修改代码位置与具体思路

我自己在Android 10上验证过的方案,是基于方案B的变体。这里假设你的目标包名是com.demo.screencontroller,模块路径在frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java。

先定位到PowerManagerService的wakeUp方法,代码大致是这个样子:

public void wakeUp(long eventTime, int reason, String details, int uid, int opPackageName, int opUid, String opPackageName2) { ... enforceWakeUpPermission(); ... }

在enforceWakeUpPermission方法的实现里,做改动:

private void enforceWakeUpPermission() { // 新增:白名单包名放行 final int callingUid = Binder.getCallingUid(); if (isWhiteListedApp(callingUid)) { return; } mContext.enforceCallingOrSelfPermission( android.Manifest.permission.DEVICE_POWER, null); }

同时加一个判断方法:

private boolean isWhiteListedApp(int uid) { String[] whitelist = {"com.demo.screencontroller"}; String packageName = mContext.getPackageManager().getPackagesForUid(uid) == null ? null : mContext.getPackageManager().getPackagesForUid(uid)[0]; if (packageName == null) return false; for (String pkg : whitelist) { if (pkg.equals(packageName)) return true; } return false; }

这种写法有几个细节要注意。第一,不要直接用包名去对比调用方,而是先通过UID查出包名再对比,原因是Binder调用侧提供的包名可能是伪造的,而UID是系统内核层面的唯一标识。第二,白名单匹配用全包名精确匹配,别用substring这类模糊判断,容易误放行。第三,如果你希望系统App也能通过,可以把uid为SYSTEM_UID的情况一并放行,但尽量别这么做,系统本身走的是self permission路线,不需要额外开口子。

类似的方法在goToSleep、userActivity的权限检查处也补充放行。Android 10的PowerManagerService里还有一个squashPowerPermissionCheck之类的逻辑,实际改的时候你把源码全局搜一下“DEVICE_POWER”,把相关的enforce方法逐个加白名单判断即可。别只改了wakeUp就以为万事大吉,灭屏场景下你真正常调用的可能还有goToSleep。

3.2 编译、打包与推送

编译这种改动,只需要重新编framework模块,不用整机编译。如果你用的是AOSP,直接敲:

source build/envsetup.sh lunch <你的产品>-userdebug make services -j16

services对应的输出产物是services.jar或者services.vdex之类,具体要看你的编译目标。改的是services/core下的代码,编完后把产物推到设备里。如果是模拟器或者可root的设备,可以尝试:

adb root adb remount adb push <编译产物路径> /system/framework/ adb reboot

但说实话,framework的改动模块是services,在Android 10上通常已经经过dex2oat优化,直接push jar不一定生效,某些设备还会在启动时校验签名和完整性。更稳妥的做法是把整个system.img编译出来重新刷写,或者在userdebug版本上通过adb sync把改好的产物同步过去。对这个过程不熟的,建议先编整包,确保环境和产物是一致的。

3.3 别忘了Doze和WakeLock这两件事

很多人在这一步有个误区:以为权限放开之后,App灭屏状态下调用wakeUp就能立刻生效。实际上权限只是第一道门槛,还有第二道门槛就是Android 10的Doze限制。

当屏幕熄灭、系统进入Doze模式后,普通应用的后台CPU、网络、Alarm都会被限制。你的App如果不在电池优化白名单里,就算拿到了DEVICE_POWER权限,应用进程可能在灭屏后进入缓存状态,代码压根没有执行机会。解决方式很简单,在设置里给目标App打开“忽略电池优化”,或者通过代码引导用户去申请REQUEST_IGNORE_BATTERY_OPTIMIZATIONS权限。

另外,从应用架构上你还需要确保持有一个WakeLock。为什么?灭屏状态下,如果没有WakeLock锁住CPU,系统可能进入深度睡眠,等你的定时任务触发时,CPU已经被挂起,代码根本不会跑。常见做法是启动一个前台服务,持有PARTIAL_WAKE_LOCK,服务启动时用PowerManager创建WakeLock并acquire,任务执行完毕后release。

注意WakeLock不是万能的,它只能保证CPU保持唤醒,不代表屏幕会点亮。真正点亮屏幕还是要靠PowerManager.wakeUp,或者setFullScreenIntent配合全屏通知。不少开发者混淆了这两者,以为持锁就能亮屏,结果代码跑到了,屏幕还是黑的。

4. 典型问题与排查实录

4.1 改了权限还是报SecurityException

这是最常碰到的问题。代码改了、编译了、也push了,结果运行时还是报“Permission Denial: requires android.permission.DEVICE_POWER”。

我的排查思路是按照调用链一层层确认。首先,你的修改到底有没有生效。可以用反编译工具看看设备上的services.jar里是否有你加的白名单判断,或者直接log确认isWhiteListedApp有没有被调到。其次,确认调用方UID是不是目标App的UID。很多远程控制场景里,调用wakeUp的是系统里的某个代理进程,而不是你白名单里的App本身。最后,看看是不是别的服务抛的异常——比如你在用InputManager注入电源键事件,那个服务检查的是INJECT_EVENTS权限,跟DEVICE_POWER一点关系都没有。

曾经有人问我,为什么我改了PowerManagerService,但调用还是被拒。我让他把完整堆栈贴出来,才发现异常是从PhoneWindowManager那边抛出来的,根本不是PowerManagerService的锅。所以排查前先看清异常堆栈,别对着错误的源码改半天。

4.2 重启之后改动被覆盖

userdebug版本经常遇到这个现象。你push完framework重新开机,功能好使,但过了几天或者重新刷机后又变回原样。原因通常是系统分区被OTA或者启动校验还原了,framework模块的改动没有真正落进你的长期系统镜像里。

正规做法是把改动合入源码,重新生成固件包。只靠adb push做临时验证可以,但别指望它能长久生效。另外,部分厂商的Android 10会有dm-verity校验,system分区被改动后设备根本无法启动或者自动回滚,此时需要先关闭dm-verity,或者采用vbmeta配置来处理。这一步每个项目差异很大,没有统一命令,做之前先确认你手里的设备支不支持remount。

4.3 权限拿到了,但屏幕还是点不亮

这个问题很典型。App已经申请到了DEVICE_POWER权限,调用wakeUp也没有抛异常,但屏幕就是没反应。

我遇到的情况可以分为三类。第一类是屏幕亮度被系统设置成了最低且锁屏状态下有安全策略,手表或某些设备在锁屏时禁止直接亮屏,需要先解除Keyguard。Android 10上你可以调用setShowWhenLocked或者通过WindowManager添加FLAG_SHOW_WHEN_LOCKED。第二类是目标Activity没有使用全屏Intent,通知栏亮屏只适用于通知场景,直接调wakeUp后没有任何界面,系统出于省电策略可能又立刻把屏幕关了。第三类是硬件层面的传感器和防误触逻辑在起作用,比如距离传感器被遮挡时,系统会拒绝亮屏,这不是权限问题,而是PhoneWindowManager的preventScreenTurningOn逻辑在作怪。

碰到这类问题,除了看Logcat里的PowerManagerService信息,还建议用命令手动验证:

adb shell dumpsys power | grep -E "mWakefulness|mScreenBrightnessOverrideFromWindowManager|mProximityPositive"

重点看mProximityPositive是否为true,是的话说明距离传感器认为有东西挡着,亮屏操作被系统压住了。把传感器问题排除后,再去检查权限和WakeLock。

4.4 白名单列表太长,维护困难

当你需要放行的应用从1个变成几十个,代码里维护数组就变得很痛苦。我建议改动的时候就在源码里加一个可配置的读取逻辑,把白名单放到resource里,或者通过SystemProperties读取:

String config = SystemProperties.get("persist.sys.power.whitelist", ""); String[] whitelist = config.split(",");

这样后续加包名不用改代码,只要在设备上用setprop设置好即可。对于行业终端这种定制场景,灵活度会高很多。注意SystemProperties的值有长度限制,几百个包名就不要硬塞了,改用配置文件更合适。

4.5 安全性的再提醒

最后必须说一句,放开DEVICE_POWER权限只是手段,不是目的。做系统定制时,每一次权限松绑都应该记录在案,明确谁在用、什么时候用、为什么用。白名单方案相比全局放开安全得多,但也别因此掉以轻心——一旦你的白名单App被植入恶意模块,等于直接获得了电源控制能力。建议配合SELinux策略限制目标App的访问范围,不要给它多余的shell、root或者读写其他应用数据的权限。

结尾

PowerManagerService的权限拦截只是Android 10系统定制里的一个小点,但“灭屏后要操作电源”这个需求,几乎每个月都会有人踩一遍。我个人在实际操作中的体会是:方案B最稳,方案C最省事,方案A尝鲜可以,上生产绝对别用。改代码前先把调用链理清楚,改完后一定要测Doze场景、锁屏场景、传感器场景,不要只测亮屏状态下的正常调用。这个思路在Android 11、12上其实也适用,只是代码位置和部分方法名有变化,做版本升级时记得重新核对。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询