Android11 无Root 访问data目录实现、Android11访问data目录、Android11解除data目录限制、Android11 data空白解决

Android11 无Root 访问data目录 实现

  • 正文开始
  • 关于Android11权限变化
  • 作为普通安卓用户该如何方便快速地访问Android/data目录
  • 开发者该如何实现无ROOT访问Data目录
  • 正式开始解决Android/data问题
  • 获取某个文件目录的权限
  • 回调并永久保存某个目录的权限
  • 通过DocumentFile Api访问目录
  • 实现遍历或管理Android/data文件目录
  • 重要的坑:为什么不直接使用路径Path来实现文件浏览呢?
  • 解决方案
  • SAF方案缺点
  • 放大招,ROOT权限直接解锁后带权访问Data目录
  • 结语
  • 封装好的工具类

正文开始

关于Android11权限变化

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

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

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

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

软件下载

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

App界面预览

在这里插入图片描述

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

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

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission
        android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />

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

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

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

        android:preserveLegacyExternalStorage="true"
        android:requestLegacyExternalStorage="true"

相关判断

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

获取权限

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

正式开始解决Android/data问题

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

获取某个文件目录的权限

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

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

调用后的示意图:


在这里插入图片描述

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

    //返回授权状态
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        Uri uri;

        if (data == null) {
            return;
        }

        if (requestCode == REQUEST_CODE_FOR_DIR && (uri = data.getData()) != null) {
            getContentResolver().takePersistableUriPermission(uri, data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION
                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION));//关键是这里,这个就是保存这个目录的访问权限
            PreferencesUtil.saveString(MainActivity.this, statusHolder.path + "授权", "true");//我自己处理的逻辑,大家不用管

        }

    }

权限授权并永久保存成功


在这里插入图片描述

通过DocumentFile Api访问目录

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

DocumentFile documentFile = DocumentFile.fromTreeUri(context, Uri.parse(fileUriUtils.changeToUri3(path)));
//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进行遍历


    //遍历示例,不进行额外逻辑处理
    void getFiles(DocumentFile documentFile) {
        Log.d("文件:", documentFile.getName());
        if (documentFile.isDirectory()) {
            for (DocumentFile file : documentFile.listFiles()) {
                Log.d("子文件", file.getName());
                if (file.isDirectory()) {
                    getFiles(file);//递归调用
                }
            }

        }
    }

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

 class file{
        String title;
        DocumentFile documentFile;

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public DocumentFile getDocumentFile() {
            return documentFile;
        }

        public void setDocumentFile(DocumentFile documentFile) {
            this.documentFile = documentFile;
        }
    }

    MainActivity{
        //加载数据
        void getFiles(DocumentFile documentFile) {
            ArrayList<file> arrayList = new ArrayList<>();
            if (documentFile.isDirectory()) {
                for (DocumentFile documentFile_inner : documentFile.listFiles()) {
                    file file = new file();
                    file.setTitle(documentFile_inner.getName());
                    file.setDocumentFile(documentFile_inner);
                }
            }
        }
        }
    }

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

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

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

我实现的文件管理(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()方法:

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

很显然,可以了!可以生成正确的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

结语

以上就是我的解决方案了,已经完全解决Android11系统访问Android/data的问题,有问题可以留言哦,我看到会回复的,如果您有更好的解决的方案请在评论区留言,我会及时更新上去。

当然,这个方案肯定会有些不如意,但是这已经是没方案中的最好的办法,毕竟谷歌限制不让你访问data目录,我们某些涉及文件管理的应用又确实需要访问,方案亲测可用,我已经按照以上方案在我的app中进行了Android11适配,算是差强人意吧。

我的App:
软件下载
欢迎各位看官下载体验。

封装好的工具类

因为个人项目还在运营不方便把全部代码都开源至GitHub,所以就放出工具类给大家使用吧。
真的超级简单呀,认真看一遍就可以上手了,都是日常操作,对于各位大佬来说就是有手就行。

public class fileUriUtils {
    public static String root = Environment.getExternalStorageDirectory().getPath() + "/";

    public static String treeToPath(String path) {
        String path2;
        if (path.contains("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary")) {
            path2 = path.replace("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A", root);
            path2 = path2.replace("%2F", "/");
        } else {
            path2 = root + textUtils.getSubString(path + "测试", "document/primary%3A", "测试").replace("%2F", "/");

        }
        return path2;
    }

    //判断是否已经获取了Data权限,改改逻辑就能判断其他目录,懂得都懂
    public static boolean isGrant(Context context) {
        for (UriPermission persistedUriPermission : context.getContentResolver().getPersistedUriPermissions()) {
            if (persistedUriPermission.isReadPermission() && persistedUriPermission.getUri().toString().equals("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata")) {
                return true;
            }
        }
        return false;
    }

    //直接返回DocumentFile
    public static DocumentFile getDocumentFilePath(Context context, String path, String sdCardUri) {
        DocumentFile document = DocumentFile.fromTreeUri(context, Uri.parse(sdCardUri));
        String[] parts = path.split("/");
        for (int i = 3; i < parts.length; i++) {
            document = document.findFile(parts[i]);
        }
        return document;
    }

    //转换至uriTree的路径
    public static String changeToUri(String path) {
        if (path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }
        String path2 = path.replace("/storage/emulated/0/", "").replace("/", "%2F");
        return "content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A" + path2;
    }

    //转换至uriTree的路径
    public static DocumentFile getDoucmentFile(Context context, String path) {
        if (path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }
        String path2 = path.replace("/storage/emulated/0/", "").replace("/", "%2F");
        return DocumentFile.fromSingleUri(context, Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A" + path2));
    }

    //转换至uriTree的路径
    public static String changeToUri2(String path) {
        String[] paths = path.replaceAll("/storage/emulated/0/Android/data", "").split("/");
        StringBuilder stringBuilder = new StringBuilder("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata");
        for (String p : paths) {
            if (p.length() == 0) continue;
            stringBuilder.append("%2F").append(p);
        }
        return stringBuilder.toString();

    }

    //转换至uriTree的路径
    public static String changeToUri3(String path) {
        path = path.replace("/storage/emulated/0/", "").replace("/", "%2F");
        return ("content://com.android.externalstorage.documents/tree/primary%3A" + path);

    }

//获取指定目录的权限
    public static void startFor(String path, Activity context, int REQUEST_CODE_FOR_DIR) {
        statusHolder.path = path;
        String uri = changeToUri(path);
        Uri parse = Uri.parse(uri);
        Intent intent = new Intent("android.intent.action.OPEN_DOCUMENT_TREE");
        intent.addFlags(
                Intent.FLAG_GRANT_READ_URI_PERMISSION
                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                        | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
                        | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, parse);
        }
        context.startActivityForResult(intent, REQUEST_CODE_FOR_DIR);

    }

//直接获取data权限,推荐使用这种方案
    public static void startForRoot(Activity context, int REQUEST_CODE_FOR_DIR) {
        Uri uri1 = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata");
//        DocumentFile documentFile = DocumentFile.fromTreeUri(context, uri1);
        String uri = changeToUri(Environment.getExternalStorageDirectory().getPath());
        uri = uri + "/document/primary%3A" + Environment.getExternalStorageDirectory().getPath().replace("/storage/emulated/0/", "").replace("/", "%2F");
        Uri parse = Uri.parse(uri);
        DocumentFile documentFile = DocumentFile.fromTreeUri(context, uri1);
        Intent intent1 = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
        intent1.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
                | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
        intent1.putExtra(DocumentsContract.EXTRA_INITIAL_URI, documentFile.getUri());
        context.startActivityForResult(intent1, REQUEST_CODE_FOR_DIR);

    }

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容