0x1、引言

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

上节 《【Jetpack】学穿:Activity Results API》 说过,在重构公司项目的BaseFragment时,遇到 onRequest源码PermissionResult() Deprecated的问题,顺带系统地学了一波源码之家 Activity Rejs函数式编程sults API 相关的姿势。

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

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

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


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>

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

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

② 申请权限

  • 调用 ContextCompat.checkSjs函数式编程elfPermission() 判断是否具有相应权限,此方法会返回:PER接口是什么MISSION_GRANTED (已授权) 或 PE接口测试RMISSION_DENIED (未授权);
  • 未授权的话调用 ActivityCompat.requestPermissions() 申请权限;
  • 重写 onReque接口自动化stPerm安全教育平台作业登录issionsResult() 回调方法,对授权结果进行判定,执行后续操作;

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

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

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

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

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


0x3、现在申请权限

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

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

运行效果如下:

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

接着试下多个权限申请,用到工龄越长退休金越多吗另一公司让员工下班发手机电量截图个协定 ReqGouestMultiplePermissions() ,代码示例如下:

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

运行效果如下:

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

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

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


0x4、亿点权限常识

详细学习资料可参见:《官方文档:Android 中的权安全教育平台作业登录限》

① 设立应用权限的原因

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

② 权限分类

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

  • 官方文档:权限 API 参考文档,源码编程器Ctrl + F 搜 Protection level: normal (按需替换) 查找对应级别的权限。
  • 系统源码/frameworks/base/core/res/工龄差一年工资差多少Android源码时代Manifest.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.shouldShowRequestPermisjs函数式编程sionRationale()

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

  • 没申请过权限,返回false,你申请就是了;
  • 申请过,但用户拒绝了,返回true,可以弹窗接口是什么提醒用户,然后在此申请权限;
  • 用户选择拒绝且不再显示,返回false,可以弹窗提醒必要权限,引导跳转设置手动授权;
  • 用户允许安全教育平台作业登录了,返回false,不需要申请接口自动化也不需要提示了;

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

④ 权限组

Android为了提高用户体验而设置的,根据设备能力或功能 将不同的权限组织为组,比如 REA源码编程器D_CONTACTSWRIT接口自动化E_CONTACTS 就属于同一个组。用户不必理解每个公司让员工下班发手机电量截图权限的具体定义,当组中某个权限被授予了,在请求组内的其他权限时,系统不会弹出询问授权窗口安全教育平台登录入口,而是直接授予,系统自动完成的,不用做啥处理

⑤ 兼容适配问题

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

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

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

常识了解得差不多了源码编程器,接着常识用新的API封装一接口文档个权限请求库~

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


0x5、封装源码时代探索之旅

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

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

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

整一波代码:

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()
    }
}

运行结果如下:

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

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

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

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


② 自定义ActivityResultContract

直接抄一波 RequestPermiss工资超过5000怎么扣税ion 改点东西就好,代码如下:

// 注: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")

一下子清爽了不少啊~

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

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

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

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


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

大佬的封装如下:安全教育平台作业登录

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

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

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

这里源码交易平台看得我有点懵,这样相比直接传函数有啥好处?

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

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

1接口) 什么是SAM

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

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

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

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

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

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

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

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

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

2) 高阶扩展函数

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

这里你可能一时半伙看不懂,但改安全教育平台作业登录成下面这样,你就懂了:

onShowRequestRationale: (PermissionScope) -> Unit

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

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

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

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

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

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

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

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

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

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

改下调用处:

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

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

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

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

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

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

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

参考资料:《android定位权限适配看这篇就够了》

三个定位权限:

<!-- 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 1接口和抽象类的区别0新增的权限,如果没有获得后台定位权限,当APP处于后台时,获取定位会失败。兼容处理如下:

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

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

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

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

就当我以为可以安全收工万事大吉时,运行源码编程器结果却是这样:

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

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

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

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

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

淦,直接就返回了,怪不得直源码编辑器下载接触发失败回调,难搞哦。难搞,看来申请多个权限不能这样搞安全生产法,只能用 Activ源码之家ityResultContracts#RequestMult工资超过5000怎么扣税iplePermissions,然后对请求结果进行工龄差一年工资差多少处理接口crc错误计数js函数式编程


⑥ 编写registe安全教育平台rForPermissionsResult

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

基本就这样,接着要加上前台和后台源码精灵永久兑换码定位授权的L安全教育平台auncher:

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

接着把权限申请部分的 registerForPermissionsRes接口自动化ult() 改成 registerForPermiss接口和抽象类的区别ionsResult() 就阔以了。

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

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

  • 郭霖:PermissionX
  • 轮子哥:XXPermissions

宫颈癌谢开源大佬的奉献精神,让我们少走了很多弯路,有了更多的时间摸鱼~


0x6、小结

本节先对比了新旧权安全教育平台登录入口限申请API的差异,接着学习了亿点权限相关的常识,紧接着工龄差一年工资差多少尝试用新Activ龚俊ity Results API对权限申请进行封装,然后又借鉴了一波大佬的封源码1688装进行修改,最后还对后台定位权限做了兼容性处理。如果读者耐心看完,想自己封装一个权限请求库枸杞,应该是手到擒来的小事了。

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

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