1. 先想清楚:你要哪种“设置默认”,这决定了 API 路线做 Android 系统定制和应用开发的朋友,应该都遇到过这个需求:设备上装了好几个桌面(Launcher),用户一按 HOME 键就弹出一个“选择要使用的桌面”列表,领导要求开机或者升级之后必须直接落在我们预置的那个桌面上。于是很多人会搜“Android 11.0 设置默认 Launcher 的方法”。这个标题看起来是个一句话需求,实际做起来最大的坑在于:同一句话背后有好几条完全不同的技术路线。你是系统应用、MDM 企业管控应用,还是普通三方应用,能用的 API、权限要求、用户感知完全不一样。不看场景直接抄代码,十有八九会翻车。1.1 三种典型场景与权限前提我把实际项目里最常见的三种诉求整理成了下面这张表,建议你先对号入座:场景开发者身份需要的前提条件用户是否感知定制 ROM 预置桌面,开机就是自家 Launcher系统应用(平台签名)MANAGE_ROLE_HOLDERS 签名权限 + privapp 白名单完全静默企业设备强制绑定某桌面,防止员工切换设备所有者(Device Owner)应用DevicePolicyManager 设备所有者身份完全静默普通三方 App 引导用户把自家设为桌面普通应用无特殊权限,只能弹系统确认框用户手动确认第一种是我见过的绝大多数定制 ROM 项目的诉求,也是这篇文章的绝对主角。第二种多见于 MDM 方案,它的实现思路和第一种不完全一样,但有不少人会混淆,后面我会单开一节讲清楚。第三种其实不算“设置”,而是“引导用户去设置”,因为 Android 从 10 开始把默认桌面这类系统角色的授予权收得非常紧,普通应用没有能力静默修改。1.2 为什么 Android 11 上“万能代码”基本绝迹如果你是从 Android 7、8、9 一路做过来的,应该记得以前有一些“野路子”能绕过去:反射调用 PackageManager 的 addPreferredActivity、直接改 Settings 数据库、或者把别家的桌面组件 disable 掉。这些办法在 Android 11 上基本都失效了,原因是系统做了两件比较大的改动:第一,默认应用从“偏好列表”迁移到了“角色(Role)机制”。Android 10 引入了 RoleManager,把桌面、拨号、短信、浏览器这些默认应用统一定义成角色。HOME 这个角色是单例的,同一个时刻只能有一个包持有。系统解析 HOME 意图时优先看角色持有者,旧的那套 preferred activity 偏好列表变成兜底甚至被绕过。第二,隐藏 API 限制更严格了。Android 9 开始做非 SDK 接口限制,到了 11 上,addPreferredActivity 这类老方法要么被 @UnsupportedAppUsage 标记,要么在运行时被 hidden API 策略挡住。系统应用虽然签名等级高,但也不是所有隐藏 API 都能畅通无阻。所以结论很简单:Android 11 上设置默认 Launcher 的正规路径就是操作 HOME 角色。下面的内容全部围绕这个核心展开。2. 官方路线:用 RoleManager 操作 HOME 角色2.1 Role 机制到底改了什么东西简单理解 Role 机制:系统把“哪个 App 是默认桌面”这个状态从散落的 intent-filter 优先级、preferred activity 列表里抽出来,统一放到一个叫 RoleManager 的服务里管理。每个角色有明确的持有者列表,HOME 角色是单例角色,所以系统保证任意时刻最多只有一个桌面处于“默认”状态。这套机制带来的直接变化是:你不再需要去“告诉 PackageManager 谁的优先级最高”,只需要把自己的 Launcher 包名变成 HOME 角色的持有者。系统在后续解析 HOME 意图时会自动问 RoleManager 要答案,用户手动在设置里修改默认桌面,本质上也走的是同一套角色读写流程。还有一个关键细节:向一个单例角色添加新持有者时,系统会自动把旧的持有者移除。也就是说如果你已经通过设置把别的桌面设成了默认,调用 addRoleHolderAsUser 会把老的踢掉,不用自己手动清理。2.2 系统应用静默设置完整代码假设你的定制 Launcher 包名是 com.example.mylauncher,主 Activity 是 MainHomeActivity,你需要在一个有平台签名的系统应用里执行下面的代码。先看 Manifest 声明:<uses-permission android:name="android.permission.MANAGE_ROLE_HOLDERS" />注意,这个权限是 signature|privileged 级别,仅仅在 AndroidManifest 里写一行不够。如果你把应用放在 /system/priv-app 下,还要在对应权限白名单文件里登记,否则运行时会被 SecurityException 打回来:<permissions> <privapp-permissions package="com.example.rolesetter"> <permission name="android.permission.MANAGE_ROLE_HOLDERS" /> </privapp-permissions> </permissions>然后是核心代码:public final class LauncherRoleSetter { private static final String TAG = "LauncherRoleSetter"; private static final ComponentName MY_LAUNCHER = new ComponentName("com.example.mylauncher", "com.example.mylauncher.MainHomeActivity"); public static void setHomeRole(Context context) { RoleManager roleManager = context.getSystemService(RoleManager.class); if (roleManager == null) { Log.w(TAG, "RoleManager not available"); return; } // 如果自己已经是 HOME 角色持有者,直接返回,避免重复添加 if (roleManager.getRoleHolders(RoleManager.ROLE_HOME) .contains(MY_LAUNCHER.getPackageName())) { Log.i(TAG, "already home role holder"); return; } Executor executor = command -> new Handler(Looper.getMainLooper()).post(command); roleManager.addRoleHolderAsUser( RoleManager.ROLE_HOME, MY_LAUNCHER, 0, // flags,一般传 0 即可 executor, new RoleManager.RoleHolderCallback() { @Override public void onRoleHolderAdded(String roleName, ComponentName activity) { // 走到这里才说明角色真正写入成功 Log.i(TAG, "role added: " + activity.flattenToShortString()); } @Override public void onRoleHolderRemoved(String roleName, ComponentName activity) { // 可能是旧桌面被顶掉,也可能是自己被动过,注意区分包名 Log.i(TAG, "role removed: " + activity.flattenToShortString()); } }, Process.myUserHandle()); } }这里有几个细节值得展开。第一个是addRoleHolderAsUser 是异步的,调用后系统会和角色控制器做一次往返交互,成功后才回调 onRoleHolderAdded。不要以为方法不报错就是成功了,实际项目里出现过方法返回后用户按 HOME 键还是弹选择框的情况,原因就是角色还没写进持久化文件。第二个是ComponentName 里的主 Activity 必须真实存在,并且要带 ACTION_MAIN + CATEGORY_HOME 过滤条件,还要是 android:exported="true"。否则系统在检验候选组件时会直接抛 SecurityException,连回调都走不到。2.3 普通应用和企业场景的替代路径如果你不是系统应用,别硬试 addRoleHolderAsUser,那个 API 会直接 SecurityException。普通三方应用想成为默认桌面,官方只留了两条路。一条是发角色请求 Intent,系统会弹出一个让用户确认的界面:RoleManager roleManager = context.getSystemService(RoleManager.class); Intent requestIntent = roleManager.createRequestRoleHolderIntent( RoleManager.ROLE_HOME, executor, callback); context.startActivity(requestIntent);另一条是跳转到系统设置里的默认应用页:Intent settingsIntent = new Intent(Settings.ACTION_HOME_SETTINGS); context.startActivity(settingsIntent);这两条路都只能做到“引导”,最终需要用户手点确认,这是 Android 11 的底线,绕不过去。MDM 企业应用则不一样,设备所有者身份下可以用 DevicePolicyManager 的持久化偏好接口,下一节我会专门讲。3. 老办法还能不能救:preferred activity 与 DPM 的边界3.1 addPreferredActivity 为什么在 11 上失效老代码里最常出现的是 PackageManager.addPreferredActivity(),很多早期 ROM 项目用它直接把自家桌面的优先级写死。到了 Android 11,这条路基本断了,原因有两层。第一层是 API 层面。addPreferredActivity 在 API 29 被标记废弃,同时被归入隐藏 API。普通应用编译期看不到,运行时走反射会被 hidden API 策略拦截。系统应用虽然可以从源码编译进去,但是方法的内部实现已经变了:它写入的 legacy preferred activity 列表,在 Android 11 解析 HOME 意图时优先级排在角色持有者之后。也就是说,哪怕你把老桌面写进了偏好列表,只要 RoleManager 里已经有一个 HOME 角色持有者,系统就根本不会看你的偏好。第二层是维护层面。Android 11 的角色管理和旧偏好列表是两套并行的状态,老方法写的东西在版本升级、用户手动切换之后容易出现“看起来设了但实际没生效”的诡异状态。调试成本远高于直接走角色。3.2 Device Owner 的 addPersistentPreferredActivityDevicePolicyManager 的 addPersistentPreferredActivity 是一个官方 API,做企业管控的朋友应该很熟。它的作用是把某个 IntentFilter 对应的默认 Activity 持久化写入系统偏好,设备所有者身份下可用。我的建议是:如果项目已经是 MDM 形态,Android 11 上优先用它兜底,同时对 HOME 这种角色管理的场景仍然要理解它的边界。DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService( Context.DEVICE_POLICY_SERVICE); ComponentName admin = new ComponentName(context, MyAdminReceiver.class); if (!dpm.isDeviceOwnerApp(context.getPackageName())) { Log.e(TAG, "not device owner, cannot add persistent preferred activity"); return; } IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN); filter.addCategory(Intent.CATEGORY_HOME); filter.addCategory(Intent.CATEGORY_DEFAULT); ComponentName launcher = new ComponentName( "com.example.mylauncher", "com.example.mylauncher.MainHomeActivity"); dpm.addPersistentPreferredActivity(admin, filter, launcher);要让一个应用成为设备所有者,常用的调试手段是 adb 命令:adb shell dpm set-device-owner com.example.admin/.MyAdminReceiver注意这一步有前提:设备上不能已有其他 device owner,通常也不能存在已登录的账号。测试机上跑没问题,但如果你在一台已经日常使用的设备上试,会被系统直接拒绝。这条路的实际边界在于:persistent preferred activity 在 Android 11 上并不是 HOME 解析的最高优先级。一旦用户在系统设置里手动改过默认桌面,角色系统会把用户的选择记成新的角色持有者,这时候 DPM 写的偏好就不一定能抢回默认。所以严肃的企业方案通常会两条腿走路:既有设备所有者权限,又会再去调 RoleManager 把 HOME 角色拿回来。3.3 “禁用其他桌面”的野路子还有一个经常出现在各种论坛里的办法:把除目标桌面外的所有 Launcher 组件全部 disable 掉,这样系统解析 HOME 意图时只剩一个候选,自然就落到目标桌面头上。实现思路大致是这样:PackageManager pm = context.getPackageManager(); Intent homeIntent = new Intent(Intent.ACTION_MAIN); homeIntent.addCategory(Intent.CATEGORY_HOME); List<ResolveInfo> infos = pm.queryIntentActivities( homeIntent, PackageManager.MATCH_ALL); for (ResolveInfo info : infos) { String pkg = info.activityInfo.packageName; if (pkg.equals("com.example.mylauncher")) { continue; } pm.setComponentEnabledSetting( new ComponentName(pkg, info.activityInfo.name), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); }这个方法对权限的要求是 CHANGE_COMPONENT_ENABLED_STATE,同样需要 system/privileged 级别。它的问题也很明显:这不是“设置默认”,而是“消灭候选”。一旦用户之后安装了新的桌面,系统还是会弹选择框;而且禁用别人的桌面组件属于非常粗暴的操作,很容易被安全审核类工具标记。另外在 Android 11 上,跨包查询 Launcher 还受包可见性限制,调用方要在 Manifest 里加上 queries 声明,否则 query 结果会是空的。我见过不少项目在这上面折过:<queries> <intent> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.HOME" /> </intent> </queries>把这几种老办法放在一起看,结论很清晰:在 Android 11 上最省心、最不容易出诡异问题的就是 RoleManager 角色方案。老方法可以应急,但不值得花时间维护。4. HOME 键按下之后:系统解析链路的源码视角4.1 从按键到解析器的调用链想真正理解为什么“操作角色”就能改默认,最好把 HOME 键按下之后系统干的活走一遍,这里我按 Android 11 的源码结构做个简化描述。用户按 HOME 键,输入事件经过窗口管理到 ActivityTaskManagerService,系统随后构造一个标准的 HOME 意图:action 是 ACTION_MAIN,category 里包含 CATEGORY_HOME,然后交给 PackageManagerService 的 resolveActivity 去解析。这个解析过程的核心问题是:在所有声明了 MAIN + HOME 过滤条件的组件里,系统究竟选谁。4.2 角色、偏好列表与选择器的优先级Android 11 的解析顺序大致是这样的:第一步,系统咨询 RoleManager,查询 HOME 角色当前持有者是谁。如果持有者存在且其主 Activity 处于可用状态,直接返回这个组件。第二步,如果角色为空,再去看 persistent preferred activity(DPM 写的那种)和 legacy preferred activity。第三步,如果前两步都没有结果,才会把所有候选组件做成选择器弹给用户。这个顺序解释了文章前面提到的所有现象:RoleManager 是最高优先级,DPM 偏好是第二优先级,addPreferredActivity 老方法在第三优先级甚至更低。所以你在 11 上把老偏好写满也没用,系统根本走不到那一步。“设置默认 Launcher”在现代 Android 里的本质,就是保证自家桌面出现在第一步的答案里。另外值得注意:角色状态的持久化是在系统服务里完成的,不是应用进程写一个 SharedPreferences 那么简单。系统在写入角色之前还会校验目标组件是否满足该角色的应用集条件,这也是为什么目标 Activity 必须带正确的 MAIN/HOME intent-filter 并且 exported 的原因。4.3 用 dumpsys 观察真实状态调试时最有用的命令是 dumpsys role,它能把当前系统里各个角色的持有者列出来:adb shell dumpsys role | grep -B 2 -A 4 "android.app.role.HOME"正常输出里你会看到 HOME 角色下面挂着一个包名。如果这个包名是你要的,说明角色设置成功;如果空着,说明角色还没写进去,继续查代码和权限。想快速用命令行验证某个包能否成为 HOME 角色持有者,可以试试:adb shell cmd role get-role-holders android.app.role.HOME具体子命令的完整语法可以用adb shell cmd role help查看。这一类命令在开发阶段非常有用,不用反复杀进程看日志就能确认系统内部状态。5. 实测记录:从 9.0 老方案迁移到 11.0 的完整过程5.1 环境与前置准备我近期在一个基于 Android 11.0 源码的定制项目里做过一次默认桌面切换,目标是把预置的轻量桌面从“老二”扶正成唯一默认。项目背景很简单:系统里有两个桌面,原生的那个桌面不符合产品定位,但没法直接从文件系统里删掉,只能靠设置默认来压制。我的做法是把负责设置的组件做成系统应用,平台签名,放在 priv-app 目录。目标 Launcher 同样预置在 priv-app 下。编译阶段就要确认 Manifest 里带上了 MANAGE_ROLE_HOLDERS,同时把 privapp-permissions 白名单写好,这一步漏了的话,运行时会直接 SecurityException。5.2 第一版失败:反射 addPreferredActivity 被挡最初我是按照 Android 9 时代的老经验写的:用反射调用 PackageManager.addPreferredActivity,把目标桌面写进偏好列表。编译没问题,运行到反射那一步也没立即报错,但随后验证发现 HOME 键还是弹选择器,说明写进去的东西根本没被解析流程采纳。之后我又尝试绕过隐藏 API 限制去调 SystemServiceRegistry 里的实例,结果发现 addPreferredActivity 在 Android 11 上不仅被废弃,内部实现也已经不再影响 HOME 角色的解析。与其跟系统框架的设计对着干,不如直接换到官方角色 API。这一步算是这次迁移里最值得记住的教训:老经验越深厚,越容易在版本升级时踩坑,一定要先确认目标版本的解析策略再动手。5.3 第二版成功:MANAGE_ROLE_HOLDERS + 白名单切换到 RoleManager 方案后,我把设置代码封装成独立模块,在 BOOT_COMPLETED 广播里触发。第一次验证就成功了,dumpsys role 里能查到目标包名,按 HOME 键也不再弹选择框。这中间还踩了几个小坑:第一个是回调顺序的错觉。addRoleHolderAsUser 执行时,onRoleHolderRemoved 可能先被回调,代表旧桌面被系统自动移除。如果你在回调里做条件判断不够严谨,很容易把“旧持有者被移除”误判成“设置失败”,然后重复执行添加逻辑。第二个是目标 Activity 的属性。我的 Launcher 主 Activity 一开始没有显式写 android:exported="true",在 Android 11 上虽然带 intent-filter 的系统应用不一定会被强制要求,但角色校验阶段偶尔会出现组件不可见的异常。把 exported 显式声明为 true 之后彻底稳定了。第三个是异步时序。调用返回不代表角色已落盘,系统的角色写入是异步的。我的第一版验证代码在 addRoleHolderAsUser 返回后立刻模拟 HOME 键,结果偶尔能弹选择框,误导我调了好一阵子。之后改成在 onRoleHolderAdded 回调里做验证,问题消失。5.4 多用户与开机时序的两个坑这个项目最终上线前还暴露了两个比较深的问题,我觉得值得单独写出来。一是多用户。角色状态是按用户空间隔离的,我只给主用户设置了 HOME 角色,结果切换到访客用户时,访客那边按 HOME 键照样弹选择器。解决方法是枚举所有用户,逐个设置:UserManager userManager = context.getSystemService(UserManager.class); List<UserInfo> users = userManager.getUsers(); for (UserInfo user : users) { if (!user.isEnabled()) { continue; } roleManager.addRoleHolderAsUser( RoleManager.ROLE_HOME, MY_LAUNCHER, 0, executor, callback, user.getUserHandle()); }二是开机时序。BOOT_COMPLETED 广播发送的时机晚于桌面启动。如果你做的是“第一次开机就要正确落在目标桌面”的 ROM,光靠 BOOT_COMPLETED 触发设置不够,系统可能在设置发生之前就已经把选择器弹给用户了。这个需求需要在更早的阶段下手,比如集成到 SetupWizard 流程,或者由系统服务在启动早期统一处理角色分配。普通“开机后切换默认桌面”的需求则不必过度设计,BOOT_COMPLETED 足够。6. 收尾清单:验证方法、回滚方案与最后提醒6.1 验证是否真的生效我习惯在设置完成后做三层验证。第一层看日志:角色设置组件打印 onRoleHolderAdded 成功日志。第二层查系统状态:adb shell dumpsys role | grep -B 2 -A 4 "android.app.role.HOME" adb shell input keyevent KEYCODE_HOME第三层用 am 命令直接发起 HOME 意图,看是否进入目标桌面且无选择器:adb shell am start -a android.intent.action.MAIN -c android.intent.category.HOME这三层都通过,基本可以认为默认桌面设置真正生效了。注意第二层的按键验证要在 onRoleHolderAdded 回调之后再做,不然容易误判。6.2 回滚与重置开发阶段回滚角色同样简单,代码里可以调用:roleManager.removeRoleHolderAsUser( RoleManager.ROLE_HOME, MY_LAUNCHER, 0, executor, callback, Process.myUserHandle());命令行调试时也可以直接操作:adb shell cmd role remove-role-holder android.app.role.HOME com.example.mylauncher如果用 DPM 方案,对应清理接口是 clearPackagePersistentPreferredActivities。开发过程中最好把这些命令记在项目 Wiki 里,方便团队反复测试。6.3 我最后想提醒的几件事先说产品层面。静默修改用户默认桌面属于强干预行为,如果设备是用户自有的,最好在设置里留一个恢复入口,或者在引导流程中明确告知。企业设备的刚性管控场景另说,普通消费设备建议谨慎。再说实现层面。如果设置组件和目标 Launcher 在同一个 APK 里,BOOT_COMPLETED 每次开机都会触发,记得用一个持久化标记判断是否已经设置过,避免反复调用角色写操作。系统角色写入虽然不重,但没必要每次开机都做一遍。最后补充一个容易忽略的点:给其他用户设置角色时,要确保调用应用有足够的跨用户权限,否则会收到 SecurityException。我实测的平台签名应用没问题,但如果你在熟悉的 ROM 裁剪环境里做,权限裁剪可能导致异常,遇到时优先检查这个方向。这套流程跑通之后,我最大的感受是:Android 11 的默认桌面管理其实比老版本清晰很多。角色机制把分散的偏好逻辑收敛到了一处,只要顺着官方 API 走,代码量不大,行为也可预期。遇到问题先从解析链路入手排查,比盲目试老代码效率高得多。
1. 先想清楚:你要哪种“设置默认”,这决定了 API 路线做 Android 系统定制和应用开发的朋友,应该都遇到过这个需求:设备上装了好几个桌面(Launcher),用户一按 HOME 键就弹出一个“选择要使用的桌面”列表,领导要求开机或者升级之后必须直接落在我们预置的那个桌面上。于是很多人会搜“Android 11.0 设置默认 Launcher 的方法”。这个标题看起来是个一句话需求,实际做起来最大的坑在于:同一句话背后有好几条完全不同的技术路线。你是系统应用、MDM 企业管控应用,还是普通三方应用,能用的 API、权限要求、用户感知完全不一样。不看场景直接抄代码,十有八九会翻车。1.1 三种典型场景与权限前提我把实际项目里最常见的三种诉求整理成了下面这张表,建议你先对号入座:场景开发者身份需要的前提条件用户是否感知定制 ROM 预置桌面,开机就是自家 Launcher系统应用(平台签名)MANAGE_ROLE_HOLDERS 签名权限 + privapp 白名单完全静默企业设备强制绑定某桌面,防止员工切换设备所有者(Device Owner)应用DevicePolicyManager 设备所有者身份完全静默普通三方 App 引导用户把自家设为桌面普通应用无特殊权限,只能弹系统确认框用户手动确认第一种是我见过的绝大多数定制 ROM 项目的诉求,也是这篇文章的绝对主角。第二种多见于 MDM 方案,它的实现思路和第一种不完全一样,但有不少人会混淆,后面我会单开一节讲清楚。第三种其实不算“设置”,而是“引导用户去设置”,因为 Android 从 10 开始把默认桌面这类系统角色的授予权收得非常紧,普通应用没有能力静默修改。1.2 为什么 Android 11 上“万能代码”基本绝迹如果你是从 Android 7、8、9 一路做过来的,应该记得以前有一些“野路子”能绕过去:反射调用 PackageManager 的 addPreferredActivity、直接改 Settings 数据库、或者把别家的桌面组件 disable 掉。这些办法在 Android 11 上基本都失效了,原因是系统做了两件比较大的改动:第一,默认应用从“偏好列表”迁移到了“角色(Role)机制”。Android 10 引入了 RoleManager,把桌面、拨号、短信、浏览器这些默认应用统一定义成角色。HOME 这个角色是单例的,同一个时刻只能有一个包持有。系统解析 HOME 意图时优先看角色持有者,旧的那套 preferred activity 偏好列表变成兜底甚至被绕过。第二,隐藏 API 限制更严格了。Android 9 开始做非 SDK 接口限制,到了 11 上,addPreferredActivity 这类老方法要么被 @UnsupportedAppUsage 标记,要么在运行时被 hidden API 策略挡住。系统应用虽然签名等级高,但也不是所有隐藏 API 都能畅通无阻。所以结论很简单:Android 11 上设置默认 Launcher 的正规路径就是操作 HOME 角色。下面的内容全部围绕这个核心展开。2. 官方路线:用 RoleManager 操作 HOME 角色2.1 Role 机制到底改了什么东西简单理解 Role 机制:系统把“哪个 App 是默认桌面”这个状态从散落的 intent-filter 优先级、preferred activity 列表里抽出来,统一放到一个叫 RoleManager 的服务里管理。每个角色有明确的持有者列表,HOME 角色是单例角色,所以系统保证任意时刻最多只有一个桌面处于“默认”状态。这套机制带来的直接变化是:你不再需要去“告诉 PackageManager 谁的优先级最高”,只需要把自己的 Launcher 包名变成 HOME 角色的持有者。系统在后续解析 HOME 意图时会自动问 RoleManager 要答案,用户手动在设置里修改默认桌面,本质上也走的是同一套角色读写流程。还有一个关键细节:向一个单例角色添加新持有者时,系统会自动把旧的持有者移除。也就是说如果你已经通过设置把别的桌面设成了默认,调用 addRoleHolderAsUser 会把老的踢掉,不用自己手动清理。2.2 系统应用静默设置完整代码假设你的定制 Launcher 包名是 com.example.mylauncher,主 Activity 是 MainHomeActivity,你需要在一个有平台签名的系统应用里执行下面的代码。先看 Manifest 声明:<uses-permission android:name="android.permission.MANAGE_ROLE_HOLDERS" />注意,这个权限是 signature|privileged 级别,仅仅在 AndroidManifest 里写一行不够。如果你把应用放在 /system/priv-app 下,还要在对应权限白名单文件里登记,否则运行时会被 SecurityException 打回来:<permissions> <privapp-permissions package="com.example.rolesetter"> <permission name="android.permission.MANAGE_ROLE_HOLDERS" /> </privapp-permissions> </permissions>然后是核心代码:public final class LauncherRoleSetter { private static final String TAG = "LauncherRoleSetter"; private static final ComponentName MY_LAUNCHER = new ComponentName("com.example.mylauncher", "com.example.mylauncher.MainHomeActivity"); public static void setHomeRole(Context context) { RoleManager roleManager = context.getSystemService(RoleManager.class); if (roleManager == null) { Log.w(TAG, "RoleManager not available"); return; } // 如果自己已经是 HOME 角色持有者,直接返回,避免重复添加 if (roleManager.getRoleHolders(RoleManager.ROLE_HOME) .contains(MY_LAUNCHER.getPackageName())) { Log.i(TAG, "already home role holder"); return; } Executor executor = command -> new Handler(Looper.getMainLooper()).post(command); roleManager.addRoleHolderAsUser( RoleManager.ROLE_HOME, MY_LAUNCHER, 0, // flags,一般传 0 即可 executor, new RoleManager.RoleHolderCallback() { @Override public void onRoleHolderAdded(String roleName, ComponentName activity) { // 走到这里才说明角色真正写入成功 Log.i(TAG, "role added: " + activity.flattenToShortString()); } @Override public void onRoleHolderRemoved(String roleName, ComponentName activity) { // 可能是旧桌面被顶掉,也可能是自己被动过,注意区分包名 Log.i(TAG, "role removed: " + activity.flattenToShortString()); } }, Process.myUserHandle()); } }这里有几个细节值得展开。第一个是addRoleHolderAsUser 是异步的,调用后系统会和角色控制器做一次往返交互,成功后才回调 onRoleHolderAdded。不要以为方法不报错就是成功了,实际项目里出现过方法返回后用户按 HOME 键还是弹选择框的情况,原因就是角色还没写进持久化文件。第二个是ComponentName 里的主 Activity 必须真实存在,并且要带 ACTION_MAIN + CATEGORY_HOME 过滤条件,还要是 android:exported="true"。否则系统在检验候选组件时会直接抛 SecurityException,连回调都走不到。2.3 普通应用和企业场景的替代路径如果你不是系统应用,别硬试 addRoleHolderAsUser,那个 API 会直接 SecurityException。普通三方应用想成为默认桌面,官方只留了两条路。一条是发角色请求 Intent,系统会弹出一个让用户确认的界面:RoleManager roleManager = context.getSystemService(RoleManager.class); Intent requestIntent = roleManager.createRequestRoleHolderIntent( RoleManager.ROLE_HOME, executor, callback); context.startActivity(requestIntent);另一条是跳转到系统设置里的默认应用页:Intent settingsIntent = new Intent(Settings.ACTION_HOME_SETTINGS); context.startActivity(settingsIntent);这两条路都只能做到“引导”,最终需要用户手点确认,这是 Android 11 的底线,绕不过去。MDM 企业应用则不一样,设备所有者身份下可以用 DevicePolicyManager 的持久化偏好接口,下一节我会专门讲。3. 老办法还能不能救:preferred activity 与 DPM 的边界3.1 addPreferredActivity 为什么在 11 上失效老代码里最常出现的是 PackageManager.addPreferredActivity(),很多早期 ROM 项目用它直接把自家桌面的优先级写死。到了 Android 11,这条路基本断了,原因有两层。第一层是 API 层面。addPreferredActivity 在 API 29 被标记废弃,同时被归入隐藏 API。普通应用编译期看不到,运行时走反射会被 hidden API 策略拦截。系统应用虽然可以从源码编译进去,但是方法的内部实现已经变了:它写入的 legacy preferred activity 列表,在 Android 11 解析 HOME 意图时优先级排在角色持有者之后。也就是说,哪怕你把老桌面写进了偏好列表,只要 RoleManager 里已经有一个 HOME 角色持有者,系统就根本不会看你的偏好。第二层是维护层面。Android 11 的角色管理和旧偏好列表是两套并行的状态,老方法写的东西在版本升级、用户手动切换之后容易出现“看起来设了但实际没生效”的诡异状态。调试成本远高于直接走角色。3.2 Device Owner 的 addPersistentPreferredActivityDevicePolicyManager 的 addPersistentPreferredActivity 是一个官方 API,做企业管控的朋友应该很熟。它的作用是把某个 IntentFilter 对应的默认 Activity 持久化写入系统偏好,设备所有者身份下可用。我的建议是:如果项目已经是 MDM 形态,Android 11 上优先用它兜底,同时对 HOME 这种角色管理的场景仍然要理解它的边界。DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService( Context.DEVICE_POLICY_SERVICE); ComponentName admin = new ComponentName(context, MyAdminReceiver.class); if (!dpm.isDeviceOwnerApp(context.getPackageName())) { Log.e(TAG, "not device owner, cannot add persistent preferred activity"); return; } IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN); filter.addCategory(Intent.CATEGORY_HOME); filter.addCategory(Intent.CATEGORY_DEFAULT); ComponentName launcher = new ComponentName( "com.example.mylauncher", "com.example.mylauncher.MainHomeActivity"); dpm.addPersistentPreferredActivity(admin, filter, launcher);要让一个应用成为设备所有者,常用的调试手段是 adb 命令:adb shell dpm set-device-owner com.example.admin/.MyAdminReceiver注意这一步有前提:设备上不能已有其他 device owner,通常也不能存在已登录的账号。测试机上跑没问题,但如果你在一台已经日常使用的设备上试,会被系统直接拒绝。这条路的实际边界在于:persistent preferred activity 在 Android 11 上并不是 HOME 解析的最高优先级。一旦用户在系统设置里手动改过默认桌面,角色系统会把用户的选择记成新的角色持有者,这时候 DPM 写的偏好就不一定能抢回默认。所以严肃的企业方案通常会两条腿走路:既有设备所有者权限,又会再去调 RoleManager 把 HOME 角色拿回来。3.3 “禁用其他桌面”的野路子还有一个经常出现在各种论坛里的办法:把除目标桌面外的所有 Launcher 组件全部 disable 掉,这样系统解析 HOME 意图时只剩一个候选,自然就落到目标桌面头上。实现思路大致是这样:PackageManager pm = context.getPackageManager(); Intent homeIntent = new Intent(Intent.ACTION_MAIN); homeIntent.addCategory(Intent.CATEGORY_HOME); List<ResolveInfo> infos = pm.queryIntentActivities( homeIntent, PackageManager.MATCH_ALL); for (ResolveInfo info : infos) { String pkg = info.activityInfo.packageName; if (pkg.equals("com.example.mylauncher")) { continue; } pm.setComponentEnabledSetting( new ComponentName(pkg, info.activityInfo.name), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); }这个方法对权限的要求是 CHANGE_COMPONENT_ENABLED_STATE,同样需要 system/privileged 级别。它的问题也很明显:这不是“设置默认”,而是“消灭候选”。一旦用户之后安装了新的桌面,系统还是会弹选择框;而且禁用别人的桌面组件属于非常粗暴的操作,很容易被安全审核类工具标记。另外在 Android 11 上,跨包查询 Launcher 还受包可见性限制,调用方要在 Manifest 里加上 queries 声明,否则 query 结果会是空的。我见过不少项目在这上面折过:<queries> <intent> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.HOME" /> </intent> </queries>把这几种老办法放在一起看,结论很清晰:在 Android 11 上最省心、最不容易出诡异问题的就是 RoleManager 角色方案。老方法可以应急,但不值得花时间维护。4. HOME 键按下之后:系统解析链路的源码视角4.1 从按键到解析器的调用链想真正理解为什么“操作角色”就能改默认,最好把 HOME 键按下之后系统干的活走一遍,这里我按 Android 11 的源码结构做个简化描述。用户按 HOME 键,输入事件经过窗口管理到 ActivityTaskManagerService,系统随后构造一个标准的 HOME 意图:action 是 ACTION_MAIN,category 里包含 CATEGORY_HOME,然后交给 PackageManagerService 的 resolveActivity 去解析。这个解析过程的核心问题是:在所有声明了 MAIN + HOME 过滤条件的组件里,系统究竟选谁。4.2 角色、偏好列表与选择器的优先级Android 11 的解析顺序大致是这样的:第一步,系统咨询 RoleManager,查询 HOME 角色当前持有者是谁。如果持有者存在且其主 Activity 处于可用状态,直接返回这个组件。第二步,如果角色为空,再去看 persistent preferred activity(DPM 写的那种)和 legacy preferred activity。第三步,如果前两步都没有结果,才会把所有候选组件做成选择器弹给用户。这个顺序解释了文章前面提到的所有现象:RoleManager 是最高优先级,DPM 偏好是第二优先级,addPreferredActivity 老方法在第三优先级甚至更低。所以你在 11 上把老偏好写满也没用,系统根本走不到那一步。“设置默认 Launcher”在现代 Android 里的本质,就是保证自家桌面出现在第一步的答案里。另外值得注意:角色状态的持久化是在系统服务里完成的,不是应用进程写一个 SharedPreferences 那么简单。系统在写入角色之前还会校验目标组件是否满足该角色的应用集条件,这也是为什么目标 Activity 必须带正确的 MAIN/HOME intent-filter 并且 exported 的原因。4.3 用 dumpsys 观察真实状态调试时最有用的命令是 dumpsys role,它能把当前系统里各个角色的持有者列出来:adb shell dumpsys role | grep -B 2 -A 4 "android.app.role.HOME"正常输出里你会看到 HOME 角色下面挂着一个包名。如果这个包名是你要的,说明角色设置成功;如果空着,说明角色还没写进去,继续查代码和权限。想快速用命令行验证某个包能否成为 HOME 角色持有者,可以试试:adb shell cmd role get-role-holders android.app.role.HOME具体子命令的完整语法可以用adb shell cmd role help查看。这一类命令在开发阶段非常有用,不用反复杀进程看日志就能确认系统内部状态。5. 实测记录:从 9.0 老方案迁移到 11.0 的完整过程5.1 环境与前置准备我近期在一个基于 Android 11.0 源码的定制项目里做过一次默认桌面切换,目标是把预置的轻量桌面从“老二”扶正成唯一默认。项目背景很简单:系统里有两个桌面,原生的那个桌面不符合产品定位,但没法直接从文件系统里删掉,只能靠设置默认来压制。我的做法是把负责设置的组件做成系统应用,平台签名,放在 priv-app 目录。目标 Launcher 同样预置在 priv-app 下。编译阶段就要确认 Manifest 里带上了 MANAGE_ROLE_HOLDERS,同时把 privapp-permissions 白名单写好,这一步漏了的话,运行时会直接 SecurityException。5.2 第一版失败:反射 addPreferredActivity 被挡最初我是按照 Android 9 时代的老经验写的:用反射调用 PackageManager.addPreferredActivity,把目标桌面写进偏好列表。编译没问题,运行到反射那一步也没立即报错,但随后验证发现 HOME 键还是弹选择器,说明写进去的东西根本没被解析流程采纳。之后我又尝试绕过隐藏 API 限制去调 SystemServiceRegistry 里的实例,结果发现 addPreferredActivity 在 Android 11 上不仅被废弃,内部实现也已经不再影响 HOME 角色的解析。与其跟系统框架的设计对着干,不如直接换到官方角色 API。这一步算是这次迁移里最值得记住的教训:老经验越深厚,越容易在版本升级时踩坑,一定要先确认目标版本的解析策略再动手。5.3 第二版成功:MANAGE_ROLE_HOLDERS + 白名单切换到 RoleManager 方案后,我把设置代码封装成独立模块,在 BOOT_COMPLETED 广播里触发。第一次验证就成功了,dumpsys role 里能查到目标包名,按 HOME 键也不再弹选择框。这中间还踩了几个小坑:第一个是回调顺序的错觉。addRoleHolderAsUser 执行时,onRoleHolderRemoved 可能先被回调,代表旧桌面被系统自动移除。如果你在回调里做条件判断不够严谨,很容易把“旧持有者被移除”误判成“设置失败”,然后重复执行添加逻辑。第二个是目标 Activity 的属性。我的 Launcher 主 Activity 一开始没有显式写 android:exported="true",在 Android 11 上虽然带 intent-filter 的系统应用不一定会被强制要求,但角色校验阶段偶尔会出现组件不可见的异常。把 exported 显式声明为 true 之后彻底稳定了。第三个是异步时序。调用返回不代表角色已落盘,系统的角色写入是异步的。我的第一版验证代码在 addRoleHolderAsUser 返回后立刻模拟 HOME 键,结果偶尔能弹选择框,误导我调了好一阵子。之后改成在 onRoleHolderAdded 回调里做验证,问题消失。5.4 多用户与开机时序的两个坑这个项目最终上线前还暴露了两个比较深的问题,我觉得值得单独写出来。一是多用户。角色状态是按用户空间隔离的,我只给主用户设置了 HOME 角色,结果切换到访客用户时,访客那边按 HOME 键照样弹选择器。解决方法是枚举所有用户,逐个设置:UserManager userManager = context.getSystemService(UserManager.class); List<UserInfo> users = userManager.getUsers(); for (UserInfo user : users) { if (!user.isEnabled()) { continue; } roleManager.addRoleHolderAsUser( RoleManager.ROLE_HOME, MY_LAUNCHER, 0, executor, callback, user.getUserHandle()); }二是开机时序。BOOT_COMPLETED 广播发送的时机晚于桌面启动。如果你做的是“第一次开机就要正确落在目标桌面”的 ROM,光靠 BOOT_COMPLETED 触发设置不够,系统可能在设置发生之前就已经把选择器弹给用户了。这个需求需要在更早的阶段下手,比如集成到 SetupWizard 流程,或者由系统服务在启动早期统一处理角色分配。普通“开机后切换默认桌面”的需求则不必过度设计,BOOT_COMPLETED 足够。6. 收尾清单:验证方法、回滚方案与最后提醒6.1 验证是否真的生效我习惯在设置完成后做三层验证。第一层看日志:角色设置组件打印 onRoleHolderAdded 成功日志。第二层查系统状态:adb shell dumpsys role | grep -B 2 -A 4 "android.app.role.HOME" adb shell input keyevent KEYCODE_HOME第三层用 am 命令直接发起 HOME 意图,看是否进入目标桌面且无选择器:adb shell am start -a android.intent.action.MAIN -c android.intent.category.HOME这三层都通过,基本可以认为默认桌面设置真正生效了。注意第二层的按键验证要在 onRoleHolderAdded 回调之后再做,不然容易误判。6.2 回滚与重置开发阶段回滚角色同样简单,代码里可以调用:roleManager.removeRoleHolderAsUser( RoleManager.ROLE_HOME, MY_LAUNCHER, 0, executor, callback, Process.myUserHandle());命令行调试时也可以直接操作:adb shell cmd role remove-role-holder android.app.role.HOME com.example.mylauncher如果用 DPM 方案,对应清理接口是 clearPackagePersistentPreferredActivities。开发过程中最好把这些命令记在项目 Wiki 里,方便团队反复测试。6.3 我最后想提醒的几件事先说产品层面。静默修改用户默认桌面属于强干预行为,如果设备是用户自有的,最好在设置里留一个恢复入口,或者在引导流程中明确告知。企业设备的刚性管控场景另说,普通消费设备建议谨慎。再说实现层面。如果设置组件和目标 Launcher 在同一个 APK 里,BOOT_COMPLETED 每次开机都会触发,记得用一个持久化标记判断是否已经设置过,避免反复调用角色写操作。系统角色写入虽然不重,但没必要每次开机都做一遍。最后补充一个容易忽略的点:给其他用户设置角色时,要确保调用应用有足够的跨用户权限,否则会收到 SecurityException。我实测的平台签名应用没问题,但如果你在熟悉的 ROM 裁剪环境里做,权限裁剪可能导致异常,遇到时优先检查这个方向。这套流程跑通之后,我最大的感受是:Android 11 的默认桌面管理其实比老版本清晰很多。角色机制把分散的偏好逻辑收敛到了一处,只要顺着官方 API 走,代码量不大,行为也可预期。遇到问题先从解析链路入手排查,比盲目试老代码效率高得多。