Android——Android10的分區(qū)存儲(chǔ)(Scoped Storage)

分區(qū)存儲(chǔ)介紹

在Android10以前魁衙,只要程序獲得了READ_EXTERNAL_STORAGE權(quán)限,就可以隨意讀取外部的存儲(chǔ)公有目錄湾盗。只要程序獲得了WRITE_EXTERNAL_STORAGE權(quán)限,就可以隨意在寫入外部存儲(chǔ)的公有目錄上新建文件或文件夾


Android Q之前,應(yīng)用存儲(chǔ)視圖

于是Google在Android10中提出了分區(qū)存儲(chǔ)泳唠,意在限制程序?qū)ν獠看鎯?chǔ)中公有目錄的使用。
分區(qū)存儲(chǔ)對(duì)內(nèi)部存儲(chǔ)私有目錄和外部存儲(chǔ)私有目錄都沒有影響

Android Q之后應(yīng)用存儲(chǔ)視圖

簡(jiǎn)單來說就是卿操,在Android10中警检,

  • 對(duì)于私有目錄的讀寫沒有變化,仍然可以使用File那一套害淤,且不需要任何權(quán)限扇雕。
  • 對(duì)于公有目錄的讀寫,則必須使用MediaStore提供的API或是SAF(存儲(chǔ)訪問框架)

在后續(xù)的Android11中窥摄,沒有了Android10中的兼容模式镶奉,不能使用File I/O來讀取App外置存儲(chǔ)的目錄

使用分區(qū)存儲(chǔ)的應(yīng)用對(duì)自己創(chuàng)建的文件始終擁有讀/寫權(quán)限,無論文件是否位于應(yīng)用的私有目錄內(nèi)崭放,所以哨苛,如果應(yīng)用僅保存和訪問自己創(chuàng)建的文件,則無需請(qǐng)求獲得READ_EXTERNAL_STORAGE或WRITE_EXTERNAL_STORAGE權(quán)限

如果要訪問其他應(yīng)用創(chuàng)建的文件币砂,則需要READ_EXTERNAL_STORAGE權(quán)限建峭。并且仍然只能使用MediaStore提供的API或是SAF訪問。
這里需要注意的是决摧,MediaStore提供的API只能訪問圖片亿蒸、視頻凑兰、音頻,如果需要訪問其它任意格式的文件边锁,需要使用SAF姑食,它會(huì)調(diào)用系統(tǒng)內(nèi)置的文件瀏覽器供用戶自主選擇文件

SAF是Android在 4.4中引入的一套存儲(chǔ)訪問框架(Storage Access Framework),借助 SAF茅坛,用戶可輕松在其所有首選文檔存儲(chǔ)提供程序中瀏覽并打開文檔音半、圖像及其他文件。用戶可通過易用的標(biāo)準(zhǔn)界面贡蓖,以統(tǒng)一方式在所有應(yīng)用和提供程序中瀏覽文件曹鸠,以及訪問最近使用的文件。


SAF交互使用介紹

Android Q規(guī)定了App有兩種存儲(chǔ)空間模式視圖:Legacy View摩梧、Filtered View

  • Legacy View(兼容模式) 跟以前Android Q一樣物延,App訪問Sdcard一樣
  • Filtered View(沙箱模式)
    App只能直接訪問App-specific目錄文件,沒有權(quán)限訪問App-specific外的文件仅父。訪問其他目錄叛薯,只能通過MediaStore、SAF笙纤、或者其他App提供ContentProvider訪問

Scoped Storage的存儲(chǔ)空間

  • 公共目錄:Downloads耗溜、Documents、Pictures省容、DCIM抖拴、Movies、Music腥椒、Ringtones
    • 公共目錄的文件在App卸載后阿宅,不會(huì)刪除
    • 可以通過SAF、MediaStore接口訪問
  • App-specific目錄
    • 對(duì)于Filtered View App笼蛛,App-specific目錄只能自己直接訪問
    • App卸載洒放,數(shù)據(jù)會(huì)清除

運(yùn)行視圖

App運(yùn)行視圖

系統(tǒng)通過下列方式確定App的運(yùn)行模式:

  • App的TargetSDK>=Q,默認(rèn)為Filtered View
  • App的TargetSDK<Q滨砍,聲明了READ_EXTERNAL_STORAGE或者WRITE_EXTERNAL_STORAGE權(quán)限往湿,默認(rèn)Legacy View
  • 應(yīng)用通過AndroidManifest.xml設(shè)置 requestLegacyExternalStorage
    • true:表示兼容模式Legacy View
    • false:表示沙箱模式 Filtered View

判斷當(dāng)前App的運(yùn)行模式

判斷當(dāng)前App運(yùn)行的是什么模式,可以通過Environment提供的API進(jìn)行判斷

Environment.isExternalStorageLegacy()

MediaStore的Uri定義

MediaStore提供了下列幾種類型的訪問Uri惋戏,通過查找對(duì)應(yīng)Uri數(shù)據(jù)领追,達(dá)到訪問的目的。

獲取所有的Volume

我們還可以使用getContentUri獲取所有<volumeName>


image.png

Uri跟公共目錄關(guān)系

MediaProvider對(duì)于App存放到公共目錄文件傀蓉,通過ContentResolver insert方法中Uri來確定


image.png

權(quán)限

MediaStroe通過不同Uri,為用戶提供了增职抡、刪葬燎、改方法,權(quán)限對(duì)應(yīng)如下


image.png
  • 由上表可以看出沒在操作共享存儲(chǔ)空間時(shí)缚甩,獲取的權(quán)限不同可以對(duì)應(yīng)不同的操作共享存儲(chǔ)空間的方式
    • WRITE_EXTERNAL_STORAGE:獲取這個(gè)權(quán)限谱净,可以修改所有的app新建的文件,但是都需要授予權(quán)限
    • READ_EXTERNAL_STORAGE:獲取這個(gè)權(quán)限擅威,可以讀取所有的app新建的文件壕探,不能修改其他App新建的文件
    • 什么權(quán)限都不獲取的話,只能讀取郊丛、修改自己app新建的文件

操作共享存儲(chǔ)空間李请,讀寫公共目錄

通過Media定義的URI

新建文件(通過ContentResolver的insert接口,使用不同的Uri選擇存儲(chǔ)到不同的目錄)


    /**
     * 通過SAF創(chuàng)建文件文件夾
     *
     * @param view
     */
    public void createSAF(View view) {
        Uri uri = MediaStore.Files.getContentUri("external");
        ContentResolver contentResolver = this.getContentResolver();
        //定義path
        String path = Environment.DIRECTORY_DOWNLOADS + "/wx";
        ContentValues contentValues = new ContentValues();
        contentValues.put(MediaStore.Downloads.RELATIVE_PATH, path);
        contentValues.put(MediaStore.Downloads.DISPLAY_NAME, "wxPic");
        contentValues.put(MediaStore.Downloads.TITLE, "it's title");
        Uri resultUri = contentResolver.insert(uri, contentValues);
        if (resultUri != null) {
            Toast.makeText(this, "創(chuàng)建文件夾成功", Toast.LENGTH_SHORT).show();
        }
    }

新建一張圖片

 /**
     * 插入一張圖片
     *
     * @param view
     */

    public void insertImage(View view) {
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.moto);
        String disPlayPicName = System.currentTimeMillis() + "123.jpg";
        String mimeType = "image/jpeg";
        ContentValues contentValues = new ContentValues();
        contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, disPlayPicName);
        contentValues.put(MediaStore.Images.ImageColumns.MIME_TYPE, mimeType);
        contentValues.put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator + "wx" + File.separator);
        imageUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
        OutputStream outputStream = null;
        try {
            outputStream = getContentResolver().openOutputStream(imageUri);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
            outputStream.close();
            FileDescriptor fileDescriptor = getContentResolver().openFileDescriptor(imageUri, "r").getFileDescriptor();
        } catch (Exception e) {
            e.printStackTrace();
        }
        Toast.makeText(this, "添加圖片成功", Toast.LENGTH_SHORT).show();
    }

修改照片

   /**
     * 修改數(shù)據(jù)
     *
     * @param view
     */
    public void updateImage(View view) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, "test.jpg");
        int update = getContentResolver().update(imageUri, contentValues, null, null);
        if (update > 0) {
            Toast.makeText(this, "修改成功", Toast.LENGTH_SHORT).show();
        }
    }

查詢數(shù)據(jù)

  /**
     * 查詢數(shù)據(jù)
     *
     * @param view
     */
    public void query(View view) {
        //獲取URI
        Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        //創(chuàng)建selection
        String selection = MediaStore.Images.Media.DISPLAY_NAME + "=?";
        String[] arg = new String[]{"test.jpg"};
        Cursor cursor = getContentResolver().query(external, null, selection, arg, null);
        if (cursor != null && cursor.moveToFirst()) {
            Uri idUri = ContentUris.withAppendedId(external, cursor.getLong(0));
            Toast.makeText(this, "獲取成功" + idUri, Toast.LENGTH_SHORT).show();
            cursor.close();
        }
    }

刪除數(shù)據(jù)

 /**
     * 刪除圖片
     *
     * @param view
     */
    public void deleteImage(View view) {
        int delete = getContentResolver().delete(imageUri, null);
        if (delete > 0) {
            Toast.makeText(this, "刪除成功", Toast.LENGTH_SHORT).show();
        }
    }

關(guān)于RecoverableSecurityException異常
當(dāng)我們刪除其他應(yīng)用創(chuàng)建的資源時(shí)會(huì)報(bào)出RecoverableSecurityException異常厉熟,我們可以捕獲這個(gè)異常然后提示給與uri修改或刪除的權(quán)限

private fun deleteImage(imageUri: Uri, adapterPosition: Int) {
    var row = 0
    try {
        // Android 10+中,如果刪除的是其它應(yīng)用的Uri,則需要用戶授權(quán)
        // 會(huì)拋出RecoverableSecurityException異常
        row = contentResolver.delete(imageUri, null, null)
    } catch (securityException: SecurityException) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            val recoverableSecurityException =
                securityException as? RecoverableSecurityException
                    ?: throw securityException
            pendingDeleteImageUri = imageUri
            pendingDeletePosition = adapterPosition
            // 我們可以使用IntentSender向用戶發(fā)起授權(quán)
            requestRemovePermission(recoverableSecurityException.userAction.actionIntent.intentSender)
        } else {
            throw securityException
        }
    }

    if (row > 0) {
        Toast.makeText(this, "刪除成功", Toast.LENGTH_SHORT).show()
        pictureAdapter.deletePosition(adapterPosition)
    }
}

private fun requestRemovePermission(intentSender: IntentSender) {
    startIntentSenderForResult(intentSender, REQUEST_DELETE_PERMISSION, 
        null, 0, 0, 0, null)
}

private fun deletePendingImageUri(){
    pendingDeleteImageUri?.let {
        pendingDeleteImageUri = null
        deleteImage(it,pendingDeletePosition)
        pendingDeletePosition = -1
    }
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (resultCode == Activity.RESULT_OK &&
        requestCode == REQUEST_DELETE_PERMISSION
    ) {
        // 執(zhí)行之前的刪除邏輯
        deletePendingImageUri()
    }
}

如果想獲取Download文件夾下的某個(gè)非媒體文件怎么辦

例如PDF导盅,PDF為非媒體類文件,因此我們不能通過MediaStore來獲取揍瑟,對(duì)于這種其他類型的文件白翻,一般使用SAF來讓用戶選擇

private fun selectPdfUseSAF() {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        type = "application/pdf"
        // 我們需要使用ContentResolver.openFileDescriptor讀取數(shù)據(jù)
        addCategory(Intent.CATEGORY_OPENABLE)
    }
    startActivityForResult(intent, REQUEST_OPEN_PDF)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    when (requestCode) {
        REQUEST_OPEN_PDF -> {
            if (resultCode == Activity.RESULT_OK) {
                data?.data?.also { documentUri ->
                    val fileDescriptor =
                        contentResolver.openFileDescriptor(documentUri, "r") ?: return
                    // 現(xiàn)在,我們可以使用PdfRenderer等類通過fileDescriptor讀取pdf內(nèi)容
                    Toast.makeText(this, "pdf讀取成功", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}

如何創(chuàng)建任意類型的文件

我們也推薦使用SAF讓用戶自己去創(chuàng)建,IntentAction為:ACTION_CREATE_DOCUMENT

訪問App-specific目錄

訪問app-specific分為兩種情況绢片,一種是訪問App自身App-specific目錄滤馍,第二是訪問其他App目錄文件

App訪問自身App-specific目錄

Android Q,App如果啟動(dòng)了Filtered View底循,那么只能直接訪問自己目錄的文件:

  • Environment.getExternalStorageDirectory巢株、getExternalStoragePublicDirectory這些接口在Android Q上廢棄,App是Filtered View此叠,無法直接訪問這個(gè)目錄纯续。

  • 通過File(“/sdcard/”)訪問App是Filtered View,無法直接訪問這個(gè)目錄灭袁。

  • 獲取App-specific目錄

    • 獲取Media接口:getExternalMediaDirs
    • 獲取Cache接口:getExternalCacheDirs
    • 獲取Obb接口:getObbDirs
    • 獲取Data接口:getExternalFilesDirs

App訪問App-sepecific目錄內(nèi)部的多媒體文件

  • App自身訪問猬错,和App訪問自身的App-soecific目錄一樣
  • 其他App訪問
    • 默認(rèn)情況下,Media Scanner不會(huì)掃描App-specific里的多媒體文件茸歧,如果需要掃描通過MediaScannerConnection.scanFile添加到MediaProvider數(shù)據(jù)庫(kù)中倦炒,訪問方式和訪問共享存儲(chǔ)空間方式一樣
    • App通過創(chuàng)建ContentProvider共享出去

App訪問其他App目錄文件

App是FilteredView,其他App無法直接訪問當(dāng)前App私有目錄软瞎,需要通過以下方法:

通過SAF文件

  • App自定義DocumentsProvider
  • 訪問App通過ACTION_OPEN_DOCUMENT啟動(dòng)SAF瀏覽

實(shí)現(xiàn)FileProvider(某些手機(jī)可能有問題)

App自定義私有Provider

App訪問不同目錄的權(quán)限總結(jié)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末逢唤,一起剝皮案震驚了整個(gè)濱河市拉讯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鳖藕,老刑警劉巖魔慷,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異著恩,居然都是意外死亡院尔,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門喉誊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來邀摆,“玉大人,你說我怎么就攤上這事伍茄《绊铮” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵敷矫,是天一觀的道長(zhǎng)例获。 經(jīng)常有香客問我,道長(zhǎng)沪饺,這世上最難降的妖魔是什么躏敢? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮整葡,結(jié)果婚禮上件余,老公的妹妹穿的比我還像新娘。我一直安慰自己遭居,他們只是感情好啼器,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著俱萍,像睡著了一般端壳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上枪蘑,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天损谦,我揣著相機(jī)與錄音,去河邊找鬼岳颇。 笑死照捡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的话侧。 我是一名探鬼主播栗精,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了悲立?” 一聲冷哼從身側(cè)響起鹿寨,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎薪夕,沒想到半個(gè)月后脚草,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡寥殖,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年玩讳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嚼贡。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖同诫,靈堂內(nèi)的尸體忽然破棺而出粤策,到底是詐尸還是另有隱情,我是刑警寧澤误窖,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布叮盘,位于F島的核電站,受9級(jí)特大地震影響霹俺,放射性物質(zhì)發(fā)生泄漏柔吼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一丙唧、第九天 我趴在偏房一處隱蔽的房頂上張望愈魏。 院中可真熱鬧,春花似錦想际、人聲如沸培漏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)牌柄。三九已至,卻和暖如春侧甫,著一層夾襖步出監(jiān)牢的瞬間珊佣,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工披粟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留咒锻,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓僻爽,卻偏偏與公主長(zhǎng)得像虫碉,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子胸梆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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