Android11 無(wú)Root 訪問(wèn)data目錄實(shí)現(xiàn)、Android11訪問(wèn)data目錄、Android11解除data目錄限制、Android11 data空白解決

Android11 無(wú)Root 訪問(wèn)data目錄 實(shí)現(xiàn)

  • 正文開(kāi)始
  • 關(guān)于Android11權(quán)限變化
  • 作為普通安卓用戶該如何方便快速地訪問(wèn)Android/data目錄
  • 開(kāi)發(fā)者該如何實(shí)現(xiàn)無(wú)ROOT訪問(wèn)Data目錄
  • 正式開(kāi)始解決Android/data問(wèn)題
  • 獲取某個(gè)文件目錄的權(quán)限
  • 回調(diào)并永久保存某個(gè)目錄的權(quán)限
  • 通過(guò)DocumentFile Api訪問(wèn)目錄
  • 實(shí)現(xiàn)遍歷或管理Android/data文件目錄
  • 重要的坑:為什么不直接使用路徑Path來(lái)實(shí)現(xiàn)文件瀏覽呢?
  • 解決方案
  • SAF方案缺點(diǎn)
  • 放大招,ROOT權(quán)限直接解鎖后帶權(quán)訪問(wèn)Data目錄
  • 結(jié)語(yǔ)
  • 封裝好的工具類

正文開(kāi)始

關(guān)于Android11權(quán)限變化

谷歌在Android11及以上系統(tǒng)中采用了文件沙盒存儲(chǔ)模式,導(dǎo)致第三方應(yīng)用無(wú)法像以前一樣訪問(wèn)Android/data目錄,這是好事。但是我所不能理解的是已經(jīng)獲得"所有文件管理"權(quán)限的APP為何還是限制了,豈不是完全不留給清理、文件管理類軟件后路?實(shí)在不應(yīng)該!

作為普通安卓用戶該如何方便快速地訪問(wèn)Android/data目錄

眾所周知,不能訪問(wèn)Android/data目錄非常不方便,比如要管理QQ、微信接收到的文件、其他App下載的數(shù)據(jù)(如迅雷等等)。

現(xiàn)本人開(kāi)發(fā)的應(yīng)用已實(shí)現(xiàn)無(wú)Root訪問(wèn)Android/data目錄(其中文件瀏覽器功能),并且可以方便地進(jìn)行管理。

軟件下載

歡迎安卓手機(jī)用戶下載使用 和 Android開(kāi)發(fā)者下載預(yù)覽功能的實(shí)現(xiàn)。

App界面預(yù)覽

在這里插入圖片描述

開(kāi)發(fā)者該如何實(shí)現(xiàn)無(wú)ROOT訪問(wèn)Data目錄

1.首先,可根據(jù)需要獲取所有文件管理權(quán)限:
在清單中聲明:

    <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.動(dòng)態(tài)獲取讀寫(xiě)權(quán)限,這個(gè)不用多說(shuō)了吧,如果覺(jué)得麻煩可以使用郭霖大神的permissionX庫(kù)
Github

關(guān)于"管理所有文件"權(quán)限
這個(gè)權(quán)限可以讓你的App跟Android11以前一樣,通過(guò)File API訪問(wèn)所有文件(除Android/data目錄)

如有需要,請(qǐng)?jiān)谇鍐温暶鞑粏⒂蒙澈写鎯?chǔ)

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

相關(guān)判斷

   //判斷是否需要所有文件權(quán)限
            if (!(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager())) {
            //表明已經(jīng)有這個(gè)權(quán)限了
            }

獲取權(quán)限

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

正式開(kāi)始解決Android/data問(wèn)題

首先,使用的方式是SAF框架(Android Storage Access Framework)
這個(gè)框架在Android4.4就引入了,如果沒(méi)有了解過(guò)的話,可以百度。

獲取某個(gè)文件目錄的權(quán)限

方法很簡(jiǎn)單,使用android.intent.action.OPEN_DOCUMENT_TREE(調(diào)用SAF框架的文件選擇器選擇一個(gè)文件夾)的Intent就可以授權(quán)了
等下會(huì)放出工具類,現(xiàn)在看下例子:

//獲取指定目錄的訪問(wèn)權(quán)限
 public static void startFor(String path, Activity context, int REQUEST_CODE_FOR_DIR) {
        statusHolder.path = path;//這里主要是我的一個(gè)狀態(tài)保存類,說(shuō)明現(xiàn)在獲取權(quán)限的路徑是他,大家不用管。
        String uri = changeToUri(path);//調(diào)用方法,把path轉(zhuǎn)換成可解析的uri文本,這個(gè)方法在下面會(huì)公布
        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);//開(kāi)始授權(quán)
    }

調(diào)用后的示意圖:


在這里插入圖片描述

回調(diào)并永久保存某個(gè)目錄的權(quán)限

    //返回授權(quán)狀態(tài)
    @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));//關(guān)鍵是這里,這個(gè)就是保存這個(gè)目錄的訪問(wèn)權(quán)限
            PreferencesUtil.saveString(MainActivity.this, statusHolder.path + "授權(quán)", "true");//我自己處理的邏輯,大家不用管

        }

    }

權(quán)限授權(quán)并永久保存成功


在這里插入圖片描述

通過(guò)DocumentFile Api訪問(wèn)目錄

使用起來(lái)非常簡(jiǎn)單
先看看怎么生成DocumentFile對(duì)象

DocumentFile documentFile = DocumentFile.fromTreeUri(context, Uri.parse(fileUriUtils.changeToUri3(path)));
//changeToUri3方法是我封裝好的方法,后面會(huì)用到,這個(gè)是通過(guò)path生成指定可解析URI的方法

真所謂有手就行,調(diào)用DocumentFile.fromTreeUri()方法就可以了,這個(gè)方法說(shuō)的是從一個(gè)文件夾URI生成DocumentFile對(duì)象(treeUri就是文件夾URI)

當(dāng)然還有其他方法:
DocumentFile.fromSingleUri();
DocumentFile.fromFile();
DocumentFile.isDocumentUri();

看名字就明白了,但是我們有的的是一個(gè)文件夾uri,當(dāng)然使用這個(gè)方法來(lái)生成DocumentFile對(duì)象,不同方法生成的DocumentFile對(duì)象有不同效果,如果你用fromTreeUri生成的默認(rèn)是文件夾對(duì)象,有ListFiles() 方法
DocumentFile.ListFiles()也就是列出文件夾里面的全部子文件,類似于File.listFiles()方法

然后就這樣啊,得到了DocumentFile對(duì)象就可以進(jìn)行騷操作了啊,比如列出子文件啊,刪除文件啊,移動(dòng)啊,刪除啊什么的都可以,沒(méi)錯(cuò),Android/data目錄就是這樣進(jìn)行操作和訪問(wèn)的!

實(shí)現(xiàn)遍歷或管理Android/data文件目錄

比較基礎(chǔ),我就不多說(shuō)啦,簡(jiǎn)單講講實(shí)現(xiàn)方案和踩過(guò)的坑

1.遍歷,跟普通全遍歷沒(méi)啥差別,但是不能通過(guò)直接傳入Path進(jìn)行遍歷


    //遍歷示例,不進(jìn)行額外邏輯處理
    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);//遞歸調(diào)用
                }
            }

        }
    }

2.實(shí)現(xiàn)文件管理器方案(管理Android/data目錄就是這個(gè)方案)
以下僅介紹方法

 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{
        //加載數(shù)據(jù)
        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);
                }
            }
        }
        }
    }

當(dāng)列表被點(diǎn)擊了,處理方案:

  public void onclick(int postion){
       file file = arrayList.get(postion);
       getFiles(file.getDocumentFile());//獲取該文件夾的document對(duì)象,再把該文件夾遍歷出來(lái)
       //然后再次顯示就完事了
   }

以上就是模擬實(shí)現(xiàn)文件管理器->文件瀏覽功能,大家應(yīng)該一目了然,只介紹方案。

我實(shí)現(xiàn)的文件管理(Android11上直接免root管理data目錄)

在這里插入圖片描述

重要的坑:為什么不直接使用路徑Path來(lái)實(shí)現(xiàn)文件瀏覽呢?

對(duì)呀,很明顯使用傳統(tǒng)的通過(guò)文件的path來(lái)實(shí)現(xiàn)文件管理豈不是更加方便?
我也這樣覺(jué)得的,在我當(dāng)時(shí)在對(duì)Android11進(jìn)行適配的時(shí)候?yàn)榱烁膭?dòng)小,肯定是想用這個(gè)方法來(lái)進(jìn)行適配,但是根本行不通!

我們不是獲取了Android/data目錄的權(quán)限了嗎? 明明說(shuō)好的獲取該目錄的權(quán)限后擁有該文件夾及所有子文件的讀寫(xiě)權(quán)限的!
我為什么不能直接通過(guò)調(diào)用changToUri把path轉(zhuǎn)換成uri,再生成DocumentFile對(duì)象呢?
這樣豈不是更加方便嘛? 而且SAF的文件效率比File低多了。
但是試了好幾次后,我確定這樣是不行的!

就算你生成的是Android/data目錄下子文件的正確URI,再生成DocumentFile對(duì)象,還是不行,因?yàn)槟闵傻腄ocumentFile對(duì)象始終指向Android/data(也就是你授權(quán)過(guò)的那個(gè)目錄), 無(wú)解!

剛剛開(kāi)始我還以為是我生成的URI不正確,但是當(dāng)我嘗試再次把我想獲取的子目錄路徑進(jìn)行文件目錄授權(quán)后,再用同一個(gè)URI生成DocumentFile對(duì)象卻能指向正正確目錄了。

看到這里大家應(yīng)該懂了吧,是谷歌對(duì)沒(méi)有授權(quán)的子文件夾目錄進(jìn)行了限制,不讓你直接通過(guò)TreeUri生成正確的Docment對(duì)象,至少在Android/data目錄是這樣的。

現(xiàn)在是不是覺(jué)得谷歌官方解釋: 獲取該目錄的權(quán)限后擁有該文件夾及所有子文件的讀寫(xiě)權(quán)限的!
是放屁?確實(shí)是!

解決方案

既然我們不能直接生成不了已授權(quán)目錄的子目錄DocumentFile對(duì)象,那我能不能試試直接對(duì)應(yīng)子路徑生成DocumentFile對(duì)象(非treeUri),我們?cè)囋囉胒romSingleUri()方法:

    //根據(jù)路徑獲得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對(duì)象了,我們又可以用它來(lái)做一些好玩的東西了,比如直接通過(guò)path生成DocumentFile對(duì)象對(duì)某個(gè)文件獲取大小啊、判斷存在狀態(tài)啊,等等。
這個(gè)Android11上Android/data受限后,我覺(jué)得這個(gè)是很好的解決方案了,畢竟可以實(shí)現(xiàn)無(wú)Root訪問(wèn)并實(shí)現(xiàn)管理。

SAF方案缺點(diǎn)

很顯然,通過(guò)SAF文件存儲(chǔ)框架訪問(wèn)文件,速度和效率遠(yuǎn)遠(yuǎn)低于File API,因?yàn)镾AF本來(lái)用途就不是用來(lái)解決Android11/data目錄文件訪問(wèn)的。

但是對(duì)于一些涉及文件管理類的App來(lái)說(shuō)目前這個(gè)算是最全或較優(yōu)的解決方案了。

放大招,ROOT權(quán)限直接解鎖后帶權(quán)訪問(wèn)Data目錄

通過(guò)ROOT權(quán)限執(zhí)行
"chmod -R 777 /storage/emulated/0/Android/data"
命令就可以解鎖Android/data目錄,注意:不可逆。

至于怎么通過(guò)ROOT權(quán)限訪問(wèn)目錄,就需要參考MT文件管理器或張海大神開(kāi)源的文件管理器了
Github

結(jié)語(yǔ)

以上就是我的解決方案了,已經(jīng)完全解決Android11系統(tǒng)訪問(wèn)Android/data的問(wèn)題,有問(wèn)題可以留言哦,我看到會(huì)回復(fù)的,如果您有更好的解決的方案請(qǐng)?jiān)谠u(píng)論區(qū)留言,我會(huì)及時(shí)更新上去。

當(dāng)然,這個(gè)方案肯定會(huì)有些不如意,但是這已經(jīng)是沒(méi)方案中的最好的辦法,畢竟谷歌限制不讓你訪問(wèn)data目錄,我們某些涉及文件管理的應(yīng)用又確實(shí)需要訪問(wèn),方案親測(cè)可用,我已經(jīng)按照以上方案在我的app中進(jìn)行了Android11適配,算是差強(qiáng)人意吧。

我的App:
軟件下載
歡迎各位看官下載體驗(yàn)。

封裝好的工具類

因?yàn)閭€(gè)人項(xiàng)目還在運(yùn)營(yíng)不方便把全部代碼都開(kāi)源至GitHub,所以就放出工具類給大家使用吧。
真的超級(jí)簡(jiǎn)單呀,認(rèn)真看一遍就可以上手了,都是日常操作,對(duì)于各位大佬來(lái)說(shuō)就是有手就行。

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 + "測(cè)試", "document/primary%3A", "測(cè)試").replace("%2F", "/");

        }
        return path2;
    }

    //判斷是否已經(jīng)獲取了Data權(quán)限,改改邏輯就能判斷其他目錄,懂得都懂
    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;
    }

    //轉(zhuǎn)換至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;
    }

    //轉(zhuǎn)換至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));
    }

    //轉(zhuǎn)換至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();

    }

    //轉(zhuǎn)換至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);

    }

//獲取指定目錄的權(quán)限
    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權(quán)限,推薦使用這種方案
    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);

    }

}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容