分區(qū)存儲(chǔ)介紹
在Android10以前魁衙,只要程序獲得了READ_EXTERNAL_STORAGE權(quán)限,就可以隨意讀取外部的存儲(chǔ)公有目錄湾盗。只要程序獲得了WRITE_EXTERNAL_STORAGE權(quán)限,就可以隨意在寫入外部存儲(chǔ)的公有目錄上新建文件或文件夾
于是Google在Android10中提出了分區(qū)存儲(chǔ)泳唠,意在限制程序?qū)ν獠看鎯?chǔ)中公有目錄的使用。
分區(qū)存儲(chǔ)對(duì)內(nèi)部存儲(chǔ)私有目錄和外部存儲(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)用和提供程序中瀏覽文件曹鸠,以及訪問最近使用的文件。
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á)到訪問的目的。
- Audio
- Internal:MediaStore.Audio.Media.INTERNAL_CONTENT_URI
- content://media/internal/audio/media
- External:MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
- content://media/external/audio/media
- 可移動(dòng)存儲(chǔ):MediaStore.Audio.Media.getContentUri
- content://media/<volumeName>/audio/media
- Video
- Internal:MediaStore.Video.Media.INTERNAL_CONTENT_URI
- content://media/internal/video/media
- External:MediaStore.Video.Media.EXTERNAL_CONTENT_URI
- content://media/external/video/media
- 可移動(dòng)存儲(chǔ):MediaStore.Video.Media.getContentUri
- content://media/<volumeName>/video/media
- Image
- Internal: MediaStore.Images.Media.INTERNAL_CONTENT_URI
- content://media/internal/images/media响逢。
- External: MediaStore.Images.Media.EXTERNAL_CONTENT_URI
- content://media/external/images/media绒窑。
- 可移動(dòng)存儲(chǔ): MediaStore.Images.Media.getContentUri
- content://media/<volumeName>/images/media。
- File
- MediaStore. Files.Media.getContentUri
- content://media/<volumeName>/file舔亭。
- Downloads
- Internal: MediaStore.Downloads.INTERNAL_CONTENT_URI
- content://media/internal/downloads回论。
- External: MediaStore.Downloads.EXTERNAL_CONTENT_URI
- content://media/external/downloads散罕。
- 可移動(dòng)存儲(chǔ): MediaStore.Downloads.getContentUri
- content://media/<volumeName>/downloads。
獲取所有的Volume
我們還可以使用getContentUri獲取所有<volumeName>
Uri跟公共目錄關(guān)系
MediaProvider對(duì)于App存放到公共目錄文件傀蓉,通過ContentResolver insert方法中Uri來確定
權(quán)限
MediaStroe通過不同Uri,為用戶提供了增职抡、刪葬燎、改方法,權(quán)限對(duì)應(yīng)如下
- 由上表可以看出沒在操作共享存儲(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瀏覽