关于Android11权限变化

谷歌在Android11及以上系统中采用了文件沙盒存储模式,导致第三方应用无法像以前一样访问Android/data目录,这是好事。但是我所不能理解的是已经获得”所有文件管理”权限的APP为何还是限制了,岂不是完全不留给清理、文件管理类软件后路?实在不应该!

作为普通安卓用户该如何方便快速地访问Android/data目录

众所周知,不能访问Android/data目录非常不方便,比如要管理QQ、微信接收到的文件、其他App下载的数据(如迅雷等等)。

现本人开发的应用已实现无Root访问Android/data目录(其中文件浏览器功能),并且可以方便地进行管理。

https://www.coolapk.com/apk/com.magicalstory.cleaner

软件下载

欢迎安卓手机用户下载使用 和 Android开发者下载预览功能的实现。

App界面预览
在这里插入图片描述

开发者该如何实现无ROOT访问Data目录

1.首先,可根据需要获取所有文件管理权限:
在清单中声明:

  1. <uses-permission android:name=“android.permission.WRITE_EXTERNAL_STORAGE” />
  2. <uses-permission android:name=“android.permission.READ_EXTERNAL_STORAGE” />
  3. <uses-permission
  4. android:name=“android.permission.MANAGE_EXTERNAL_STORAGE”
  5. tools:ignore=“ScopedStorage” />

2.动态获取读写权限,这个不用多说了吧,如果觉得麻烦可以使用郭霖大神的permissionX库
Github

关于”管理所有文件”权限
这个权限可以让你的App跟Android11以前一样,通过File API访问所有文件(除Android/data目录)

如有需要,请在清单声明不启用沙盒存储

  1. android:preserveLegacyExternalStorage=“true”
  2. android:requestLegacyExternalStorage=“true”

相关判断

  1. //判断是否需要所有文件权限
  2. if (!(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager())) {
  3. //表明已经有这个权限了
  4. }

获取权限

  1. Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
  2. startActivity(intent);

正式开始解决Android/data问题

首先,使用的方式是SAF框架(Android Storage Access Framework)
这个框架在Android4.4就引入了,如果没有了解过的话,可以百度。

获取某个文件目录的权限

方法很简单,使用android.intent.action.OPEN_DOCUMENT_TREE(调用SAF框架的文件选择器选择一个文件夹)的Intent就可以授权了
等下会放出工具类,现在看下例子:

  1. //获取指定目录的访问权限
  2. public static void startFor(String path, Activity context, int REQUEST_CODE_FOR_DIR) {
  3. statusHolder.path = path;//这里主要是我的一个状态保存类,说明现在获取权限的路径是他,大家不用管。
  4. String uri = changeToUri(path);//调用方法,把path转换成可解析的uri文本,这个方法在下面会公布
  5. Uri parse = Uri.parse(uri);
  6. Intent intent = new Intent(“android.intent.action.OPEN_DOCUMENT_TREE”);
  7. intent.addFlags(
  8. Intent.FLAG_GRANT_READ_URI_PERMISSION
  9. | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
  10. | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
  11. | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
  12. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  13. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, parse);
  14. }
  15. context.startActivityForResult(intent, REQUEST_CODE_FOR_DIR);//开始授权
  16. }

调用后的示意图:
在这里插入图片描述

回调并永久保存某个目录的权限

  1. //返回授权状态
  2. @Override
  3. protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
  4. super.onActivityResult(requestCode, resultCode, data);
  5. Uri uri;
  6. if (data == null) {
  7. return;
  8. }
  9. if (requestCode == REQUEST_CODE_FOR_DIR && (uri = data.getData()) != null) {
  10. getContentResolver().takePersistableUriPermission(uri, data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION
  11. | Intent.FLAG_GRANT_WRITE_URI_PERMISSION));//关键是这里,这个就是保存这个目录的访问权限
  12. PreferencesUtil.saveString(MainActivity.this, statusHolder.path + “授权”, “true”);//我自己处理的逻辑,大家不用管
  13. }
  14. }

权限授权并永久保存成功
在这里插入图片描述

通过DocumentFile Api访问目录

使用起来非常简单
先看看怎么生成DocumentFile对象

  1. DocumentFile documentFile = DocumentFile.fromTreeUri(context, Uri.parse(fileUriUtils.changeToUri3(path)));
  2. //changeToUri3方法是我封装好的方法,后面会用到,这个是通过path生成指定可解析URI的方法

真所谓有手就行,调用DocumentFile.fromTreeUri()方法就可以了,这个方法说的是从一个文件夹URI生成DocumentFile对象(treeUri就是文件夹URI)

当然还有其他方法:
DocumentFile.fromSingleUri();
DocumentFile.fromFile();
DocumentFile.isDocumentUri();

看名字就明白了,但是我们有的的是一个文件夹uri,当然使用这个方法来生成DocumentFile对象,不同方法生成的DocumentFile对象有不同效果,如果你用fromTreeUri生成的默认是文件夹对象,有ListFiles() 方法
DocumentFile.ListFiles()也就是列出文件夹里面的全部子文件,类似于File.listFiles()方法

然后就这样啊,得到了DocumentFile对象就可以进行骚操作了啊,比如列出子文件啊,删除文件啊,移动啊,删除啊什么的都可以,没错,Android/data目录就是这样进行操作和访问的!

实现遍历或管理Android/data文件目录

比较基础,我就不多说啦,简单讲讲实现方案和踩过的坑

1.遍历,跟普通全遍历没啥差别,但是不能通过直接传入Path进行遍历

  1. //遍历示例,不进行额外逻辑处理
  2. void getFiles(DocumentFile documentFile) {
  3. Log.d(“文件:”, documentFile.getName());
  4. if (documentFile.isDirectory()) {
  5. for (DocumentFile file : documentFile.listFiles()) {
  6. Log.d(“子文件”, file.getName());
  7. if (file.isDirectory()) {
  8. getFiles(file);//递归调用
  9. }
  10. }
  11. }
  12. }

2.实现文件管理器方案(管理Android/data目录就是这个方案)
以下仅介绍方法

  1. class file{
  2. String title;
  3. DocumentFile documentFile;
  4. public String getTitle() {
  5. return title;
  6. }
  7. public void setTitle(String title) {
  8. this.title = title;
  9. }
  10. public DocumentFile getDocumentFile() {
  11. return documentFile;
  12. }
  13. public void setDocumentFile(DocumentFile documentFile) {
  14. this.documentFile = documentFile;
  15. }
  16. }
  17. MainActivity{
  18. //加载数据
  19. void getFiles(DocumentFile documentFile) {
  20. ArrayList<file> arrayList = new ArrayList<>();
  21. if (documentFile.isDirectory()) {
  22. for (DocumentFile documentFile_inner : documentFile.listFiles()) {
  23. file file = new file();
  24. file.setTitle(documentFile_inner.getName());
  25. file.setDocumentFile(documentFile_inner);
  26. }
  27. }
  28. }
  29. }
  30. }

当列表被点击了,处理方案:

  1. public void onclick(int postion){
  2. file file = arrayList.get(postion);
  3. getFiles(file.getDocumentFile());//获取该文件夹的document对象,再把该文件夹遍历出来
  4. //然后再次显示就完事了
  5. }

以上就是模拟实现文件管理器->文件浏览功能,大家应该一目了然,只介绍方案。

我实现的文件管理(Android11上直接免root管理data目录)
在这里插入图片描述

重要的坑:为什么不直接使用路径Path来实现文件浏览呢?

对呀,很明显使用传统的通过文件的path来实现文件管理岂不是更加方便?
我也这样觉得的,在我当时在对Android11进行适配的时候为了改动小,肯定是想用这个方法来进行适配,但是根本行不通!

我们不是获取了Android/data目录的权限了吗? 明明说好的获取该目录的权限后拥有该文件夹及所有子文件的读写权限的!
我为什么不能直接通过调用changToUri把path转换成uri,再生成DocumentFile对象呢?
这样岂不是更加方便嘛? 而且SAF的文件效率比File低多了。
但是试了好几次后,我确定这样是不行的!

就算你生成的是Android/data目录下子文件的正确URI,再生成DocumentFile对象,还是不行,因为你生成的DocumentFile对象始终指向Android/data(也就是你授权过的那个目录), 无解!

刚刚开始我还以为是我生成的URI不正确,但是当我尝试再次把我想获取的子目录路径进行文件目录授权后,再用同一个URI生成DocumentFile对象却能指向正正确目录了。

看到这里大家应该懂了吧,是谷歌对没有授权的子文件夹目录进行了限制,不让你直接通过TreeUri生成正确的Docment对象,至少在Android/data目录是这样的。

现在是不是觉得谷歌官方解释: 获取该目录的权限后拥有该文件夹及所有子文件的读写权限的!
是放屁?确实是!

解决方案

既然我们不能直接生成不了已授权目录的子目录DocumentFile对象,那我能不能试试直接对应子路径生成DocumentFile对象(非treeUri),我们试试用fromSingleUri()方法:

  1. //根据路径获得document文件
  2. public static DocumentFile getDoucmentFile(Context context, String path) {
  3. if (path.endsWith(“/”)) {
  4. path = path.substring(0, path.length() – 1);
  5. }
  6. String path2 = path.replace(“/storage/emulated/0/”, “”).replace(“/”, “%2F”);
  7. return DocumentFile.fromSingleUri(context, Uri.parse(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A” + path2));
  8. }

很显然,可以了!可以生成正确的DocumentFile对象了,我们又可以用它来做一些好玩的东西了,比如直接通过path生成DocumentFile对象对某个文件获取大小啊、判断存在状态啊,等等。
这个Android11上Android/data受限后,我觉得这个是很好的解决方案了,毕竟可以实现无Root访问并实现管理。

SAF方案缺点

很显然,通过SAF文件存储框架访问文件,速度和效率远远低于File API,因为SAF本来用途就不是用来解决Android11/data目录文件访问的。

但是对于一些涉及文件管理类的App来说目前这个算是*全或较优的解决方案了。

放大招,ROOT权限直接解锁后带权访问Data目录

通过ROOT权限执行
“chmod -R 777 /storage/emulated/0/Android/data”
命令就可以解锁Android/data目录,注意:不可逆。

至于怎么通过ROOT权限访问目录,就需要参考MT文件管理器或张海大神开源的文件管理器了

Github
Github:https://github.com/zhanghai/MaterialFiles

参考https://blog.csdn.net/qq_17827627/article/details/113931692

封装好的工具类

真的超级简单呀,认真看一遍就可以上手了,都是日常操作,希望对大家有帮助。

  1. public class fileUriUtils {
  2. public static String root = Environment.getExternalStorageDirectory().getPath() + “/”;
  3. public static String treeToPath(String path) {
  4. String path2;
  5. if (path.contains(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary”)) {
  6. path2 = path.replace(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A”, root);
  7. path2 = path2.replace(“%2F”, “/”);
  8. } else {
  9. path2 = root + textUtils.getSubString(path + “测试”, “document/primary%3A”, “测试”).replace(“%2F”, “/”);
  10. }
  11. return path2;
  12. }
  13. //判断是否已经获取了Data权限,改改逻辑就能判断其他目录,懂得都懂
  14. public static boolean isGrant(Context context) {
  15. for (UriPermission persistedUriPermission : context.getContentResolver().getPersistedUriPermissions()) {
  16. if (persistedUriPermission.isReadPermission() && persistedUriPermission.getUri().toString().equals(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata”)) {
  17. return true;
  18. }
  19. }
  20. return false;
  21. }
  22. //直接返回DocumentFile
  23. public static DocumentFile getDocumentFilePath(Context context, String path, String sdCardUri) {
  24. DocumentFile document = DocumentFile.fromTreeUri(context, Uri.parse(sdCardUri));
  25. String[] parts = path.split(“/”);
  26. for (int i = 3; i < parts.length; i++) {
  27. document = document.findFile(parts[i]);
  28. }
  29. return document;
  30. }
  31. //转换至uriTree的路径
  32. public static String changeToUri(String path) {
  33. if (path.endsWith(“/”)) {
  34. path = path.substring(0, path.length() – 1);
  35. }
  36. String path2 = path.replace(“/storage/emulated/0/”, “”).replace(“/”, “%2F”);
  37. return “content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A” + path2;
  38. }
  39. //转换至uriTree的路径
  40. public static DocumentFile getDoucmentFile(Context context, String path) {
  41. if (path.endsWith(“/”)) {
  42. path = path.substring(0, path.length() – 1);
  43. }
  44. String path2 = path.replace(“/storage/emulated/0/”, “”).replace(“/”, “%2F”);
  45. return DocumentFile.fromSingleUri(context, Uri.parse(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A” + path2));
  46. }
  47. //转换至uriTree的路径
  48. public static String changeToUri2(String path) {
  49. String[] paths = path.replaceAll(“/storage/emulated/0/Android/data”, “”).split(“/”);
  50. StringBuilder stringBuilder = new StringBuilder(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata”);
  51. for (String p : paths) {
  52. if (p.length() == 0) continue;
  53. stringBuilder.append(“%2F”).append(p);
  54. }
  55. return stringBuilder.toString();
  56. }
  57. //转换至uriTree的路径
  58. public static String changeToUri3(String path) {
  59. path = path.replace(“/storage/emulated/0/”, “”).replace(“/”, “%2F”);
  60. return (“content://com.android.externalstorage.documents/tree/primary%3A” + path);
  61. }
  62. //获取指定目录的权限
  63. public static void startFor(String path, Activity context, int REQUEST_CODE_FOR_DIR) {
  64. statusHolder.path = path;
  65. String uri = changeToUri(path);
  66. Uri parse = Uri.parse(uri);
  67. Intent intent = new Intent(“android.intent.action.OPEN_DOCUMENT_TREE”);
  68. intent.addFlags(
  69. Intent.FLAG_GRANT_READ_URI_PERMISSION
  70. | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
  71. | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
  72. | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
  73. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  74. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, parse);
  75. }
  76. context.startActivityForResult(intent, REQUEST_CODE_FOR_DIR);
  77. }
  78. //直接获取data权限,推荐使用这种方案
  79. public static void startForRoot(Activity context, int REQUEST_CODE_FOR_DIR) {
  80. Uri uri1 = Uri.parse(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata”);
  81. // DocumentFile documentFile = DocumentFile.fromTreeUri(context, uri1);
  82. String uri = changeToUri(Environment.getExternalStorageDirectory().getPath());
  83. uri = uri + “/document/primary%3A” + Environment.getExternalStorageDirectory().getPath().replace(“/storage/emulated/0/”, “”).replace(“/”, “%2F”);
  84. Uri parse = Uri.parse(uri);
  85. DocumentFile documentFile = DocumentFile.fromTreeUri(context, uri1);
  86. Intent intent1 = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
  87. intent1.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
  88. | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
  89. | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
  90. | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
  91. intent1.putExtra(DocumentsContract.EXTRA_INITIAL_URI, documentFile.getUri());
  92. context.startActivityForResult(intent1, REQUEST_CODE_FOR_DIR);
  93. }
  94. }