比如要实现拨打电话的功能,一般我们要编写如下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
在这里插入图片描述

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐