19、Launcher的权限管理:运行时权限请求、存储权限与桌面布局备份、权限对Widget的影响
权限管理,说实话,是Launcher开发里最容易踩坑的地方之一。我早期做Launcher时,总觉得权限嘛,清单文件里声明一下不就完了?结果用户一反馈,各种崩溃、Widget不显示、备份失败……嗯,后来我才真正重视起来。
这一章,我们就来聊聊Launcher里的权限那些事。你想想看,Launcher作为桌面,它要读写存储(备份布局)、要显示Widget(需要绑定权限)、还要处理Android 6.0之后的运行时权限模型。每一步,都藏着细节。
19.1 运行时权限模型:Launcher的特殊性
Android 6.0(API 23)引入了运行时权限。普通应用在需要权限时弹个对话框就行。但Launcher不一样——它是系统桌面,用户可能一开机就看到它。你总不能一启动就弹权限框吧?那体验太差了。
我个人习惯的做法是:延迟请求,按需触发。比如用户要备份桌面布局时,才去请求存储权限。而不是在Launcher启动时一股脑全要。
核心原则:Launcher的权限请求,必须与用户操作强关联。不要主动、不要提前、不要频繁。
来看一个典型的运行时权限请求代码。我建议你把它封装成一个工具类,方便复用:
public class PermissionHelper {
public static boolean checkAndRequestStoragePermission(Activity activity, int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (activity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
activity.requestPermissions(
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
requestCode);
return false;
}
}
return true;
}
public static boolean checkAndRequestBindWidgetPermission(Activity activity, int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (activity.checkSelfPermission(Manifest.permission.BIND_APPWIDGET)
!= PackageManager.PERMISSION_GRANTED) {
activity.requestPermissions(
new String[]{Manifest.permission.BIND_APPWIDGET},
requestCode);
return false;
}
}
return true;
}
}
注意,BIND_APPWIDGET 这个权限比较特殊。它是系统权限,普通应用拿不到。但Launcher作为系统桌面,通常会被预置为系统应用,或者在manifest里声明后由系统授予。我在项目中遇到过,第三方Launcher如果没有系统签名,直接请求这个权限会静默失败——用户根本看不到弹窗。
警告:BIND_APPWIDGET 权限只能由系统应用或具有系统签名的应用持有。如果你的Launcher不是系统级应用,Widget绑定功能会受限。
19.2 存储权限与桌面布局备份
桌面布局备份,说白了就是把用户摆放的图标、Widget位置、文件夹结构等数据,序列化后存到文件里。这样用户换手机或重置后,能一键恢复。
我做过一个备份功能,流程大概是这样的:
- 用户点击「备份布局」按钮
- 检查存储权限,如果没有则请求
- 权限授予后,将桌面数据序列化为JSON
- 写入到外部存储的指定目录(比如
LauncherBackup/) - 提示用户备份成功
这里有个坑:Android 10(API 29)开始,强制分区存储。你不能随便往外部存储写文件了。我建议使用 MediaStore 或者应用专属目录。但如果你要生成可移植的备份文件(用户能手动拷贝),那还是得用 MANAGE_EXTERNAL_STORAGE 权限。
小技巧:对于Android 10+,我习惯把备份文件写到 getExternalFilesDir(null) 目录下。这样不需要额外权限,而且用户卸载Launcher时文件会自动清理。缺点是用户不能直接通过文件管理器找到它。
来看一个备份JSON的示例结构:
{
"version": 1,
"backup_time": 1700000000000,
"workspace": {
"screen_count": 5,
"items": [
{
"type": "shortcut",
"screen": 0,
"x": 0,
"y": 0,
"intent": "component=com.example/.MainActivity",
"label": "我的应用"
},
{
"type": "widget",
"screen": 1,
"x": 2,
"y": 1,
"spanX": 4,
"spanY": 2,
"provider": "com.example.widget/.MyWidgetProvider"
}
]
},
"hotseat": {
"items": [
{
"type": "shortcut",
"position": 0,
"intent": "component=com.android.phone/.Dialer"
}
]
}
}
恢复时,解析这个JSON,然后调用Launcher的数据库插入接口。注意,Widget恢复时还需要重新绑定,这又涉及到权限问题。
19.3 权限对Widget的影响
Widget是Launcher的重要组成部分。但权限问题经常导致Widget无法添加或显示异常。我总结了几种常见情况:
| 场景 | 权限要求 | 典型表现 |
|---|---|---|
| 添加Widget | BIND_APPWIDGET | 点击添加无反应,或提示「无法添加小组件」 |
| Widget加载数据 | 网络/存储权限(取决于Widget自身) | Widget显示空白或加载失败 |
| Widget配置Activity | 无特殊权限,但需要Launcher能启动Activity | 配置页面无法弹出 |
| 备份恢复Widget | 存储权限 + BIND_APPWIDGET | 恢复后Widget不显示,或显示为默认状态 |
我曾经遇到一个案例:用户反馈说「我明明添加了天气Widget,但一直显示加载中」。排查后发现,天气Widget需要网络权限,但用户把网络权限关了。这不是Launcher的问题,但用户会认为是桌面不好用。所以,我建议在添加Widget时,可以提示用户检查相关权限。
经验之谈:Widget的权限问题,一半在Launcher侧(BIND_APPWIDGET),另一半在Widget应用侧。Launcher能做的是:确保自己有绑定权限,并且在添加失败时给出明确的错误提示。
19.4 知识体系总览
下面这张图,是我梳理的Launcher权限管理核心逻辑。你可以把它当作一个检查清单:
19.5 避坑指南与最佳实践
最后,分享几个我实际项目中踩过的坑:
- 不要假设权限已经授予。 用户可能在设置里手动关闭权限。每次操作前都检查一下。
- 权限被拒绝后,不要反复弹窗。 如果用户选择了「不再询问」,你应该引导用户去系统设置里手动开启。可以用
shouldShowRequestPermissionRationale()判断。 - 备份文件要考虑兼容性。 我遇到过备份文件版本升级后,旧文件无法恢复的情况。建议在JSON里加一个
version字段,做向前兼容。 - Widget恢复时,先检查Widget是否还存在。 用户可能已经卸载了提供Widget的应用。如果直接绑定,会抛异常。
我曾经犯过一个低级错误:备份时把Widget的provider组件名写死了。结果应用更新后包名变了,恢复时全部失败。后来我改成动态获取,每次备份都从AppWidgetManager读取最新的provider信息。
嗯,权限管理这块,说难不难,说简单也不简单。关键是养成好习惯:先检查,再操作;失败了,给提示。做到这三点,Launcher的权限部分基本稳了。
公众号:蓝海资料掘金营,微信deep3321