本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
1、前言
有读者留言,希望可以写个插件,实现动态删除某个清单文件权限的功能。
一般来说,不会去删除清单文件中的某个权限,但是随着厂商对安全隐私合规检测的越来越严格,对于权限的申请和调用也要求越来越高,也需要有合理的说明。
但是厂商给的检测报告中,或许有的权限实际上是用不到的,它可能是某个依赖带进来的,然后该权限涉及的相关功能也是用不上的,但是依赖又没有提供裁剪版本,所以便想着看能不能把这个权限给删除了,不打进包里。
那么接下来就带大家一起看下怎么去实现这个功能。
2、权限
Android 6.0之后权限分为两种:
安装时权限
:也叫一般权限,比如网络(android.permission.INTERNET),这种只需要在manifest文件中声明就可以用了,不需要额外去动态申请;运行时权限
:也叫危险权限,比如相机(android.permission.CAMERA),这种就需要运行时申请且同意之后才可以用;
声明权限(uses-permission):
<manifest ...>
<uses-permission android:name="android.permission.CAMERA"/>
<application ...>
...
</application>
</manifest>
请求运行时权限的工作流:
3、方案一
上面我们提到声明权限需要用到uses-permission
标签,uses-permission标签中有个tools:node
属性。
tools:node
的作用是在清单合并过程中移除某个元素,比如 。
tools:node
属性正好符合我们的诉求,在清单文件合并时把依赖里面某个权限给删除了。
3.1、实践
在示例项目GradleX里依赖了阿里云的音视频SDK,其中就带进来了许多权限
并且也确实打进包里了,比如录音权限android.permission.RECORD_AUDIO
3.2、使用
我们以录音权限android.permission.RECORD_AUDIO
为例,在清单文件中添加如下代码:
- 声明
xmlns:tools
命名空间
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
- 添加想要移除的权限,并设置
tools:node
属性
<uses-permission
android:name="android.permission.RECORD_AUDIO"
tools:node="remove"/>
然后来编译看看清单文件中还有没有。
3.3、效果
从上图可以看到,编译后清单文件中已经搜不到RECORD_AUDIO
权限了,已经被删除了。
3.4、小结
通过在manifest清单文件中,对想要移除的权限添加tools:node="remove"
,即可实现对某个权限的动态删除,配置起来也非常简单,推荐使用。
不过本着学习的态度,接下来再看下通过Gradle
是如何实现的。
4、方案二
4.1、思路
在日常开发中,如果大家有留意编译日志,你会发现在编译中会有manifest
相关的Task执行,比如:
又或者背过八股文「Android打包流程」的话,应该知道,manifest作为一个配置文件并不是直接塞到apk文件里面的,是会先处理合并和文件里面的占位符啊等等。
所以我们的思路就是在处理manifest的时候去修改最终的文件。
4.2、实操
代码如下:
applicationVariants.configureEach { variant ->
variant.outputs.each { output ->
output.processManifest.doLast {
// 获取manifest文件
def manifestOutFile = output.processResourcesProvider.get().getManifestFile()
// 读取manifest文件
def manifestContent = manifestOutFile.getText()
// 删除指定权限
manifestContent = manifestContent.replaceAll('android.permission.RECORD_AUDIO', '')
// 再写回manifest文件
manifestOutFile.write(manifestContent)
}
}
}
上面这段代码会在构建每个变体的manifest处理阶段之后去执行,具体操作也很简单。
- 首先获取manifest文件;
- 然后读取manifest文件;
- 用replaceAll来删除指定权限;
- 最后再写回manifest文件;
4.3、效果
然后我们来编译看看效果
正常编译运行,然后也确实没有RECORD_AUDIO权限了。
但是发现权限列表里有一个奇怪的东西:
<uses-permission
android:name="@string/0x18" />
@string/0x18
,就这个东西曾经让我们在上架华为应用市场的时候被拒了,我记得好像说是配置文件异常吧,当时还拉了华为的技术同学一起排查,最后才定位到是这个原因。
这是因为我们上面把要删除的权限替换成空字符串了,所以我们要修改一下替换规则,替换成INTERNET即可。
把
replaceAll('android.permission.RECORD_AUDIO', '')
改成
replaceAll('android.permission.RECORD_AUDIO', 'android.permission.INTERNET')
虽然最后manifest文件里面会有多个INTERNET权限,但是对实际程序运行不会有任何影响。
5、问题
那么问题来了,如果需要申请的权限被删除了,或者说在代码中申请了没有在manifest文件注册的权限,会有什么问题?
如果是一般权限,会有运行时异常,相信大家都遇到过SecurityException
,这种只要按照提示去声明一下就好了。
那如果是运行时权限呢?下面我们来测试一下。
5.1、测试
还是以RECORD_AUDIO权限为例:
private fun requestPermission() {
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 1000)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 1000) {
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
ToastUtil.show("权限申请成功")
} else {
ToastUtil.show("权限申请失败")
}
}
}
5.2、结果
直接是toast申请失败了(PERMISSION_DENIED = -1),也没有授予权限的提示弹窗:
正常弹窗应该是这样的:
6、总结
虽然删除运行时权限不会造成程序崩溃,但是可能会有上面例子中的异常Toast提示导致给用户产生困扰,而且也可能会阻断业务逻辑导致功能不可用,所以在使用时不管是那种方案,还是要谨慎一些,做好充分的调研和测试,在做好隐私合规的同时,也要确保良好的用户体验。
7、最后
虽然方案一非常简单,不过还是用方案二写了个插件,大家也可以学习实践一波,这个插件已经发布远端了,按照下面三步走,即可使用。
Step 1. Add the JitPack repository to your build file
repositories {
...
maven { url 'https://jitpack.io' }
}
Step 2. Add the dependency
dependencies {
classpath('com.github.yechaoa.GradleX:plugin:1.8')
}
Step 3. Add the Plugin Id to your build file and configure the gradleX{ } dsl
plugins {
id 'com.yechaoa.plugin.gradleX'
}
gradleX {
printDependencies = false
analysisSo = true
checkSnapshot = true
blockSnapshot = false
permissionsToRemove = ['android.permission.RECORD_AUDIO','android.permission.WRITE_EXTERNAL_STORAGE']
}