【Gradle-17】动态删除清单文件中的某个权限

2,198 阅读5分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

1、前言

有读者留言,希望可以写个插件,实现动态删除某个清单文件权限的功能。

一般来说,不会去删除清单文件中的某个权限,但是随着厂商对安全隐私合规检测的越来越严格,对于权限的申请和调用也要求越来越高,也需要有合理的说明。

但是厂商给的检测报告中,或许有的权限实际上是用不到的,它可能是某个依赖带进来的,然后该权限涉及的相关功能也是用不上的,但是依赖又没有提供裁剪版本,所以便想着看能不能把这个权限给删除了,不打进包里。

那么接下来就带大家一起看下怎么去实现这个功能。

2、权限

Android 6.0之后权限分为两种:

  1. 安装时权限:也叫一般权限,比如网络(android.permission.INTERNET),这种只需要在manifest文件中声明就可以用了,不需要额外去动态申请;
  2. 运行时权限:也叫危险权限,比如相机(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为例,在清单文件中添加如下代码:

  1. 声明xmlns:tools命名空间
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
  1. 添加想要移除的权限,并设置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处理阶段之后去执行,具体操作也很简单。

  1. 首先获取manifest文件;
  2. 然后读取manifest文件;
  3. 用replaceAll来删除指定权限;
  4. 最后再写回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']
}

8、GitHub

github.com/yechaoa/Gra…

9、相关文档