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位置、文件夹结构等数据,序列化后存到文件里。这样用户换手机或重置后,能一键恢复。

我做过一个备份功能,流程大概是这样的:

  1. 用户点击「备份布局」按钮
  2. 检查存储权限,如果没有则请求
  3. 权限授予后,将桌面数据序列化为JSON
  4. 写入到外部存储的指定目录(比如 LauncherBackup/
  5. 提示用户备份成功

这里有个坑: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权限管理核心逻辑。你可以把它当作一个检查清单:

Launcher权限管理核心逻辑 Launcher权限管理 运行时权限请求 存储权限与备份 Widget权限影响 延迟请求,按需触发 与用户操作强关联 序列化为JSON存储 Android 10+分区存储适配 BIND_APPWIDGET系统权限 添加/恢复时重新绑定 权限问题 → 明确提示 → 引导用户操作

19.5 避坑指南与最佳实践

最后,分享几个我实际项目中踩过的坑:

  • 不要假设权限已经授予。 用户可能在设置里手动关闭权限。每次操作前都检查一下。
  • 权限被拒绝后,不要反复弹窗。 如果用户选择了「不再询问」,你应该引导用户去系统设置里手动开启。可以用 shouldShowRequestPermissionRationale() 判断。
  • 备份文件要考虑兼容性。 我遇到过备份文件版本升级后,旧文件无法恢复的情况。建议在JSON里加一个 version 字段,做向前兼容。
  • Widget恢复时,先检查Widget是否还存在。 用户可能已经卸载了提供Widget的应用。如果直接绑定,会抛异常。

我曾经犯过一个低级错误:备份时把Widget的provider组件名写死了。结果应用更新后包名变了,恢复时全部失败。后来我改成动态获取,每次备份都从AppWidgetManager读取最新的provider信息。

嗯,权限管理这块,说难不难,说简单也不简单。关键是养成好习惯:先检查,再操作;失败了,给提示。做到这三点,Launcher的权限部分基本稳了。


公众号:蓝海资料掘金营,微信deep3321