Android 的 Cross-Profile Intent 限制,工作资料和个人资料的隔离

Android 的 Cross-Profile Intent 限制,工作资料和个人资料的隔离

Android 的 Cross-Profile Intent 限制,工作资料和个人资料的隔离


Android 的 Cross-Profile Intent 限制:企业安全的遮羞布,还是开发者的新牢笼?


如果你手上有一台运行 Android 14 或 15 的 Pixel 手机,并且通过公司 MDM enroll 了一个 Work Profile,大概率会碰上一个极其诡异的 bug:工作资料里的 Slack 或者企业微信,点击一个外部链接时,系统不再弹出 Chrome 或者 Edge,而是直接告诉你「没有应用可以执行此操作」。更离谱的是,你个人资料里明明装了好几个浏览器,而且它们在工作资料的应用列表里也能看到影子,但就是跳不过去。抓个 log 看看,会看到一行让开发者血压飙升的日志:SecurityException: Permission Denial: starting Intent { act=android.intent.action.VIEW dat=https://... } from UserHandle{10} not allowed because the target is in a different profile


这就是 Google 在 Android 14 里默默收紧的 Cross-Profile Intent 限制。说它「默默」,是因为直到你的线上用户开始投诉之前,官方文档里几乎找不到一句人话能把这件事讲清楚。Device Policy API 的文档像考古现场,StackOverflow 上的回答还停留在 Android 11 的蜜月期,而 AOSP 代码里那几道锁,已经实实在在地把 Personal Profile 和 Work Profile 切成了两个世界。


Android 14 那道看不见的墙


事情在 Android 14(API 34)之前还不是这样的。早些年,Work Profile 虽然基于 Android 的多用户架构(Multi-user),Google 在代码层面做了不少妥协来让企业应用和个人应用能勉强对话。系统服务里有一个概念叫 INTERACT_ACROSS_USERS_FULL,只有系统签名或者持有特定权限的应用才能跨用户边界发送 Intent,但普通应用可以依靠 DevicePolicyManager#addCrossProfileIntentFilter 这个 API 来注册白名单。企业 IT 管理员配置一下策略,工作资料里的邮件客户端就能唤起个人资料里的地图应用,反过来也行。那时候虽然也有诸多限制,但至少链路是通的,开发者不需要关心底层 UserId 的映射关系。


Android 14 发版后,Google 在 ActivityStarter.javaIntentFirewall 里加了新的检查逻辑。核心变化是:跨 Profile 的隐式 Intent(Implicit Intent)默认被系统直接拒绝。只有显式 Intent(Explicit Intent),也就是你明确指定了 ComponentNamesetPackage() 的 Intent,才有可能通过层层安检。而且这个「有可能」还得满足一个额外条件:目标 package 必须被 IT 管理员通过 DevicePolicyManager#setCrossProfilePackages() 或者 addCrossProfileIntentFilter 显式加入白名单。


换句话说,如果你的应用在工作资料里,想调起个人资料里的某个应用处理一个 ACTION_VIEW,除非你提前知道那个应用的精确包名和 Activity 路径,并且你的企业 IT 部门恰好把这个目标应用加入了白名单,否则系统会直接在 resolveActivity 阶段就把你掐死。用户看不到崩溃,但会看到那个经典的「无法打开此链接」Toast,而 logcat 里躺着的就是前面提到的那条 UserHandle{10} 权限拒绝。


这个 UserHandle{10} 很关键。Android 的 Work Profile 本质上是在主用户(User 0,Parent Profile)之上创建的一个次级用户(通常是 User 10 到 User 15 之间,取决于设备厂商)。虽然 Google 在 UI 层把它包装成一个「资料」而不是「用户」,但底层跑的是同一套 UserHandle 隔离机制。Linux UID 通过 PER_USER_RANGE(通常是 100000)做偏移,文件系统有独立的 /data/user/10,连 PackageManager 的解析缓存都是分开的。Google 一直对外宣传 Work Profile 让工作和生活无缝切换,但从系统架构来看,这从来就不是无缝的,它从一开始就是两道墙,只是 Android 14 之前墙上开着几扇透风的窗户,现在 Google 觉得风太大,直接把窗户焊死了。


工作资料不是第二个用户,但 Google 一直这么假装


Android Enterprise 团队在市场宣传里最喜欢用的词就是「balance」和「separation without duplication」。听起来很美:工作应用和个人应用分开,数据不互通,下班后关掉 Work Profile,所有企业应用图标变灰,世界瞬间清净。但实际上,从 AOSP 的代码来看,Work Profile 就是一个完整的 secondary user,只不过被 UserManager 打上了 FLAG_MANAGED_PROFILE 标记,然后在 SystemUI 里做特殊渲染,让它看起来像是「内嵌」在主用户里而已。


这种架构上的偷懒导致了后续一堆历史包袱。真正的跨用户通信在 Android 里一直是个黑魔法。系统应用靠 INTERACT_ACROSS_USERS 权限,framework 靠 ActivityManagerService 里的 isSingleton 组件判断,而普通应用以前靠 FLAG_MANAGED_CAN_ACCESS_PARENTFLAG_PARENT_CAN_ACCESS_MANAGED 这两个 Intent filter flag 来打通。很多开发者甚至不知道这两个 flag 的存在,但它们确实在 Android 13 及之前版本里维系着跨资料分享的最后体面。


到了 Android 14,Google 工程师在 Intent 解析链路里加了一个 canInteractAcrossProfiles 检查。如果你在 AOSP 源码里搜索 isIntentRestricted 这个私有方法,会发现它的逻辑极其粗暴:只要发送方和接收方不在同一个 UserHandle,且没有通过 enterprise policy 显式放行,系统就直接返回 RESTRICTION_BLOCKED。更有趣的是,这个检查发生在 resolveActivity 阶段,意味着你的应用甚至拿不到 ActivityNotFoundException 之外任何有意义的反馈。你没法告诉用户「因为公司策略限制,所以打不开」,你只能告诉用户「没找到应用」——这他妈是两种完全不同的用户体验。


Google 做这件事的公开理由是安全。企业安全团队担心工作资料里的恶意应用通过隐式 Intent 扫描个人资料里的应用列表,进而推断用户的私人偏好(比如装了哪些银行 App、约会软件)。这个顾虑本身不算错,但解决方式却极其懒惰。一刀切地禁止隐式 Intent,相当于因为担心有人用刀杀人,就把所有人的刀没收,连切菜的权力也剥夺了。真正的企业级安全应该依赖用户显式授权和细粒度审计,而不是在 Framework 层直接让 API 行为回退到十年前。


Intent 防火墙的灰色地带


让我们看一个更具体的场景,这个场景在 GitHub 的 AOSP issue tracker 和企业开发者论坛里被反复吐槽:从 Work Profile 分享图片到 Personal Profile。


假设你公司用的钉钉装在工作资料里,你收到了一张设计稿,想把它发到个人资料里的微信或者 Telegram 做进一步处理。你点击分享按钮,钉钉构造了一个 ACTION_SENDmimeTypeimage/png,附上一个 content:// URI。在 Android 13 上,只要 IT 管理员配了默认的 cross-profile intent filter,系统会弹出分享面板,里面既有工作资料的应用,也有个人资料的应用。但在 Android 14 上,这个分享面板里可能只剩下工作资料的那几个应用了。个人资料里的微信、WhatsApp、Snapseed 全部消失。


为什么?因为分享面板(Share Sheet)依赖 Intent.createChooser()PackageManager#queryIntentActivities() 做跨资料聚合。而 Android 14 的改动让 queryIntentActivities 在跨用户查询时直接过滤掉了未授权的组件。系统不再帮你做合并,Work Profile 的 Launcher 甚至不会向 Parent Profile 发起跨用户的包查询。除非钉钉的开发者显式通过 CrossProfileApps API(android.content.pm.CrossProfileApps)去唤起目标应用,否则这条路就是死的。


CrossProfileApps 这个 API 本身也是个半成品。它允许你启动另一个 Profile 里的特定 Activity,但要求你提前知道目标包名,并且用户必须在设置里手动开启某个隐藏的开关。更讽刺的是,CrossProfileApps#canInteractAcrossProfiles() 这个方法的返回值在 Android 14 上基本等于抛硬币——它依赖的策略配置项分散在 DevicePolicyManagerUserManagerAppOpsManager 三个服务里,任何一个没对齐就会返回 false,而开发者拿到 false 之后没有任何补救手段。


还有更隐蔽的坑:URI Scheme。很多应用注册了自定义 scheme 来做深度链接,比如 slack://channel?id=xxx 或者 notion://page/xxx。在 Work Profile 环境下,这些 scheme 的解析完全依赖 IntentFilter 的匹配。Android 14 之后,如果 scheme 的解析目标跨了 Profile,系统会直接拒绝匹配,哪怕目标应用已经安装且 scheme 声明无误。你在 logcat 里看不到任何 PackageManager 的解析日志,因为 IntentResolver 在跨用户分支里直接 return 了 null。对于做深度链接体系的开发者来说,这简直是灾难——你的 Universal Link 体系在 Work Profile 用户那里会莫名其妙地全部失效,而你甚至无法从崩溃统计里感知到这个问题,因为它不抛异常,只是静默失败。


企业 MDM 厂商的沉默与 Google 的专制


面对这种限制,企业 MDM 厂商的反应出奇地一致:装死。


Microsoft Intune、VMware Workspace ONE、MobileIron,这些卖企业服务的厂商在它们的官方文档里给出的解决方案永远是同一句话:「请联系您的 IT 管理员配置 Cross-Profile Intent Filter。」这等于什么都没说。因为大部分中小企业的 IT 管理员根本分不清 addCrossProfileIntentFiltersetApplicationRestrictions 有什么区别,而 MDM 厂商自己的控制台 UI 也只是把 Google 的 API 套了一层皮,策略下发下去有没有生效,全靠运气。


Google 自己的 Workspace 套件则是另一番景象。Gmail、Google Drive、Chrome 这些亲儿子应用在跨资料场景下享有一定的「系统级豁免」。AOSP 代码里虽然看不到硬编码的白名单,但 Google 移动服务(GMS)里的 GooglePermissionControllerIntentResolverService 显然有额外的通道。这造成了一个不公平的竞争环境:第三方企业应用被锁在 Work Profile 的牢笼里动弹不得,而 Google

Android 开发者官网的隐藏页面和工具 2026-08-14

评论区