Android R 如何訪問Android/data目錄混驰?

前言

Android R上分區(qū)存儲的限制得到進一步加強,無論APP的targetsdkversion是多少,都將無法訪問Android/data和Android/obb這二個應(yīng)用私有目錄渊季。這無疑對會部分APP的業(yè)務(wù)場景及用戶體驗造成沖擊,典型的如下

  • 文件管理類軟件:微信罚渐、QQ傳輸?shù)奈募o法展示給用戶以便捷使用
  • 垃圾清理類軟件:清理緩存功能受阻

“你有你的張良計却汉,我有我的過墻梯”,現(xiàn)市面上文件管理類軟件(如MT管理器)已解決上述系統(tǒng)限制荷并,本文將淺析其實現(xiàn)方案合砂,并主要分析以下2個問題:

  • SAF是通過何種方式訪問文件系統(tǒng)的,MediaStore API ? File API ? Native Code 源织?
  • SAF為何能訪問Android/data目錄

實現(xiàn)方案

其實現(xiàn)方案很簡單翩伪,就是通過Intent ACTION_OPEN_DOCUMENT_TREE,啟動SAF讓用戶授權(quán)訪問Android/data目錄谈息,屬于官方公開的方法缘屹。
前提是APP的targetsdkversion要小于30

摘自官方文檔
摘自官方文檔

文檔鏈接:
文檔訪問限制
授予對目錄內(nèi)容的訪問權(quán)限

基本使用

  1. 通過Intent啟動SAF授權(quán)界面侠仇,注意URI的百分號編解碼(%3A和%2F)轻姿,別隨意替換,否則SAF無法導(dǎo)航到Android/data目錄
     @TargetApi(26)
    private void requestAccessAndroidData(Activity activity){
        try {
            Uri uri = Uri.parse("content://com.android.externalstorage.documents/document/primary%3AAndroid%2Fdata");
            Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
            intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri);
            //flag看實際業(yè)務(wù)需要可再補充
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                            | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
            activity.startActivityForResult(intent, 6666);
        } catch (Exception e) {
            e.printStackTrace();
        }
    } 
   /**
     * 值必須為document uri 或者是帶document id的document tree uri
     * eg.
     * document uri:
     * "content://com.android.externalstorage.documents/document/primary%3AAndroid%2Fdata"
     *
     * document tree uri with document id:
     * content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata%2Ffoo
     */
    public static final String EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI";
授權(quán)申請
  1. 在用戶同意授權(quán)后逻炊,持久化uri權(quán)限(否則關(guān)機重啟或授權(quán)界面finish后互亮,APP就無權(quán)限訪問了),并只能通過DocumentFile進行業(yè)務(wù)操作嗅骄,F(xiàn)ile API操作是無效的胳挎,此授權(quán)只是授權(quán)uri操作,并未授權(quán)文件系統(tǒng)溺森,后續(xù)章節(jié)有說明慕爬。
 implementation "androidx.documentfile:documentfile:1.0.1"
  @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case 6666:
                if (resultCode == Activity.RESULT_OK) {
                    //persist uri 
                    getContentResolver().takePersistableUriPermission(data.getData(),
                            Intent.FLAG_GRANT_READ_URI_PERMISSION
                                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

                    //now use DocumentFile to do some file op
                    DocumentFile documentFile = DocumentFile
                            .fromTreeUri(this, data.getData());
                    DocumentFile[] files = documentFile.listFiles();
                   //補充說明下授權(quán)文件夾后窑眯,文件夾中的子文件的uri格式如下,可自行按格式拼接直接訪問子文件:
                   //content://com.android.externalstorage.documents/tree/primary%3ATest%2Ftest/document/primary%3ATest%2Ftest%2F666.mp3
                    ......
                }
                break;
            default:
                break;
        }
    }
  1. 注意這個授權(quán)用戶是可以撤回的医窿,通過點擊應(yīng)用信息界面的存儲磅甩,就會看到撤回界面,所以業(yè)務(wù)需要去動態(tài)判斷
 public boolean isGrantAndroidData(Context context) {
        for (UriPermission persistedUriPermission : context.getContentResolver().getPersistedUriPermissions()) {
            if (persistedUriPermission.getUri().toString().
                    equals("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata")) {
                return true;
            }
        }
        return false;
    }
授權(quán)撤回

拓展

通過前面二個章節(jié)姥卢,已經(jīng)介紹了實現(xiàn)方案的基本使用卷要,下面就該分析本文的亮點內(nèi)容了

  • SAF是通過何種方式訪問文件系統(tǒng)的,MediaStore API ? File API ? Native Code 独榴?
  • SAF為何能訪問Android/data目錄
存儲訪問框架(SAF)簡介

為方便后續(xù)講解僧叉,先簡單回顧下SAF

SAF架構(gòu)

APP:
com.example.photos就是我們自己的APP

System UI:
com.google.android.documentsui,一般稱作DoucmentUI棺榔,就是上文中啟動的授權(quán)界面APP瓶堕,它只是個UI殼子

DocumentProvider:
DocumentUI中數(shù)據(jù)的提供者,這個Provider可以有很多
com.android.externalstorage症歇,是本地文件系統(tǒng)的Provider

關(guān)于SAF更詳細介紹郎笆,請參考官方存儲訪問框架
經(jīng)過SAF的簡單介紹,分析目標(biāo)很明確忘晤,那就是com.android.externalstorage

SAF是通過何種方式訪問文件系統(tǒng)的

先安利幾個AOSP源碼查看網(wǎng)址:
官方的Android Code Search
國內(nèi)的AOSP XREF

PS:后文源碼鏈接都用的是XREF宛蚓,方便國內(nèi)查看

從DocumentFile#listFile入手,經(jīng)過源碼跟蹤會發(fā)現(xiàn)最終會調(diào)用 DocumentsProvider#queryChildDocuments方法

public abstract class DocumentsProvider extends ContentProvider {
 .......
 @Override
    public final Cursor query(
            Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) {
       switch (mMatcher.match(uri)) {
                ......
                case MATCH_CHILDREN:
                case MATCH_CHILDREN_TREE:
                        .......
                        return queryChildDocuments(getDocumentId(uri), projection, queryArgs);
                        ......
                default:
                    throw new UnsupportedOperationException("Unsupported Uri " + uri);
            }
        } catch (FileNotFoundException e) {
            Log.w(TAG, "Failed during query", e);
            return null;
        }      
   }
 ......
}

接下來看看com.android.externalstorage中DocumentProvider的實現(xiàn)類
ExternalStorageProvider
frameworks/base/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java

import com.android.internal.content.FileSystemProvider;
public class ExternalStorageProvider extends FileSystemProvider 

queryChildDocuments的實現(xiàn)位于其父類 FileSystemProvider

public abstract class FileSystemProvider extends DocumentsProvider {
  ......
  private Cursor queryChildDocuments(
            String parentDocumentId, String[] projection, String sortOrder,
            @NonNull Predicate<File> filter) throws FileNotFoundException {
        final File parent = getFileForDocId(parentDocumentId);
        final MatrixCursor result = new DirectoryCursor(
                resolveProjection(projection), parentDocumentId, parent);
        if (parent.isDirectory()) {
            //重點是這行
            for (File file : FileUtils.listFilesOrEmpty(parent)) {
                if (filter.test(file)) {
                    includeFile(result, null, file);
                }
            }
        } else {
            Log.w(TAG, "parentDocumentId '" + parentDocumentId + "' is not Directory");
        }
        return result;
    }
 ......
}

FileUtils#listFilesOrEmpty

    /** {@hide} */
    public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) {
        return (dir != null) ? ArrayUtils.defeatNullable(dir.listFiles())
                : ArrayUtils.EMPTY_FILE;
    }

至此设塔,第一個問題凄吏,已經(jīng)理清:
SAF的ExternalStorageProvider最終也是通過File API來訪問文件系統(tǒng)的

那么第二個問題,就很自然的來了壹置,都是File API操作竞思,為何我們的APP就不能訪問呢?

SAF為何能訪問Android/data目錄

既然钞护,SAF和我們的APP都是File API操作盖喷,那我們就去看看com.android.externalstorage屬于哪些用戶組。
adb shell 查查com.android.externalstorage進程的用戶組

#查進程ID
generic_x86_arm:/ $ ps -A|grep com.android.external
u0_a64        16233    296 1256792  85960 0                   0 S com.android.externalstorage
#查進程所屬的用戶組
generic_x86_arm:/ $ cat /proc/16233/status
Name:   externalstorage
Umask:  0077
State:  S (sleeping)
Tgid:   16233
Ngid:   0
Pid:    16233
PPid:   296
TracerPid:      0
Uid:    10064   10064   10064   10064
Gid:    10064   10064   10064   10064
FDSize: 64
#重點關(guān)注這行輸出
Groups: 1015 1077 1078 1079 9997 20064 50064

拿著這些神秘的GID在前面介紹的網(wǎng)址中一搜难咕,就會很容易的發(fā)現(xiàn)GID的定義類
android_filesystem_config.h

#define AID_SDCARD_RW 1015       /* external storage write access */
#define AID_EXTERNAL_STORAGE 1077 /* Full external storage access including USB OTG volumes */
#define AID_EXT_DATA_RW 1078      /* GID for app-private data directories on external storage */
#define AID_EXT_OBB_RW 1079       /* GID for OBB directories on external storage */
#define AID_EVERYBODY 9997        /* shared between all apps in the same profile */

其中1078和1079分別對應(yīng)Android/data和Android/obb的訪問權(quán)限
如果我們APP能通過某種方式獲取到1078和1079的用戶組權(quán)限课梳,豈不妙哉?
遺憾的是余佃,對于三方APP這是不可能的暮刃,除非是手機廠商的預(yù)置的系統(tǒng)APP

總結(jié)

  • Android R上可通過SAF獲得訪問Android/data和Android/obb目錄的權(quán)限,前提是APP targetsdkversion 小于30
  • SAF的底層實現(xiàn)ExternalStorageProvider也是通過File API來訪問文件系統(tǒng)的
  • SAF之所以能訪問Android/data和Android/obb是因為ExternalStorageProvider
    進程具有GID 1078 和1079爆土,三方APP是不可能擁有這些GID的
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末椭懊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子步势,更是在濱河造成了極大的恐慌氧猬,老刑警劉巖背犯,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異盅抚,居然都是意外死亡漠魏,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門妄均,熙熙樓的掌柜王于貴愁眉苦臉地迎上來柱锹,“玉大人,你說我怎么就攤上這事丰包〗” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵邑彪,是天一觀的道長匹层。 經(jīng)常有香客問我,道長锌蓄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任撑柔,我火速辦了婚禮瘸爽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘铅忿。我一直安慰自己剪决,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布檀训。 她就那樣靜靜地躺著柑潦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪峻凫。 梳的紋絲不亂的頭發(fā)上渗鬼,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天,我揣著相機與錄音荧琼,去河邊找鬼譬胎。 笑死,一個胖子當(dāng)著我的面吹牛命锄,可吹牛的內(nèi)容都是我干的堰乔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼脐恩,長吁一口氣:“原來是場噩夢啊……” “哼镐侯!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起驶冒,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤苟翻,失蹤者是張志新(化名)和其女友劉穎韵卤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體袜瞬,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡怜俐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了邓尤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拍鲤。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖汞扎,靈堂內(nèi)的尸體忽然破棺而出季稳,到底是詐尸還是另有隱情,我是刑警寧澤澈魄,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布景鼠,位于F島的核電站,受9級特大地震影響痹扇,放射性物質(zhì)發(fā)生泄漏铛漓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一鲫构、第九天 我趴在偏房一處隱蔽的房頂上張望浓恶。 院中可真熱鬧,春花似錦结笨、人聲如沸包晰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽伐憾。三九已至,卻和暖如春赫模,著一層夾襖步出監(jiān)牢的瞬間树肃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工嘴瓤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扫外,地道東北人。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓廓脆,卻偏偏與公主長得像筛谚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子停忿,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容