【Jetpack】一次Android权限请求库的封装记录

0x1、引言

项目中,把申请权限相关的操作都塞到BaseActivity/BaseFragment中,可以,但不太优雅,很多子类Activity/Fragment被迫继承了这个用不到的功能。毕竟只有刚进APP、拍照录像、地图定位时才会去申请权限,属实没必要。

所以本节想做的事就是:捋下权限相关常识 + 用Activity Results API封装个权限请求库玩玩。多说无益,我直接开始!


0x2、以前申请权限

① AndroidManifest.xml中声明权限

<manifest ...>
    <!-- 访问相机 -->
    <uses-permission android:name="android.permission.CAMERA"/>
    <application ...>
        ...
    </application>
</manifest> 

如果App中用到了硬件,如相机,建议加上 可选声明,不加的话,Android系统会认为你的App要在有该硬件的情况下才能运行,如果没此硬件的话直接就阻止你App的安装。但大多数情况下,我们的App在没有该硬件的设备上也能运行,所以建议还是加上:

<manifest ...>
    <application>
        ...
    </application>
    <uses-feature android:name="android.hardware.camera"
                  android:required="false" />
<manifest> 

用到此硬件时再执行下判断,有就执行正常逻辑,没有就执行其他逻辑:

// 判断有无前置摄像头
if (applicationContext.packageManager.hasSystemFeature(
        PackageManager.FEATURE_CAMERA_FRONT)) {
    // 有,XXX
} else {
    // 没有,XXX
} 

② 申请权限

  • 调用 ContextCompat.checkSelfPermission() 判断是否具有相应权限,此方法会返回:PERMISSION_GRANTED (已授权) 或 PERMISSION_DENIED (未授权);
  • 未授权的话调用 ActivityCompat.requestPermissions() 申请权限;
  • 重写 onRequestPermissionsResult() 回调方法,对授权结果进行判定,执行后续操作;

代码示例如下:(OldRequestPermissionActivity.kt)

每次点击随机申请一个权限,回调中打印授权结果,运行效果如下:


0x3、现在申请权限

权限声明是一样的,权限申请API不同:使用 Activity Results API 提供的 RequestPermission()RequestMultiplePermissions() 来申请权限。单个权限申请代码示例如下:(NewRequestPermissionActivity.kt)

运行效果如下:

接着试下多个权限申请,用到另一个协定 RequestMultiplePermissions() ,代码示例如下:

运行效果如下:

代码好像也没精简多少,当然,这里并不完整,只是纯粹展示下最基础的API调用而已,接下来了解亿点权限常识。


0x4、亿点权限常识

详细学习资料可参见:《官方文档:Android 中的权限》

① 设立应用权限的原因

保护用户隐私,包括两个方面:数据 (如系统状态、用户联系信息) 和 操作 (如音频录制)。

② 权限分类

Android 将权限分为不同的类型,并分配了不同的 保护级别,可通过下述方式查阅所有权限API:

  • 官方文档:权限 API 参考文档,Ctrl + F 搜 Protection level: normal (按需替换) 查找对应级别的权限。
  • 系统源码:/frameworks/base/core/res/AndroidManifest.xml,该文件定义了系统权限等信息。
1) 安装时权限

系统会在用户允许安装该应用时自动授予相应权限 (需在应用中声明),分为两个子类型:

  • 普通权限 (normal) → 不会威胁到用户安全和隐私的权限,只需在AndroidManifest.xml中声明下就能直接使用。
  • 签名权限 (signature) → 当应用声明了其他应用已定义的签名权限时,如果两个应用使用同一个签名文件进行签名,系统会在安装时向前者授予该权限。否则,系统无法向前者授予该权限。
2) 运行时权限 (dangerous)

Android 6.0 (M) 的新特性,又称 危险权限,指的是 可能会触及用户隐私或对设备安全性造成影响的权限,如获得联系人信息、访问位置等。此类权限需要在代码进行申请,系统弹出许可对话框,当用户手动同意后才会获得授权。

3) 特殊权限

比较少见,Google认为此类权限比危险权限更敏感,因此需要让用户到专门的设置页面手动对某个应用程序授权。如:悬浮框权限、修改设置权限、管理外部存储等。特殊权限需要特殊处理!!!

Tips:(两个权限相关的adb命令)

# 查看应用拥有的权限
adb shell dumpsys package 应用包名

# 获取权限等级
adb shell dumpsys package permision |grep -i prot 

③ 普通权限的申请流程

例子中的API演示,只包含了 检查权限请求权限,其实还有一个 解释权限 的API:

ActivityCompat.shouldShowRequestPermissionRationale()

可在 请求权限回调后 判断:用户是否再次拒绝了请求,解读示例如下:

  • 没申请过权限,返回false,你申请就是了;
  • 申请过,但用户拒绝了,返回true,可以弹窗提醒用户,然后在此申请权限;
  • 用户选择拒绝且不再显示,返回false,可以弹窗提醒必要权限,引导跳转设置手动授权;
  • 用户允许了,返回false,不需要申请也不需要提示了;

这部分处理逻辑可以根据自己实际情况来安排,这里只是举例~

④ 权限组

Android为了提高用户体验而设置的,根据设备能力或功能 将不同的权限组织为组,比如 READ_CONTACTSWRITE_CONTACTS 就属于同一个组。用户不必理解每个权限的具体定义,当组中某个权限被授予了,在请求组内的其他权限时,系统不会弹出询问授权窗口,而是直接授予,系统自动完成的,不用做啥处理

⑤ 兼容适配问题

权限申请时还得进行兼容,比如:定位权限就经过好几次重大变更,普通定位权限、后台定位权限获取规则的不断变化。又比如:申请悬浮框权限,Android 10前能直接跳转到悬浮框设置页,Android 11后只能跳设置悬浮框的管理列表。

综上,权限请求库要做的事情就是:

申请运行时权限 + 申请特殊权限的特殊处理 + 处理兼容问题

常识了解得差不多了,接着常识用新的API封装一个权限请求库~


0x5、封装探索之旅

① 官方文档的请求权限建议流程貌似不太行?

按照官方文档给出的用户请求权限建议流程:

整一波代码:

class TestCpPermissionActivity: AppCompatActivity() {
    private lateinit var mBinding: ActivityTestCpPermissionBinding
    private var mTipsDialog: AlertDialog? = null
    private var mGoSettingDialog: AlertDialog? = null
    private val mTakePhoto = takePhoto { "是否有拍照结果:${it == null}".logD() }

    private val mPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
            if (isGranted) {
                "已授予权限".logD()
            } else {
                "未授予权限".logD()
                showGoSettingDialog()
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_test_cp_permission)
    }

    private fun requestPermission(permission: String) {
        val selfPermission = ContextCompat.checkSelfPermission(this, permission)
        when {
            // 已授权
            PackageManager.PERMISSION_GRANTED == selfPermission -> {
                shortToast("已授予权限:{$permission}")
                mTakePhoto.launch(null)
            }
            // 拒绝,但未选中 "不再提醒"; 可能弹窗提示用户,需要此权限的原因
            ActivityCompat.shouldShowRequestPermissionRationale(this, permission) -> {
                showTipsDialog(permission)
            }
            // 拒绝,且不再提醒
            else -> {
                mPermissionLauncher.launch(permission)
            }
        }
    }

    fun requestPermissionClick(v: View) {
        requestPermission(Manifest.permission.CAMERA)
    }

    private fun showTipsDialog(permission: String) {
        if(mTipsDialog == null) {
            mTipsDialog = AlertDialog.Builder(this).apply {
                setMessage("当前应用缺少必要权限,会导致功能暂时无法使用,请重新授权")
                setPositiveButton("确定") { _, _ -> mPermissionLauncher.launch(permission) }
            }.create()
        }
        mTipsDialog!!.show()
    }

    private fun showGoSettingDialog() {
        if(mGoSettingDialog == null) {
            mGoSettingDialog = AlertDialog.Builder(this).apply {
                setTitle("提示信息")
                setMessage("当前应用缺少必要权限,该功能暂时无法使用。如若需要,请单击【确定】按钮前往设置中心进行权限授权。")
                setNegativeButton("取消") { _, _ -> }
                setPositiveButton("确定") { _, _ ->
                    // 跳转到设置页
                    startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
                        Uri.parse("package:" + this@TestCpPermissionActivity.packageName)
                    ))
                }
            }.create()
        }
        mGoSettingDialog!!.show()
    }
} 

运行结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VTIBdLYE-1654695578515)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b2fdd6d3888e4d62940e7fd38f58474e~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image)]

???好像不太对劲,应该是先弹普通提示,然后再请求,再拒绝,最后才弹去设置的。shouldShowRequestPermissionRationale(Context, String) 不该放这里,应该放到权限回调里。

然后问题来了:怎么在权限回调里拿到申请的哪个权限ActivityResultContract.RequestPermission 这玩意只返回了一个 是否授权的boolean值。可以在外部定义一个全局变量,每次申请权限都给它赋值,然后直接拿,可以是可以,就是用起来很繁琐,每次申请权限还要让开发者另外定义一个变量存?

其实自定义协定,添加一个存储权限的变量,然后输出类型改为 Pair<String, Boolean> 就好了。


② 自定义ActivityResultContract

直接抄一波 RequestPermission 改点东西就好,代码如下:

// 注:Output输出类型变成了Pair<String, Boolean>
class RequestPermissionContract : ActivityResultContract<String, Pair<String, Boolean>>() {
    // 存储权限的变量
    private lateinit var mPermission: String

    override fun createIntent(context: Context, input: String): Intent {
        // 创建Intent前赋值
        mPermission = input
        return Intent(ACTION_REQUEST_PERMISSIONS).putExtra(EXTRA_PERMISSIONS, arrayOf(input))
    }

    override fun parseResult(resultCode: Int, intent: Intent?): Pair<String, Boolean> {
        if (intent == null || resultCode != Activity.RESULT_OK) return mPermission to false
        val grantResults =
            intent.getIntArrayExtra(ActivityResultContracts.RequestMultiplePermissions.EXTRA_PERMISSION_GRANT_RESULTS)
        return mPermission to
            if (grantResults == null || grantResults.isEmpty()) false
            else grantResults[0] == PackageManager.PERMISSION_GRANTED
    }

    override fun getSynchronousResult(
        context: Context,
        input: String?
    ): SynchronousResult<Pair<String, Boolean>>? =
        when {
            null == input -> SynchronousResult("" to false)
            ContextCompat.checkSelfPermission(context, input) == PackageManager.PERMISSION_GRANTED -> {
                SynchronousResult(input to true)
            }
            else -> null
        }
} 

接着调用处,使用这个协定,然后在回调里shouldShowRequestPermissionRationale(),此时就可以拿到权限了~

private val mPermissionLauncher =
    registerForActivityResult(RequestPermissionContract()) { result ->
        if (result.second) {
            "已授予权限".logD()
        } else {
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, result.first)) {
                showTipsDialog(result.first)
            } else {
                showGoSettingDialog()
            }
        }
    }

private fun requestPermission(permission: String) {
    if(ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
        shortToast("已授予权限:{$permission}")
        mTakePhoto.launch(null)
    } else {
        mPermissionLauncher.launch(permission)
    }
} 

③ 编写快速生成扩展

每次申请权限都要写这么一大坨代码显然不河里,可以ActivityResultCaller编写快速生成ActivityResultLauncher的扩展方法,传入三个处理函数,已授权授权提示拒绝授权,用户可以按需传入,所以定义为可空类型:

fun ActivityResultCaller.registerForPermissionResult(
    onGranted: (() -> Unit)? = null,
    onDenied: (() -> Unit)? = null,
    onShowRequestRationale: (() -> Unit)? = null
): ActivityResultLauncher<String> {
    return registerForActivityResult(RequestPermissionContract()) { result ->
        val permission = result.first
        when {
            // 已授权
            result.second -> onGranted?.let { it -> it() }
            // 提示授权
            permission.isNotEmpty() && ActivityCompat.shouldShowRequestPermissionRationale(
                this as Activity,
                permission
            ) -> onShowRequestRationale?.let { it -> it() }
            // 拒绝授权
            else -> onDenied?.let { it -> it() }
        }
    }
} 

调用:

// 定义
private val mPermissionLauncher = registerForPermissionResult(
    onGranted = { "已授予权限".logD(); mTakePhoto.launch(null) },
    onDenied = {  showGoSettingDialog()  },
    onShowRequestRationale = { showTipsDialog(it) })

// 申请权限
mPermissionLauncher.launch("xxx") 

一下子清爽了不少啊~

就在我有点沾沾自喜时,元认知又在提醒我:

在没人指导和阅读借鉴优秀源码的情况下,很容易有种自己代码已经写得足够好的错觉。

所以,得找几个库借(chao)鉴(xi)下,这不,刚好看到 DylanCai 大佬封装的 ActivityResult.kt


④ 借(chao)鉴(xi)别人的优秀代码

大佬的封装如下:

写法和我类似,但传递的却是 AppSettingsScope.() -> Unit高阶扩展函数,为啥这样做,而且接口的定义:

这里看得我有点懵,这样相比直接传函数有啥好处?

直接加一波作者浪哥好友,咨询一波,在其耐心点拨下 + 自己翻阅海量资料补全盲区后,终于茅塞顿开~

1) 什么是SAM

Java8后,将只有 单一抽象方法的接口 称为 SAM接口函数式接口,通过Lambda可以大大简化SAM接口的调用。

比如Java代码中SAM转换前后,代码确实精简不少:

而在Kotlin中,支持Java的SAM转换,但是却不直接支持Kotlin接口的SAM转换:

网上搜了下不支持Kotlin interface SAM转换的原因:

Kotlin 本身已经有了函数类型和高阶函数,不需要在去SAM转化。本意是鼓励开发者多尝试用函数式编程的思想替换面向对象的思维方式,所以推荐使用函数类型替代SamType。

说是这么说,Kotlin 1.4后还是支持了,只需在Kotlin Interface定义时,加上 fun 就好了:

2) 高阶扩展函数

这里你可能一时半伙看不懂,但改成下面这样,你就懂了:

onShowRequestRationale: (PermissionScope) -> Unit 

就是在内部初始化了一个 PermissionScope 实例 (通用功能实现,如跳转设置页),在调用处就能拿到这个实例:

此处需要通过 it 拿到实例,如果改成:PermissionScope.() -> Unit

直接通过this就可以拿到实例。这波封装雀食牛逼!!!

代码很Fine,下一秒My,直接定义一个跳转应用设置页的Launcher:

修改请求单个权限的代码:

改下调用处:

完美!这个时候突然想起一个问题,Kotlin里这样调用着实爽,但Java里该怎么传参呢?毕竟公司项目一堆页面用Java写的:

其实也不难,lambda表达式走一波就好了:

接着PermissionScope接口定义的粒度可以自行权衡,浪哥是每种业务类型(如跳转设置)都定义一个函数式接口,也可以把业务都塞到一个接口里,然后全部实现,好处是每个函数里可以可以调用所有业务。

⑤ 特殊权限处理:后台定位权限

三个定位权限:

<!-- Allows an app to access approximate location.  近似定位权限,api1,如:网络定位 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!-- Allows an app to access precise location 精准定位权限,api1,如:GPS定位-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- Allows an app to access location in the background. 后台定位权限,api29,android10新增 -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> 

第三个权限是Android 10新增的权限,如果没有获得后台定位权限,当APP处于后台时,获取定位会失败。兼容处理如下:

  • Android 10.0 以下,没有这个权限,只需申请 ACCESS_FINE_LOCATION 权限;
  • Android 10.0,可以同时申请ACCESS_FINE_LOCATION和ACCESS_BACKGROUND_LOCATION权限
  • Android 10.0 以上,必须先申请ACCESS_FINE_LOCATION权限并获得授权后,才能申请ACCESS_BACKGROUND_LOCATION权限,如果通知申请,不会弹窗,直接授权失败。

直接写一个launch()扩展方法,然后去重权限数据组,然后遍历,对后台权限特殊处理:

看着好像没啥问题,调用处传入后台定位权限申请:

就当我以为可以收工万事大吉时,运行结果却是这样:

拒绝了就一直弹窗,淦,咋回事啊?看了下日志输出:

一次只能申请一组权限,这是要一组完了才能申请下一组的意思吗?跟一波源码 Activity#requestPermissions

淦,直接就返回了,怪不得直接触发失败回调,难搞哦。难搞,看来申请多个权限不能这样搞,只能用 ActivityResultContracts#RequestMultiplePermissions,然后对请求结果进行处理了。


⑥ 编写registerForPermissionsResult

基本就这样,接着要加上前台和后台定位授权的Launcher:

接着把权限申请部分的 registerForPermissionsResult() 改成 registerForPermissionsResult() 就阔以了。

关于封装探索的记录就到之类,代码写得很乱,不优雅,BUG也多,后面肯定还要反复迭代改进的,库其次,折腾过程有收获就行。后续代码会丢 Github: CpPermission 上,感兴趣的可以先Star占坑,除了可以参考 DylanCai的ActivityResult 封装外,还可以借鉴一些成熟的开源权限库 (适配策略,代码设计-如郭神对于特殊权限的处理用到了责任链的设计模式),当然也可以直接用:

0x6、小结

本节先对比了新旧权限申请API的差异,接着学习了亿点权限相关的常识,紧接着尝试用新Activity Results API对权限申请进行封装,然后又借鉴了一波大佬的封装进行修改,最后还对后台定位权限做了兼容性处理。如果读者耐心看完,想自己封装一个权限请求库,应该是手到擒来的小事了。

有问题或者建议欢迎在评论区提出,肝文不易,如果本文有帮到你的话,可以给个三连,谢谢~

总结

要想成为架构师,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

一、架构师筑基必备技能

1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……

在这里插入图片描述

二、Android百大框架源码解析

1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程

在这里插入图片描述

三、Android性能优化实战解析

  • 腾讯Bugly:对字符串匹配算法的一点理解
  • 爱奇艺:安卓APP崩溃捕获方案——xCrash
  • 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
  • 百度APP技术:Android H5首屏优化实践
  • 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
  • 携程:从智行 Android 项目看组件化架构实践
  • 网易新闻构建优化:如何让你的构建速度“势如闪电”?

在这里插入图片描述

四、高级kotlin强化实战

1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》

  • 从一个膜拜大神的 Demo 开始

  • Kotlin 写 Gradle 脚本是一种什么体验?

  • Kotlin 编程的三重境界

  • Kotlin 高阶函数

  • Kotlin 泛型

  • Kotlin 扩展

  • Kotlin 委托

  • 协程“不为人知”的调试技巧

  • 图解协程:suspend

在这里插入图片描述

五、Android高级UI开源框架进阶解密

1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
在这里插入图片描述

六、NDK模块开发

1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习

在这里插入图片描述

七、Flutter技术进阶

1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)

在这里插入图片描述

八、微信小程序开发

1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……

在这里插入图片描述

全套视频资料:

一、面试合集
在这里插入图片描述
二、源码解析合集

在这里插入图片描述
三、开源框架合集

在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取【保证100%免费】↓↓↓
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值