一、Android 8.0 权限简介

在 Android 8.0 行为变更说明里面,有如下一段表述:

权限

在 Android 8.0 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。

对于针对 Android 8.0 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。

例如,假设某个应用在其清单中列出 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE。应用请求 READ_EXTERNAL_STORAGE,并且用户授予了该权限。如果该应用针对的是 API 级别 24 或更低级别,系统还会同时授予 WRITE_EXTERNAL_STORAGE,因为该权限也属于同一 STORAGE 权限组并且也在清单中注册过。如果该应用针对的是 Android 8.0,则系统此时仅会授予 READ_EXTERNAL_STORAGE;不过,如果该应用后来又请求 WRITE_EXTERNAL_STORAGE,则系统会立即授予该权限,而不会提示用户。

需要对此作出说明的是,自从 Android 6.0 引入了动态权限之后,权限这边一直就问题颇多,主要因为国内厂商的 ROM 修改,导致权限申请适配复杂,但是核心原理不变,主要 API 也没有发生变化。但是这次的调整还是会影响到一部分 apk 的安装。

二、背景概述

在产品使用过程中,突然客户反馈 apk 运行过程莫名闪退,并且只有在 8.0 出现这个问题。并没有提示 ANR 以及其他显眼的错误,经验告诉我这种情况多半是权限的问题。

同时用户还提到一个细节,只有 8.0 出现了这个问题,那就从这两个方面进行分析。

查看官方文档 8.0 行为变更,就出现以上信息。

目前,原因已知,剩下就是如何解决。

三、问题复现

关于 8.0 权限变更,主要与权限组关系比价密切。那就用存储权限组读、写存储器进行测试。

正常情况下,权限申请分为以下三部分:

1、判断是否已有对应权限

private void requestStorage() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
PackageManager.PERMISSION_GRANTED) {
requestStoragePermission();
} else {
Toast.makeText(this, "success111", Toast.LENGTH_SHORT).show();
}
}

2、权限申请

private void requestStoragePermission() {
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setCancelable(false);
dialog.setTitle("权限请求");
dialog.setMessage("请赋予 存储 权限以便执行下一步操作");
dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(MainActivity.this, "用户取消了权限赋予", Toast.LENGTH_SHORT).show();
}
});
dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_STORAGE);
}
});
dialog.show();
} else {
ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSION_REQUEST_STORAGE);
}
}

3、权限回调结果

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == PERMISSION_REQUEST_STORAGE) {
if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "success...2", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "storage permission request was denied....", Toast.LENGTH_SHORT).show();
// 用户拒绝且不再提示
}
}
}

参考两个函数:

1、读文件

private File getFileName() {
File externalCacheDir = Environment.getExternalStorageDirectory();
File resFile = new File(externalCacheDir.getAbsolutePath(), "test.txt");
Log.e(TAG, "getFileName: " + resFile.getAbsolutePath());
return resFile;
}
private void readFile() {
File fileName = getFileName();
StringBuilder sb = new StringBuilder();
try {
BufferedReader bfr = new BufferedReader(new FileReader(fileName));
String line = bfr.readLine();
while (line != null) {
sb.append(line);
sb.append("\n");
line = bfr.readLine();
}
bfr.close();
Log.d("buffer", "bufferRead: " + sb.toString());
Toast.makeText(this, "" + sb.toString(), Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}

2、写文件

private void writeFile(String msg) {
File fileName = getFileName();
try {
BufferedWriter bfw = new BufferedWriter(new FileWriter(fileName, true));
bfw.write(msg);
bfw.newLine();
bfw.flush();
bfw.close();
Toast.makeText(this, "write is success...", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(this, "" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}

四、问题分析

经过以上操作,权限赋予完成,以上代码如果运行在 8.0 以下的系统,可以正常使用读和写权限。虽然代码里只是申请了写权限,但是此时我们依然可以使用读权限。在 8.0 以后的系统,如果代码书写如上,当执行到读文件的部分,就会报错,错误信息:

java.io.FileNotFoundException: /storage/emulated/0/test.txt: open failed: EACCES (Permission denied)

目前发现,影响范围还比较小,但是我们需要提前适配以避免出现类似错误,降低影响用户体验的效果。

解决方式,就是在代码使用读权限之前,再次进行写权限申请,如果之前用户已经赋予 apk 的写权限,此时再次进行读权限申请,界面无任何提示,直接赋予读权限的。这就是 Android 8.0 修改的位置。

五、小结

在 Android 开发中,由于系统碎片化比较严重,因此,在兼容老版本的同时还需注意新版本变化,也要照顾到新版本的特性。

在问题解决分析过程中,尽量找寻官方文档,解释比较合理也比较确切。

以上。