Android申请权限库,链式调用,结果回调,仿照京东、UC权限申请并显示权限说明

911 阅读5分钟

一、前言

最近发布APP到应用商店,因为申请权限时没有相关说明被打回,参考了京东、UC等APP,写了一套申请权限框架:RequestPermission

RequestPermission:Android申请权限库,链式调用,权限结果回调,使用起来更优雅。

二、剖析京东、UC申请权限过程

我们先看京东和UC APP申请权限的过程:

京东UC
jingdong.gifUC.gif

京东、UC申请权限流程如下:

截屏2022-07-02 下午4.21.47.png

京东、UC申请权限,会把申请权限这个过程托管给一个中介activity,这个中介activity向系统申请权限,再把权限结果返回给最初的activity。 把中介activity的主题设置为透明,activity之间转场动画也去掉,所以看上去和直接使用系统方法申请权限一样,弹出系统弹窗,看起来好像没有新开页面。

截屏2022-07-02 下午4.40.36.png

有了透明的中介activity,我们可操作的东西就来了:

  • 可以在透明的中介activity上半部分,放置一个文本,用来展示为什么要获取当前权限,这样用户基本都会同意。就如上面的京东和UC申请权限一样。
  • 由于有了中介activity,我们就可以把申请权限这个过程,封装为request-response的形式,链式调用,结果回调,使用起来更优雅。

三、使用RequestPermission库申请权限

基于上面对京东和UC的分析,写了一套权限申请权限框架:RequestPermission

GitHub地址:github.com/jinxiyang/R…

1、依赖

implementation 'io.github.jinxiyang:requestpermission:0.0.2'

2、简单的使用介绍

a)、不使用中转activity,申请权限

//请求定位权限
PermissionRequester(this)
    .addPermissions(PermissionUtils.LOCATION_PERMISSIONS)
    .request {
        Toast.makeText(this@MainActivity, "申请定位权限:${it.granted()}", Toast.LENGTH_SHORT).show()
    }

b)、使用中转activity,申请权限

//请求定位权限
PermissionRequester(this)
    .addPermissions(PermissionUtils.LOCATION_PERMISSIONS)
    .requestGlobal {
        Toast.makeText(this@MainActivity, "申请定位权限:${it.granted()}", Toast.LENGTH_SHORT).show()
    }

c)、使用中转activity,申请权限,同时显示权限说明

//统一权限页面:仿京东,页面顶部显示权限提示,背景透明
private fun jdRequestPermission() {
    //定义申请权限组时的提示文字,更友好,不会被工信部点名胡乱获取用户隐私。
    //京东第一安装,打开首页相机时,会有这样的权限申请页,并在页面背景提示相关信息

    //相机权限对应的提示
    val cameraExtra = Bundle()
    cameraExtra.putString(RequestPermissionActivityJD.KEY_PERMISSION_TITLE, "相机权限使用说明")
    cameraExtra.putString(RequestPermissionActivityJD.KEY_PERMISSION_DESC, "京东正在向您获取“相机”权限,同意后," +
            "你可以使用扫码服务,巴拉巴拉……")

    PermissionRequester(this)
        .addPermissionGroup(PermissionUtils.CAMERA_PERMISSIONS.toList(), cameraExtra)
        //设置自定义仿京东请求权限页面 RequestPermissionActivityJD,背景透明
        .requestGlobal(RequestPermissionActivityJD::class.java) {
            val camera = it.granted(PermissionUtils.CAMERA_PERMISSIONS)
            Toast.makeText(this@MainActivity, "申请相机权限:$camera", Toast.LENGTH_SHORT).show()
        }
}

效果如图:

jd-my.gif

四、使用RequestPermission的好处

1、链式调用,结果回调listener,请求权限和结果回调代码写在一块儿,高内聚低耦合。不用在activity里重写权限结果回调方法onRequestPermissionsResult()

2、使用了ActivityResultContracts机制,发起registerForActivityResult,所以在结果回调listener时,activity处于可见状态,即:activity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)

这时如果显示对话框,DialogFragment.show() 没有这个问题了:java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

五、RequestPermission的技术细节

申请权限的相关代码,没什么难度,就是一些代码的封装,大家都会写,这里不提了。

这里简单介绍一下如何实现链式调用、中介activity的透明主题的配置

1、实现链式调用

activity请求权限时,我们使用一个没有视图的fragment代理发起请求,在这个fragment内包装了以下两个功能:

  • 对于不使用中转activity,申请权限时,该fragment包装了请求权限,即: ActivityResultContracts.RequestMultiplePermissions()

  • 对于使用中转activity,申请权限时,该fragment包装了与中转activity的通信,即 ActivityResultContracts.StartActivityForResult()

这样对于需要权限的activity就不用重写onRequestPermissionsResult()onActivityResult()

归根结底是对ActivityResultContracts机制的封装,封装成接口回调形式。

有兴趣的可以看看下面两个类的源码:

/**
 * 对ActivityResultContracts机制进行封装,配合ActivityResultContractsFragment一起使用
 *
 *
 * 避免这个问题:
 *            在onCreate预先定义了Launcher,如果不定义,launcher.launch()时会报错:
 *                LifecycleOwner xx is attempting to register while current state is RESUMED.
 *                LifecycleOwners must call register before they are STARTED.
 *
 */
object ActivityResultContractsHelper {

    private const val FRAGMENT_TAG = "ActivityResultContractsFragment"
    
    //封装了startActivityForResult,结果listener回调
    @JvmStatic
    fun startActivityForResult(fm: FragmentManager, intent: Intent, onStartActivityForResultListener: OnStartActivityForResultListener) {
        val fragment = fm.findFragmentByTag(FRAGMENT_TAG)
        if (fragment is ActivityResultContractsFragment) {
            fragment.setOnStartActivityForResultListener(onStartActivityForResultListener)
            fragment.startActivityForResult(intent)
        } else {
            val activityResultFragment = ActivityResultContractsFragment.newInstance(intent)
            activityResultFragment.setOnStartActivityForResultListener(onStartActivityForResultListener)
            fm.beginTransaction()
                .add(activityResultFragment, FRAGMENT_TAG)
                .commitNowAllowingStateLoss()
        }
    }

    //封装了requestMultiplePermissions,权限结果listener回调
    @JvmStatic
    fun requestMultiplePermissions(fm: FragmentManager, permissions: Array<String>, onStartActivityForResultListener: OnRequestMultiPermissionListener) {
        val fragment = fm.findFragmentByTag(FRAGMENT_TAG)
        if (fragment is ActivityResultContractsFragment) {
            fragment.setOnRequestMultiPermissionListener(onStartActivityForResultListener)
            fragment.requestMultiplePermissions(permissions)
        } else {
            val activityResultFragment = ActivityResultContractsFragment.newInstance(permissions = permissions)
            activityResultFragment.setOnRequestMultiPermissionListener(onStartActivityForResultListener)
            fm.beginTransaction()
                .add(activityResultFragment, FRAGMENT_TAG)
                .commitNowAllowingStateLoss()
        }
    }
}

下面是没有UI的fragment的源码,封装了ActivityResultContracts机制:

/**
 * 没有UI的fragment,用于发起registerForActivityResult
 *
 * 封装了ActivityResultContracts机制,在onCreate预先定义了Launcher,需要使用时,直接launcher.launch()
 */
class ActivityResultContractsFragment : Fragment() {

    private lateinit var mStartActivityForResultLauncher: ActivityResultLauncher<Intent>
    private lateinit var mRequestMultiplePermissionsLauncher: ActivityResultLauncher<Array<String>>

    private val mStartActivityForResultLiveData = MutableLiveData<Intent>()
    private val mRequestMultiplePermissionsLiveData = MutableLiveData<Array<String>>()

    private var mOnStartActivityForResultListener: OnStartActivityForResultListener? = null
    private var mOnRequestMultiPermissionListener: OnRequestMultiPermissionListener? = null

    private val activityOptionsCompat: ActivityOptionsCompat by lazy {
        //页面转场无动画
        ActivityOptionsCompat.makeCustomAnimation(requireContext(), 0, 0)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //在onCreate预先定义了Launcher,如果不定义,launcher.launch()时会报错:
        //LifecycleOwner xx is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED.

        val startActivityForResult = StartActivityForResult()
        mStartActivityForResultLauncher = registerForActivityResult(startActivityForResult) {
            mOnStartActivityForResultListener?.onActivityResult(it.resultCode, it.data)
        }

        val requestMultiplePermissions = ActivityResultContracts.RequestMultiplePermissions()
        mRequestMultiplePermissionsLauncher = registerForActivityResult(requestMultiplePermissions) {
            mOnRequestMultiPermissionListener?.onRequestMultiPermission(it)
        }

        val args = arguments
        val intent = args?.getParcelable<Intent>("intent")
        if (intent != null) {
            args.putParcelable("intent", null)
            startActivityForResult(intent)
        }
        mStartActivityForResultLiveData.observe(this) {
            mStartActivityForResultLauncher.launch(it, activityOptionsCompat)
        }

        val permissions: Array<String>? = args?.getStringArray("permissions")
        if (permissions != null) {
            args.putStringArray("permissions", null)
            requestMultiplePermissions(permissions)
        }
        mRequestMultiplePermissionsLiveData.observe(this) {
            mRequestMultiplePermissionsLauncher.launch(it)
        }
    }

    fun startActivityForResult(intent: Intent) {
        mStartActivityForResultLiveData.postValue(intent)
    }

    fun requestMultiplePermissions(permissions: Array<String>) {
        mRequestMultiplePermissionsLiveData.postValue(permissions)
    }

    fun setOnStartActivityForResultListener(onStartActivityForResultListener: OnStartActivityForResultListener?) {
        this.mOnStartActivityForResultListener = onStartActivityForResultListener
    }

    fun setOnRequestMultiPermissionListener(onRequestMultiPermissionListener: OnRequestMultiPermissionListener?) {
        this.mOnRequestMultiPermissionListener = onRequestMultiPermissionListener
    }

    companion object {

        fun newInstance(intent: Intent? = null, permissions: Array<String>? = null): ActivityResultContractsFragment {
            val args = Bundle()
            args.putParcelable("intent", intent)
            args.putStringArray("permissions", permissions)
            val fragment = ActivityResultContractsFragment()
            fragment.arguments = args
            return fragment
        }
    }
}

2、中介activity的透明主题的配置

在清单文件给activity设置透明主题时,在android 8.0/8.1 (即v26/27),则会报这个错误:

java.lang.RuntimeException: Unable to start activity ComponentInfo java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation

意思是:透明的activity不允许在清单文件设置旋转方向 android:screenOrientation

我们有两种解决方案:

a)、第一种(推荐):在AndroidManifest.xml不为activity设置旋转方向这个属性:android:screenOrientation

AndroidManifest.xml配置透明主题:

<activity
    android:name=".GlobalRequestPermissionActivity"
    android:exported="false"
    android:configChanges="orientation|screenSize"
    android:theme="@style/RequestTransparentTheme.TransparentTheme"
    />

style.xml透明主题:

<style name="RequestTransparentTheme.TransparentTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="android:background">@android:color/transparent</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowIsTranslucent">true</item>
</style>

b)、第二种:android 8.0/8.1 (即v26/27)时,主题设置为不透明背景,其他系统版本设置透明主题:

AndroidManifest.xml配置主题:

<activity
    android:name=".GlobalRequestPermissionActivity"
    android:exported="false"
    android:screenOrientation="portrait"
    android:theme="@style/RequestTransparentTheme.OrientationTransparentTheme"
    />

RequestTransparentTheme.OrientationTransparentTheme根据系统版本,在android 8.0/8.1 (即v26/27)时,主题设置为不透明背景,其他系统版本设置透明主题

values文件夹下的style.xml(系统小于android 8.0),透明主题:

<style name="RequestTransparentTheme.OrientationTransparentTheme" parent="RequestTransparentTheme.TransparentTheme"/>

values-v26文件夹下的style.xml(系统android 8.0/8.1),不透明主题:

<style name="RequestTransparentTheme.OrientationTransparentTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="android:background">@android:color/white</item>
    <item name="android:windowBackground">@android:color/white</item>
    <item name="android:windowIsTranslucent">false</item>
    <item name="android:windowDisablePreview">true</item>
</style>

values-v28文件夹下的style.xml(系统大于android 8.1),透明主题:

<!--  Android 9.0及以上  透明activity主题  -->
<style name="RequestTransparentTheme.OrientationTransparentTheme" parent="RequestTransparentTheme.TransparentTheme"/>

PS.不需要在新建values-v27(系统android 8.1)