PermissionX 现在支持 Java 了!还有 Android 11 权限变更讲解
各位小伙伴们早上好,不知道你们有没有惊讶于我的速度,因为不久之前我才新发布的开源库 PermissionX 今天又更新了。
是的,在不到一个月的时间里,PermissionX 又迎来了一次重大的版本更新。如果你觉得一个月还不算快的话,可别忘了,两周之前我还发布了 LitePal 的新版本。对于我来说,这个速度已经是相当极限了。
不过,可能还有不少朋友不知道 PermissionX 是什么,这里我给出上一篇文章的链接,还没看过的小伙伴先去补补课 Android 运行时权限终极方案,用 PermissionX 吧 (opens new window) 。
本来按照迭代计划,下一个版本中,我是准备给 PermissionX 增加自定义权限提示对话框样式的功能。然而随着第一个版本的发布,根据大家的反馈,我意识到了另一个更加紧急的需求,就是对 Java 语言的支持。
真的很遗憾看到,即使在今天,Kotlin 在国内仍然还只是少部分开发者群体使用的语言,然而这就是现实。因此,如果 PermissionX 只支持 Kotlin 语言的话,势必将大部分的开发者都拒之了门外。
其实最初我让 PermissionX 只支持 Kotlin 语言,是因为我实在不想同时维护两个版本,这样修改任何功能都需要在两个地方各改一遍,维护成本过高。
然而后面我又做了一些更全面的思考,发现只需要稍微付出一点点语法方面的代价,就可以让一份代码同时支持 Java 和 Kotlin 两种语言,那么本篇文章我们就来学习一下是如何实现的。
# 兼容Java和Kotlin
首先我们来回顾一下 PermissionX 的基本用法,这段代码在上一篇文章中我们是见过的:
PermissionX.init(this)
.permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
.onExplainRequestReason { deniedList ->
showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白")
}
.onForwardToSettings { deniedList ->
showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白")
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
}
}
复制代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
是的,在 Android 程序的权限申请变得如此简单。
首先调用 init() 方法进行初始化,调用 permissions() 方法来指定要申请哪些权限,在 onExplainRequestReason() 方法中针对那些被拒绝的权限向用户解释申请的原因并重新申请,在 onForwardToSettings() 方法中针对那些被永久拒绝的权限向用户解释为什么它们是必须的,并自动跳转到应用设置当中提醒用户手动开启权限。最后调用 request() 方法开始请求权限,并接收申请的结果。
整段用法简洁明了,而且 PermissionX 帮助开发者解决了权限申请过程中最痛苦的一些逻辑处理,比如权限被拒绝了怎么办?权限被永久拒绝了怎么办?
那么之所以能将 PermissionX 的用法设计得这么简单明了,主要得感谢 Kotlin 的高阶函数功能。上述代码示例当中的 onExplainRequestReason() 方法、onForwardToSettings() 方法、request() 方法,实际上都是高阶函数。对于高阶函数中接收的函数类型参数,我们可以直接传入一个 Lambda 表达式,然后在 Lambda 表达式当中处理回调逻辑即可。
然而问题也就出现在了这里,由于 Java 是没有高阶函数这个概念的,因此这种便捷性的语法在 Java 语言当中并不适用,所以也就导致了 PermissionX 不支持 Java 的情况。
不过,这个问题是可以解决的!
事实上,在 Kotlin 语言当中,我们除了可以向高阶函数传递 Lambda 表达式,还可以向另一种 SAM 函数传递 Lambda 表达式。SAM 的全称是 Single Abstract Method,又叫做单抽象方法。具体来讲,如果 Java 中定义的某个接口,里面只有一个待实现方法(也就是所谓的单抽象方法),那么此时我们也可以向其传递 Lambda 表达式。
举一个具体的例子,所有 Android 开发者一定都调用过 setOnClickListener() 方法,这个方法可以用于给一个控件注册点击事件。
在 Java 当中我们会这样写:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
复制代码
2
3
4
5
6
7
8
这段代码因为所有人都实在是太熟悉了,因此没什么解释的必要。
但是可以看到,在 setOnClickListener() 方法中,我们创建了一个 View.OnClickListener 的匿名类,那么 View.OnClickListener 的代码是什么样的呢?点击查看其源码,如下所示:
public class View implements Callback, android.view.KeyEvent.Callback, AccessibilityEventSource {
public interface OnClickListener {
void onClick(View view);
}
}
复制代码
2
3
4
5
6
7
8
9
可以看到,OnClickListener 是一个接口,并且这个接口当中只有一个 onClick() 方法,因此这就是一个单抽象方法接口。
那么根据上面的规则,Kotlin 允许我们向一个接收单抽象方法接口的函数传递 Lambda 表达式。因此,在 Kotlin 当中,我们给一个按钮注册点击事件通常都是这么写的:
button.setOnClickListener {
}
复制代码
2
3
4
5
看到这里,有没有受到点启发呢?反正我是受到了。也就是说,如果 PermissionX 想要同时兼容 Java 和 Kotlin 语言的话,可以很好地利用单抽象方法接口这个特性。将原本的高阶函数都改成这种 SAM 函数,那么不就自然可以兼容两种语言了吗?
没错,我也确实是这样做的,不过具体在实现的过程中还是遇到了一点问题。
因为高阶函数的功能是十分强大的,我们除了可以定义一个函数类型的参数列表以及它的返回值,还可以定义它的所属类。来看 PermissionX 中的一段示例代码:
fun onExplainRequestReason(callback: ExplainScope.(deniedList: MutableList<String>) -> Unit): PermissionBuilder {
explainReasonCallback = callback
return this
}
复制代码
2
3
4
5
6
以上代码对于没接触过 Kotlin 的朋友来说,可能会像天书一样难以理解,然而如果你学过 Kotlin 的话,就知道这只是定义了一个简单的函数类型参数。是的,这里我又要推荐我写的新书《第一行代码 第 3 版》 (opens new window)了,还没有阅读过的朋友可以认真考虑一下,能在很大程序上帮助你轻松上手 Kotlin 语言。
那么上述代码中,我们将函数类型的所属类定义在了 ExplainScope 当中,这意味着什么?意味着,在 Lambda 表达式当中,我们就自动拥有了 ExplainScope 的上下文,因此可以直接调用 ExplainScope 类中的任何方法。所以,你也已经猜到了,本篇文章第一段示例代码中调用的 showRequestReasonDialog() 方法就是定义在 ExplainScope 类当中的。
然而 Kotlin 中这个非常棒的特性,很遗憾,在 Java 当中也没有,而且即使通过 SAM 函数也无法实现。
所以,这里我不得不付出一点语法特性的代价,将 Kotlin 这种定义所属类上下文的特性改成了传递参数的方式。也因为这个原因,新版 PermissionX 的语法无法做到和上一个版本百分百兼容,而是要稍微做出一点点修改。
那么新版的 PermissionX 中实现和刚才同样的功能需要这样写:
PermissionX.init(this)
.permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
.onExplainRequestReason { scope, deniedList ->
scope.showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白")
}
.onForwardToSettings { scope, deniedList ->
scope.showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白")
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
}
}
复制代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
可以看到,只是在 onExplainRequestReason() 和 onForwardToSettings() 方法的 Lambda 表达式参数列表中增加了一个 scope 参数,然后调用解释权限申请原因对话框的时候,前面也要加上 scope 对象,仅此一点点变化,其他用法部分和之前是完全一模一样的。
而 Kotlin 在用法层面做出这一点点的牺牲,带来的却是 Java 语言的全面支持,使用 Java 实现同样的功能只需要这样写:
PermissionX.init(this)
.permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
.onExplainRequestReason(new ExplainReasonCallbackWithBeforeParam() {
@Override
public void onExplainReason(ExplainScope scope, List<String> deniedList, boolean beforeRequest) {
scope.showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白");
}
})
.onForwardToSettings(new ForwardToSettingsCallback() {
@Override
public void onForwardToSettings(ForwardScope scope, List<String> deniedList) {
scope.showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白");
}
})
.request(new RequestCallback() {
@Override
public void onResult(boolean allGranted, List<String> grantedList, List<String> deniedList) {
if (allGranted) {
Toast.makeText(MainActivity.this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "您拒绝了如下权限:" + deniedList, Toast.LENGTH_SHORT).show();
}
}
});
复制代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
单纯从两种语言上来对比,Kotlin 版的代码肯定是要远比 Java 版的更简洁,但是很多朋友或许就是更加习惯 Java 的这种语法结构吧,看起来可能也更加亲切一些。
# 支持Android 11
目前 Android 11 的 Beta 版本已在上周四正式发布了,我这次也算是走在了时代的前沿,第一时间研究了 Android 11 中的各种新特性。
其中,权限相关的部分有了较大的变化,不过大家也不用担心,需要我们开发者进行适配的地方并不多,只是你应该了解这些变化。
首先,那个让无数开发者极其讨厌的 “拒绝并不再询问” 选项没有了。但是别高兴的太早,Android 11 只是将它换成了另外一种展现形式。假如应用程序申请的某个权限被用户拒绝了两次,那么 Android 系统会自动将其视为 “拒绝并不再询问” 来处理。
另外权限申请对话框现在允许取消了,如果用户取消了权限对话框,将会视为一次拒绝。
Android 11 中还引入了权限过期的机制,本来用户授予了应用程序某个权限,该权限会一直有效,现在如果某应用程序很长时间没有启动,Android 系统会自动收回用户授予的权限,下次启动需要重新请求授权。
另外,Android 11 针对摄像机、麦克风、地理定位这 3 种权限提供了单次授权的选项。因为这 3 种权限都是属于隐私敏感的权限,如果像过去一样用户同意一次就代表永久授权,可能某些恶意应用会无节制地采集用户信息。在 Android 11 中请求摄像机权限,界面如下图所示。
可以看到,图中多了一个 “仅限这一次” 的选项。如果用户选择了这个选项,那么在整个应用程序的生命周期内,我们都是可以获取到摄像机数据的。但是当下次启动程序时,则需要再次请求权限。
以上部分就是 Android 11 中权限相关的主要变化,你会发现,这些变化其实并没有影响到我们的代码编写,也不用做什么额外的适配,所以只需要了解一下就行了。
不过接下来的部分,就是我们需要进行适配的地方了。
Android 10 系统首次引入了 android:foregroundServiceType 属性,如果你想要在前台 Service 中获取用户的位置信息,那么必须在 AndroidManifest.xml 中进行以下配置声明:
<manifest>
...
<service ...
android:foregroundServiceType="location" />
</manifest>
复制代码
2
3
4
5
6
7
而在 Android 11 系统中,这个要求扩展到了摄像机和麦克风权限。也就是说,如果你想要在前台 Service 中获取设备的摄像机和麦克风数据,那么也需要在 AndroidManifest.xml 中进行声明:
<manifest>
...
<service ...
android:foregroundServiceType="location|camera|microphone" />
</manifest>
复制代码
2
3
4
5
6
7
接下来再来看另外一个需要适配的地方。
Android 10 系统中引入了一个新的权限:ACCESS_BACKGROUND_LOCATION,用于允许应用程序在后台请求设备的位置信息。不过这个权限是不可以单独申请的,而是要和 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 一起申请才行。这个也很好理解,怎么可能连前台请求位置信息都没同意呢,就允许在后台请求位置信息了。
在 Android 10 系统中,如果我们同时申请前台和后台定位权限,那么将会出现如下界面:
可以看到,界面上的选项有些不同,“始终允许” 表示同时允许了前台和后台定位权限,“仅在使用此应用时允许” 表示只允许前台定位权限,“拒绝” 表示都不允许。
但是如果我们在 Android 11 系统中同时申请前台和后台定位权限会怎么样呢?很遗憾地告诉你,会崩溃。
因为 Android 11 系统要求,ACCESS_BACKGROUND_LOCATION 权限必须单独申请,并且在那之前,应用程序还必须已经获得了 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限才行。
这个规则其实 PermissionX 是可以不用考虑的,如果开发者在 Android 11 中同时申请前台和后台定位权限 ,那么就让系统直接抛出异常也是合理的,因为这种请求方式违反了 Android 11 的规则。
然而为了让开发者更方便地使用 PermissionX,减少这种差异化编程的的场景,我还是决定对 Android 11 的这个新规则进行适配。
具体思路也是比较简单的,如果应用程序同时申请了前台和后台定位权限,那么就先忽略后台定位权限,只申请前台定位以及其他权限,等所有权限都申请完毕后再单独去申请后台定位权限。
看上去很简单是不是?可是当我具体去实现的时候差点没把我累死,同时也暴露出了 PermissionX 的扩展性设计得非常糟糕的问题。
其实本来我一直觉得 PermissionX 的代码写得非常出色,还鼓励大家去阅读源码,然而这次为了兼容 Android 11 我才发现,有多个地方的耦合性太高,牵一发而动全身,导致难以扩展功能。
PermissionX 中有很多可以注册回调监听的地方,权限被拒绝时有回调,权限被永久拒绝时有回调,权限申请结束时有回调。而在代码逻辑中去通知这些回调的地方就更多了,传入一个空权限列表是不会进行权限请求的,直接回调结束。传入的权限列表如果全部都已经授权了,也会直接回调结束。还有点击解释权限申请原因对话框上的取消按钮,也要终止后续的权限请求。
以上还只是处理了一些边界情况,都不是正式的权限请求流程,正式请求之后的回调逻辑就更多了。
那么如此复杂的回调逻辑带来了一个什么问题?我很难找到一个切入点去判断除了后台定位权限之外的其他权限都处理完了(那么多的回调点都需要处理),然后再单独去申请后台定位权限。另外,后台定位权限还要复用之前的逻辑,这样每个回调的地方我都要知道当前是在请求非后台定位权限,还是后台定位权限(否则将无法知道接下来应该是去请求后台定位权限,还是结束请求回调给开发者)。
我大概尝试了两种不同的 if else 设计思路来实现兼容 Android 11 系统的功能,最终都失败了。写到后面逻辑越来越复杂,改了这个 bug 出现那个 bug,实在无法继续。
最终我决定将 PermissionX 的整体架构全部推翻重来。这是一个不容易的决定,但是既然已经知道 PermissionX 的扩展性设计得非常糟糕,早晚我都是要解决这个问题的。
新版 PermissionX 的整体架构改成了链式任务的执行模式,根据不同的权限类型将请求分成两种任务,权限的请求以及结果的回调都是封装在任务当中的。当一个任务执行结束之后会判断是否还有下一个任务要执行,如果有的话就执行下一个任务,没有的话就回调结束。示意图如下所示:
部分链式任务的实现代码如下:
public class RequestChain {
private BaseTask headTask;
private BaseTask tailTask;
public void addTaskToChain(BaseTask task) {
if (headTask == null) {
headTask = task;
}
if (tailTask != null) {
tailTask.next = task;
}
tailTask = task;
}
public void runTask() {
headTask.request();
}
}
复制代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
这里我使用了链表这种数据结构来实现,每当新增一个任务的时候,就将它添加到链表的尾部。执行任务的时候则从第一个任务开始执行,然后依次向后,直到所有任务执行结束才回调给开发者。
然后在请求权限的 request() 方法中,我构建了这样一条任务链:
public void request(RequestCallback callback) {
requestCallback = callback;
RequestChain requestChain = new RequestChain();
requestChain.addTaskToChain(new RequestNormalPermissions(this));
requestChain.addTaskToChain(new RequestBackgroundLocationPermission(this));
requestChain.runTask();
}
复制代码
2
3
4
5
6
7
8
9
10
11
12
可以看到,这里先是创建了 RequestChain 的实例,然后向链表中添加一个 RequestNormalPermissions 任务用于请求普通的权限,又添加了一个 RequestBackgroundLocationPermission 任务用于请求后台定位权限,接着调用 runTask() 方法就可以从链表头部依次向后执行任务了。
现在,当你使用 PermissionX 来进行权限处理,可以完全不用理会 Android 11 上的权限机制差异,所有判断逻辑 PermissionX 都会在内部帮你处理好。假如你同时请求了前台和后台定位权限,在 Android 10 系统中会将它们一起申请,在 Android 11 系统中会将它们分开申请,在 Android 9 或以下系统,则不会去申请后台定位权限,因为那个时候还没有这个权限。
另外,使用这种链式任务的执行模式之后,PermissionX 未来的扩展性会变得非常好。因为除了上述我们讨论的权限之外,Android 系统还有一些更加特殊的权限,比如悬浮窗权限。这种权限是不可以调用代码来进行申请的,而是要跳转到一个专门的设置界面,提醒用户手动开启。而现在的 PermissionX,想要支持这种权限,其实只需要再添加一个新的任务就行了。当然,这个功能是相对比较靠后的版本计划,下一个版本的重点还是自定义权限提示对话框样式的功能。
# 如何升级
关于 PermissionX 新版本的内容变化就介绍到这里,升级的方式非常简单,改一下 dependencies 当中的版本号即可:
dependencies {
...
implementation 'com.permissionx.guolindev:permissionx:1.2.2'
}
复制代码
2
3
4
5
6
尤其是还在使用 Java 语言的开发者们,这次的版本更新是非常值得一试的。
另外,如果你的项目还没有升级到 AndroidX,那么可以使用 Permission-Support 这个版本,用法都是一模一样的,只是 dependencies 中的依赖声明需要改成:
dependencies {
...
implementation 'com.permissionx.guolindev:permission-support:1.2.2'
}
复制代码
2
3
4
5
6
最后附上 PermissionX 的开源库地址,欢迎大家 star 和 fork。
github.com/guolindev/P… (opens new window)
本篇文章的内容还是比较充实的,既讲了 PermissionX 的新版用法,又讲了一些 Kotlin 的知识,还讲了 Android 11 的权限变更,当然最后还有新版 PermissionX 的架构设计思路,希望大家都有学到一些知识吧。
下个版本中,PermissionX 将会加入自定义权限提醒对话框的功能,详情请参阅 PermissionX 重磅更新,支持自定义权限提醒对话框 (opens new window) 。
如果想要学习 Kotlin 和最新的 Android 知识,可以参考我的新书 《第一行代码 第 3 版》,点击此处查看详情 (opens new window)。
作者:郭霖 链接:https://juejin.cn/post/6981034003637731358 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。