比如要实现拨打电话的功能,一般我们要编写如下Android运行时权限API
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if(ContextCompat.checkSelfPermission(this,Manifest.permission.CALL_PHONE)!=
PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE),1)
}else{
call()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when(requestCode){
//如果requestCode是1
1->{
if(grantResults.isNotEmpty()&&grantResults[0]==PackageManager.PERMISSION_GRANTED){
call()
}else{
Toast.makeText(this,"You denied the permission",Toast.LENGTH_SHORT).show()
}
}
}
}
//执行相关打电话操作
private fun call() {
//
}
}
可以看到,这种系统内置的运行时权限API的用法还是非常烦琐的,需要先判断用户是否授权我们拨打电话的权限,如果没有的话需要进行权限申请,然后还要在onRequestPermissionsResult()回调中处理权限申请的结果,最后才能去执行拨打电话的操作。
我们可以通过这个过程编写一个开源库PermissionX。
之前我们写的所有代码都是在app目录下进行的。这其实是一个专门用于开发应用程序的模块。而我们现在要开发的是一个库,因此我们需要新建一个模块。
实际上,一个Android项目中可以包含任意多个模块,并且模块与模块之间可以相互引用。比方说,我们在模块A中编写了一个功能,那么只需要在模块B中引入模块A,模块B就可以无缝地使用模块A中提供的所有功能。
在PermissionX项目中新建一个模块,并在这个模块中实现具体的功能。对着最顶层的PermissionX目录右击→New→Module,选择Android Library会弹出如下
点击“Finish”按钮完成创建,现在PermissionX工程目录下应该就有app和library两个模块了。
观察一下library模块中的build.gradle文件,其简化后的代码如下所示:
plugins {
id 'com.android.library'
id 'kotlin-android'
}
android {
compileSdkVersion 32
buildToolsVersion "30.0.3"
defaultConfig {
minSdkVersion 21
targetSdkVersion 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
...
}
...
会发现它和app模块中的build.gradle文件有两个重要的区别:第一,这里头部引入的插件是com.android.library,表示这是一个库模块,而app/build.gradle文件头部引入的插件是com.android.application,表示这是一个应用程序模块;第二,这里的defaultConfig闭包中是不可以配置applicationId属性的,而app/build.gradle中则必须配置这个属性,用于作为应用程序的唯一标识。
想要对运行时权限的API进行封装,这个操作是有特定的上下文依赖的,一般需要在Activity中接收onRequestPermissionsResult()方法的回调才行,所以不能简单地将整个操作封装到一个独立的类中。受此限制以往都是将运行权限的操作封装到BaseActivity中,或者提供一个透明的Activity来处理运行时权限等。
其实Google在Fragment中也提供了一份相同的API,使得我们在Fragment中也能申请运行时权限。不同的是,Fragment并不像Activity那样必须有界面,我们完全可以向Activity中添加一个隐藏的Fragment,然后在这个Fragment中对运行时权限的API进行封装。这是一种轻量级的做法,不用担心隐藏Fragment对Activity性能的影响。
package com.permission.yiran
import android.content.pm.PackageManager
import androidx.fragment.app.Fragment
class InvisibleFragment: Fragment() {
//callback为函数类型变量,可为空
private var callback: PermissionCallback? =null
fun requestNow(cb:PermissionCallback,vararg permissions:String){
callback=cb
requestPermissions(permissions,1)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(requestCode==1){
//使用deniedList列表来记录所有被用户拒绝的权限
val deniedList=ArrayList<String>()
for((index,result) in grantResults.withIndex()){
//如果发现某个权限未被用户授权
if(result!=PackageManager.PERMISSION_GRANTED){
deniedList.add(permissions[index])
}
}
//标识是否所有申请权限均已被授权,如果为空说明都已授权
val allGranted=deniedList.isEmpty()
//let函数用于判空,it代表callback对象
callback?.let {
it(allGranted,deniedList)
}
}
}
}
首先我们定义了一个callback变量作为运行时权限申请结果的回调通知方式,并将它声明成了一种函数类型变量,该函数类型接收Boolean和List< String >这两种类型的参数,并且没有返回值。
然后定义一个requestNow()方法,该方法接收一个与callback变量类型相同的函数类型参数,同时还使用vararg关键字接收了一个可变长度的permissions参数列表。将传递进来的函数类型参数赋值给callback变量,然后调用Fragment中提供的requestPermissions()方法去立即申请运行时权限,并将permissions参数列表传递进去,这样就可以实现由外部调用方自主指定要申请哪些权限的功能了。
接下来还需要重写onRequestPermissionsResult()方法,并在这里处理运行时权限的申请结果。可以看到,我们使用了一个deniedList列表来记录所有被用户拒绝的权限,然后遍历grantResults数组,如果发现某个权限未被用户授权,就将它添加到deniedList中。遍历结束后使用了一个allGranted变量来标识是否所有申请的权限均已被授权,判断的依据就是deniedList列表是否为空。最后使用callback变量对运行时权限的申请结果进行回调。
typealias PermissionCallback=(Boolean, List<String>) -> Unit
class InvisibleFragment: Fragment() {
//callback可为空
private var callback: PermissionCallback? =null
fun requestNow(cb:PermissionCallback,vararg permissions:String){
callback=cb
requestPermissions(permissions,1)
}
...
}
typealias 关键字可以用于给任意类型指定一个别名,比如我们将(Boolean, List< String >) -> Unit的别名指定成了PermissionCallback,这样就可以使用PermissionCallback来替代之前所有使用(Boolean, List< String >) -> Unit的地方。
接下来就是对外接口部分,新建一个PermissionX单例类
package com.permission.yiran
import androidx.fragment.app.FragmentActivity
object PermissionX {
private const val TAG="InvisibleFragment"
fun request(activity:FragmentActivity,vararg permissions:String,callback:
PermissionCallback){
val fragmentManager=activity.supportFragmentManager
val existedFragment=fragmentManager.findFragmentByTag(TAG)
val fragment=if(existedFragment!=null){
existedFragment as InvisibleFragment//大到小强制转换
}else{
val invisibleFragment=InvisibleFragment()
fragmentManager.beginTransaction().add(invisibleFragment,TAG).commitNow()
invisibleFragment
}
fragment.requestNow(callback, *permissions)
}
}
将PermissionX指定为单例类,是为了让PermissionX中的接口能够更加方便地被调用。我们在PermissionX中定义了一个request()方法,这个方法接收一个FragmentActivity参数、一个可变长度的permissions参数列表,以及一个callback回调。
在request()方法中,首先获取FragmentManager的实例,然后调用findFragmentByTag()方法来判断传入的Activity参数是否已经包含了指定TAG的Fragment,也就是我们刚才编写的InvisibleFragment。如果已经包含则直接使用该Fragment,否则就创建一个新的InvisibleFragment实例,并将它添加到Activity中,同时指定一个TAG。注意:在添加结束后一定要调用commitNow()方法,而不能调用commit()方法,因为commit()方法并不会立即执行添加操作,因而无法保证下一行代码执行时InvisibleFragment已经被添加到Activity中。
有了InvisibleFragment的实例之后,接下来我们只需要调用它的requestNow()方法就能去申请运行时权限了,申请结果会自动回调到callback参数中。需要注意的是,permissions参数在这里实际上是一个数组。对于数组,我们可以去遍历也可以通过下标访问,但是不可以直接将它传递给另外一个接收可变长度参数的方法。因为,这里在调用requestNow()方法时,在Permissions参数的前面加上一个*,这个符号表示将一个数组转换成可变长度参数传递过去。
对开源库进行测试
我们可以通过在app模块中引入library模块,然后在app模块中使用PermissionX提供的接口编写一些申请运行时权限的代码,看看能否正常工作,以此来验证PermissionX库的正确性。
dependencies {
...
implementation project(':Library')
}
接下来编写activity_main.xml文件,在里面加入一个用于拨打电话的按钮
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Make Call"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
在MainActivity 中申请拨打电话的运行时权限,并实现拨打电话的功能
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
makeCallBtn.setOnClickListener {
PermissionX.request(this,
Manifest.permission.CALL_PHONE){
allGranted,deniedList->
if(allGranted){
call()
}else{
Toast.makeText(this,"You denied $deniedList",Toast.LENGTH_SHORT).show()
}
}
}
}
private fun call(){
try {
val intent=Intent(Intent.ACTION_CALL)
intent.data= Uri.parse("tel:10086")
startActivity(intent)
}catch (e:SecurityException){
e.printStackTrace()
}
}
}
只需要调用PermissionX的request()方法,传入当前的Activity和要申请的权限名,然后再Lambda表达式中处理权限的申请结果就可以了。如果allGranted等于true,就说明所有申请的权限都被用户授权了,那么就执行拨打电话的操作,否则使用Toast弹出一条失败的提示。
另外,PermissionX也支持一次性申请多个权限,只需要将所有要申请的权限名都传入request()方法就可以了。
例如:
PermissionX.request(this,
Manifest.permission.CALL_PHONE,Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_CONTACTS){
allGranted,deniedList->
if(allGranted){
Toast.makeText(this,"All permissions are granted",Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(this,"You denied $deniedList",Toast.LENGTH_SHORT).show()
}
}
还要记得在AndroidManifest.xml文件中添加拨打电话的权限声明
<uses-permission android:name="android.permission.CALL_PHONE"/>
运行效果
点击MAKE CALL按钮
点击Allow