做AOSP定制的兄弟应该都懂,系统录屏时的确认弹窗是个很烦人的东西。不管你是给教学平板做自动录屏,还是给企业设备做远程协助,只要产品需要无人值守地采集屏幕内容,这个弹窗就会一遍遍打断自动化流程。Android 14里这个情况还更特殊,因为隐私保护逻辑加了不少新约束。我在自己项目里刚好把“去掉录制屏幕弹窗”完整走了一遍,从原理到改源码再到编译踩坑,整理一版实操笔记出来,给有同样需求的朋友做个参考。
1. 先把需求拆清楚:这个弹窗到底是谁弹的
1.1 弹窗的“真身”不是录屏服务,而是权限申请页
不少第一次接触这个需求的人会先去MediaProjectionManager、MediaProjection这些类里面翻,但实际让你点“允许/取消”的确认框,跟录屏的数据流是两套东西。
应用层拿录屏能力走的是这套链路:应用调用MediaProjectionManager.createScreenCaptureIntent(),拿到一个系统级的 Intent,然后startActivityForResult()把这个 Intent 发出去。这时候系统会拉起一个专门的权限确认页,用户点“允许”,系统回调RESULT_OK和对应的 data Intent,应用再用这个结果去获取MediaProjection实例。
这个权限确认页在Android 14的AOSP里位于packages/apps/PermissionController,具体类是com.android.permissioncontroller.permission.ui.MediaProjectionPermissionActivity。它是独立于录屏服务的,去掉录屏弹窗最直接的思路,就是让这个 Activity 在收到请求时不再弹 UI,而是直接返回授权结果。
1.2 Android 14为什么把权限收得更紧
Android 14对MediaProjection的限制确实比之前的版本严。
首先从应用侧来说,Android 14要求调用createScreenCaptureIntent的应用必须在前台运行,不能像以前那样在后台偷偷拉起授权请求。其次,应用如果要持续投屏/录屏,需要在前台服务中声明mediaProjection类型,并且在前台服务启动时拿到对应的MediaProjection实例,否则会直接抛SecurityException。
这些约束会对“去掉弹窗”的改造产生连锁反应。比如你把弹窗绕过之后,如果目标应用没有正确申请前台服务类型,录屏会话依然可能启动失败;又比如系统在启动MediaProjection会话时,SystemUI 里会同步出现“正在录制”的红色提示条,这在特定定制场景下也需要隐藏。
所以完整的“去掉录屏弹窗”改造,包含两件事:一是取消权限确认环节,二是确保后续录屏会话能正常拉起和持续运行。两个环节缺一个,都会出现“弹窗没了但录不了”或“录了几秒自己断掉”的尴尬现象。
1.3 哪些场景需要动这个弹窗
我在实际接触的项目里,主要遇到三类需求:
第一类是自动化测试。跑UI自动化时,如果需要录制机内屏幕做取证,每次都要人工点允许,测试流程根本没法持续跑。第二类是政企/教育定制设备,管理员希望设备开机后自动录屏,或者让远程协助方直接看到被控端画面,不能每次连接都让用户做选择。第三类是录屏工具类App想给用户“零打扰”体验,直接在应用内完成所有授权逻辑,不让系统弹窗打断操作。
不论哪类场景,最终在系统层要做的事情都是一样的:让MediaProjectionPermissionActivity不出现,或者让它自动放行。但“自动放行”这个动作怎么做,有不同的技术路线,下面我逐个拆一下。
2. 去弹窗方案选型:三条路子怎么选
2.1 方案一:修改PermissionController,自动返回授权结果
这个方案改动范围最小,也最符合系统原本的设计语义。核心思路是让MediaProjectionPermissionActivity在onCreate阶段直接“代用户”做出允许选择,然后setResult+finish。
好处很明显:权限确认链路完整保留,系统回调逻辑不变,目标应用不需要感知到系统被改动过。坏处也很明显:所有应用都会“被自动授权”,等于把整个系统的录屏权限白名单彻底打开。在政企单应用设备上这没问题,但在面向大众用户的多应用设备上,任何应用都能无感录屏,隐私层面的风险太高。
所以这个方案适合“设备上只有受信应用”的封闭场景,不适合开放市场的消费级ROM。
2.2 方案二:MediaProjectionManagerService 服务端放行
这个方案不做UI层的自动点击或自动返回,而是直接改frameworks/base/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java,在服务端判断用户Uid、包名、调用场景,对命中白名单的调用直接授予投影权限,其他情况继续走原确认流程。
好处是可控性强:完全由白名单决定谁可以免确认录屏,而不是全局放开。坏处是改动点在系统服务层,相对底层,代码编译和排查门槛稍高,而且各家AOSP版本的MediaProjectionManagerService内部实现差异不小,从Android 13到14这段代码变化的幅度就挺明显,升级版本时补丁要跟着适配。
如果你做的是设备级ROM,又希望普通应用保持原来的弹窗授权机制,只给特定几个内置应用开免确认通道,方案二就是比较合理的路线。
2.3 方案三:AppOps 预置权限
还有一个稍微“野”一点的思路,就是绕开源码改动,在系统启动或以root身份执行时,直接对目标包名设置PROJECT_MEDIAAppOps 权限为允许:
adb shell appops set <package> PROJECT_MEDIA allow如果你只是想在开发阶段快速验证,这条命令确实能免除一部分确认流程。但它的作用范围有限:AppOps 放行不等于MediaProjectionPermissionActivity完全不会出现——在部分Android 14版本上,权限确认 Activity 的启动判断不只依赖AppOps,还会叠加其他运行时状态,例如应用是否处于前台、是否已经拥有MediaProjectiontoken 等。它用来调试排查、临时验证是够用的,但作为量产ROM的正式方案不太够格。
2.4 我的选择:主改UI层授权 + 服务端白名单兜底
我在自己项目里采用的是“方案一 + 方案二”组合:先在MediaProjectionPermissionActivity层对指定包名自动放行,同时在MediaProjectionManagerService里加一个白名单判断,让系统内部调用路径不需要再依赖Activity回调结果。
这样做的原因是,Android 14上的投屏/录屏入口不止createScreenCaptureIntent一条:带MediaProjection的VirtualDisplay创建还会涉及MediaProjectionManager.createVirtualDisplay,而有些系统内置应用会走系统权限绕过用户确认。如果只在UI层改,部分系统内部入口仍然会被拦;如果只在服务端改,开放源码的权限确认Activity又可能在一些边界情况下被拉起。两层一起改,覆盖面最全,也最稳。
下表是三条方案的对比,方便你根据自己项目形态做选型:
| 方案 | 改动位置 | 控制粒度 | 风险 | 适用场景 |
|---|---|---|---|---|
| PermissionController自动授权 | 应用层UI | 全局或包名判断 | 全部应用可无感录屏 | 封闭设备、单应用场景 |
| MediaProjectionManagerService白名单 | 系统服务 | 可按Uid/包名精确控制 | 改动底层,需适配版本差异 | 多应用设备、定向放行 |
| AppOps预置权限 | 系统设置项 | 按包名 | 不彻底,部分版本失效 | 开发调试、快速验证 |
3. AOSP实操:在PermissionController里直接放行
3.1 准备工作和源码定位
动手之前,先确认你的AOSP源码环境是完整的,最好已经成功编译过一遍基础镜像。没编译过的源码树,直接改完再编译会遇到一堆环境问题,很难判断到底是代码问题还是构建问题。
确认环境没问题后,我先定位MediaProjectionPermissionActivity:
find packages/apps/PermissionController -name "*MediaProjection*"Android 14的路径一般是:
packages/apps/PermissionController/src/com/android/permissioncontroller/permission/ui/MediaProjectionPermissionActivity.kt这个文件从Java改成了Kotlin实现,结构跟老版本差异不小。展开之后最核心的就是onCreate里的授权链路。如果你只是想要一个快速验证版,可以直接在onCreate里把授权结果设置好:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (intent?.action == MediaProjectionManager.ACTION_MEDIA_PROJECTION_PERMISSION) { val resultData = intent.getParcelableExtra<Intent>( MediaProjectionManager.EXTRA_MEDIA_PROJECTION_PERMISSION_DATA ) val resultIntent = Intent() resultIntent.putExtra( MediaProjectionManager.EXTRA_MEDIA_PROJECTION_PERMISSION_DATA, resultData ) setResult(RESULT_OK, resultIntent) finish() return } // 原有流程 bindAndStartService() ... }注意上面这版改动是“对所有应用全局放行”,只适合封闭场景验证,量产用的版本一定要加包名或签名校验。我实际用的版本会再套一层签名白名单判断,命中白名单才自动放行,否则继续走弹窗。
3.2 别忽略权限回调的附加数据
改完setResult之后,有一个必须确认的细节:回调给上层应用的resultIntent里,那个额外的EXTRA_MEDIA_PROJECTION_PERMISSION_DATA不能丢。这个 data Intent 是MediaProjectionManagerService后来用来恢复会话状态的凭据,少了它,应用拿到了RESULT_OK但后续调用getMediaProjection时依然可能失败。
有些朋友改完代码发现“弹窗是没了,但应用一直拿不到投影会话”,很大原因就是这里只顾着setResult没把附加数据原样传回去。在系统源码里,这段数据其实是MediaProjectionPermissionActivity在正常用户点击允许后,从MediaProjectionManagerService那里拿到的结果。我直接把intent里的getParcelableExtra原样回填,保证了链路一致。
3.3 服务端白名单兜底改法
接着处理MediaProjectionManagerService的白名单兜底。这个类的位置在:
frameworks/base/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java它的核心逻辑是维护一个MediaProjection会话集合,授权来源会走到内部的状态判断。我加的改动是在授权判断入口处,先检查调用者包名是否命中预置名单:
private boolean isWhitelistedProjectionApp(int callingUid, String packageName) { final String[] whitelist = getResources().getStringArray( R.array.config_mediaProjectionAutoGrantPackages); if (whitelist == null || whitelist.length == 0) { return false; } for (String pkg : whitelist) { if (packageName != null && packageName.equals(pkg)) { return true; } } return false; }然后在服务内部创建/校验 MediaProjection 权限的地方,命中白名单时直接放行,否则走原有的权限确认逻辑。
当然,不同AOSP版本这段代码的位置差异较大。Android 14 release分支里,授权相关的入口和AppOps检查、foreground检查耦合在一起,我建议不要硬套网上的旧补丁,而是先花点时间把当前版本的调用链看清楚,再把白名单判断插到最外层入口。你可以在改之前先编译一遍基线代码,确认哪些文件、哪些方法会被调用到,避免盲改。
3.4 编译刷机验证的流程
改完代码后,需要分别编译 PermissionController 和 framework:
source build/envsetup.sh lunch <your_product>-userdebug m packages/apps/PermissionController m services只编模块然后adb sync到设备上,速度会快很多。但如果改动涉及 framework 服务,一般需要连带重启 system_server,或者直接整机刷入。更稳的做法是编完所有相关镜像后,用fastboot flashall刷机验证,避免模块间版本不一致导致诡异问题。
刷完机后验证的点有两个:第一,目标应用调用createScreenCaptureIntent的时候,不再出现任何中间确认页,直接进入录屏状态;第二,录屏数据也正常出来了,不是黑屏或花屏。只验证弹窗消失但不验证录屏结果,等于没测。
4. 编译报错实录:out/soong/build.ninja 这类坑怎么填
4.1 一个典型的构建失败现场
改造Android 14源码时,我遇到过好几次类似这样的构建失败输出:
FAILED: out/soong/build.ninja cd "$(dirname "out/soong/build.ninja")" ... soong bootstrap failed with: exit status 1看到FAILED: out/soong/build.ninja,有些朋友第一反应是去修 ninja 文件,其实方向就错了。out/soong/build.ninja是 Soong 构建系统根据你当前环境、产品配置、源码变更自动生成的总构建描述文件。它本身不是手写的,而是由soong_build生成的,真正失败的点通常在“生成这个 ninja 文件”之前的分析或配置阶段。
这个时候重点看两类日志:一类是out/soong/soong.log,另一类是out/soong/soong_ui.log。如果是环境问题,日志里一般会直接报 Python 依赖缺失、磁盘空间不足、Java 版本不符;如果是源码本身的问题,比如模块依赖写错、MK/BP文件语法错误,日志会把具体涉及的Android.bp或Android.mk指向出来。
4.2 常见的根因和处理方法
我结合自己这次踩坑的经验,整理一下最常见的几个根因:
第一,目录残留脏数据。上一次编译在不同产品配置或不同分支下留下了旧产物,再切回当前配置时,Soong 的状态文件和产物缓存不一致,导致重新生成 build.ninja 失败。处理方式比较直接:
rm -rf out/soong重新构建。这个操作会丢掉 Soong 的增量缓存,让系统重新做完整的环境探测和产物分析。如果还不行,就把整个out目录清掉重来。代价是编译时间变长,但能解决绝大部分状态错乱问题。
第二,并发量大导致资源耗尽。m -j64这种高并发在内存不够的机器上,经常出现构建进程被杀、soong_build 中途退出的情况。想判断是不是这个问题,看soong.log尾部有没有内存分配失败的痕迹。遇到的话,调低并发数,比如m -j16或者干脆让系统自己分配m。
第三,代码里的语法错误被 Soong 规则检测到,但报错信息被 build.ninja 的失败日志掩盖了。这时看soong.log里最早出现的报错,定位到具体Android.bp文件的那一行。这种问题需要冷静顺着日志往前找,不要在out/soong/build.ninja附近反复纠结。
4.3 模块编译和全量编译的选择建议
我自己的习惯是,改动小模块时先用模块编译验证:
m packages/apps/PermissionController模块编译能过,再考虑整机刷入。如果模块编译阶段就遇到out/soong/build.ninja类错误,大概率不是这个模块本身的问题,而是项目整体构建状态的问题。这时候检查环境变量是否污染、有没有多开终端同时编同一棵源码树。
另外提醒一句:尽量不要在同一棵源码树里反复切换 lunch 目标。AOSP的增量系统对“换目标”的支持并没有想象的那么健壮,很多离奇的build.ninja报错都是切换目标导致的。有条件的话,不同产品目标用不同的out目录,或者编完一个目标后彻底清理再编下一个。这个习惯能帮你少踩很多坑。
5. 常见问题与避坑手册:弹窗没了只是开始
5.1 弹窗是没了,但录出来是黑屏
这个现象在Android 14上非常典型。原因多半不在权限确认环节,而是MediaProjection的会话没有真正绑定到有效的VirtualDisplay。应用拿到授权结果后,需要调用getMediaProjection获取实例,再配合VirtualDisplay把屏幕内容投递到应用侧。
如果在服务端我做白名单放行时,没有同步处理好 token 的注册和会话恢复,就会出现“授权状态ok但数据管道没建立”的假成功。排查时先确认应用侧日志里onStart回调是否触发,再确认VirtualDisplay的Display参数是否有效。不要一上来就怀疑系统改动,先把标准录屏流程用 ADB 脚本跑一遍,能录再回头查定制逻辑。
5.2 目标应用设了前台服务,却依然被拒
Android 14的mediaProjection前台服务类型有个特殊之处:系统要求应用启动前台服务时,必须已经持有有效的MediaProjection实例,否则会抛SecurityException。这意味着“先启动前台服务再申请录屏”的老写法在Android 14上可能不工作,得先申请录屏能力、拿到实例再启动前台服务。
这个时序问题在定制ROM关闭弹窗后会变得更隐蔽,因为应用开发者还以为系统弹窗是因为自己的代码写得不对。我在项目里专门写了一份适配说明,提醒预置应用使用如下顺序:先声明foregroundServiceType="mediaProjection",在应用进入前台后发起录屏请求,拿到MediaProjection之后立即启动前台服务。把这个时序理顺了,弹窗消失之后录屏才能稳定运行。
5.3 录屏过程的通知和状态栏提示如何隐藏
系统级录屏激活后,用户会在状态栏看到一个红色的“正在录制”指示,通知栏也会出现对应通知。在一些无人值守设备上,这种视觉提示会暴露录屏行为,也需要一并处理。
这个提示由 SystemUI 里的媒体投影通知逻辑控制。如果只是在 PermissionController 层放行,通知还是会正常出现。想隐藏的话,需要到 SystemUI 源码里找到MediaProjection或ScreenCapture相关的通知创建处,把对应系统通知屏蔽,同时保留应用自身录屏状态。这里建议不要直接把所有通知都禁掉,因为录屏通知在某些场景下跟用户交互有关联,比如停止录屏的操作入口。完全屏蔽后,你会失去一个快捷停录途径,务必要在应用层提供等效的停止入口。
5.4 隐私安全的权衡
最后再说一个容易被忽略的点。去掉录屏弹窗本质是在削弱系统级隐私屏障,这在开发机和封闭设备上没问题,但如果产品要对外发布,你就得想清楚如何应对合规和用户隐私诉求。
我自己的处理思路是:保留服务端白名单,默认不全局放行;所有预置录屏应用必须声明明确的录屏用途;设备提供“录屏状态常驻通知”的全局开关,企业管理员可以决定是否关闭提示。这套组合既满足了无人值守录屏的业务需求,又不至于把系统的隐私保护完全打开。毕竟“技术上能不能做”是一回事,“产品上应不应该全开”是另一回事。
改完这轮MediaProjection相关代码,我自己最大的感受就是:别把“去弹窗”想成一个点,它是一整条链路上的状态同步问题。从权限申请、服务端授权、会话创建到SystemUI提示,每个环节都像串联的灯,只关掉其中一盏,另外几盏还会在某个时刻突然亮起来。先把整条链路跑通、理解透,再动手改,会省下好几轮测试时间。