一手遮天 Android - 存儲: Android 11 通過 MediaStore 管理文件
示例如下:
/storage/Android11Demo2.java
/**
* 本例用於演示 Android 11 通過 MediaStore 管理文件
*
* 通過 MediaStore 可以在指定的公共目錄中管理文件,包括圖片目錄,視頻目錄,音頻目錄,下載目錄
* 文檔目錄等其他公共目錄是不能通過 MediaStore 管理的
* 通過 MediaStore 保存的數據,卸載 app 后不會被刪除
* 通過 MediaStore 保存或讀取數據全部由程序管理,不需要用戶選擇(通過 Storage Access Framework 管理文件是需要用戶選擇的)
* 通過 MediaStore 保存文件時要注意,系統是通過你指定的 mime 來判斷文件類型的
* 圖片目錄只能保存圖片類型的文件
* 視頻目錄只能保存視頻類型的文件
* 音頻目錄只能保存音頻類型的文件
* 下載目錄可以保存任意類型的文件
*
* 注:
* 1、如果你卸載 app 后再重裝 app,則會失去卸載 app 之前通過 MediaStore 創建的文件的所有權
* 對於你有所有權的文件可以通過 MediaStore 訪問和刪除
* 對於你沒有所有權的文件,不可以通過 MediaStore 刪除,但是可以通過 MediaStore 訪問(前提是先要申請 READ_EXTERNAL_STORAGE 權限)
* 2、各種公共目錄並不是物理文件夾,而是將其他相關文件夾中的相關文件集中管理
* 比如視頻目錄會包括 DCIM 文件夾, Movies 文件夾, Pictures 文件夾中的視頻文件
*/
package com.webabcd.androiddemo.storage;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import android.Manifest;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import com.webabcd.androiddemo.R;
import java.io.OutputStream;
import java.util.Locale;
public class Android11Demo2 extends AppCompatActivity {
private final String LOG_TAG = "Android11Demo2";
private Button _button0;
private Button _button1;
private Button _button2;
private Button _button3;
private Button _button4;
private ImageView _imageView1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_storage_android11demo2);
_button0 = findViewById(R.id.button0);
_button1 = findViewById(R.id.button1);
_button2 = findViewById(R.id.button2);
_button3 = findViewById(R.id.button3);
_button4 = findViewById(R.id.button4);
_imageView1 = findViewById(R.id.imageView1);
sample();
}
private void sample() {
_button0.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
requestPermission();
}
});
_button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
createFile();
}
});
_button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
queryFileFirst();
}
});
_button3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
showFile();
}
});
_button4.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
deleteFileFirst();
}
});
}
// 請求“通過 MediaStore 讀取非本 app 創建的文件”的權限
// 先在 AndroidManifest.xml 中配置 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
// 然后按照如下方式請求(注:WRITE_EXTERNAL_STORAGE 權限已經無效了)
private void requestPermission() {
String[] storagePermissions = { Manifest.permission.READ_EXTERNAL_STORAGE };
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, storagePermissions, 123);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
for (int i = 0; i < permissions.length; i++) {
Log.i(LOG_TAG, "申請的權限為:" + permissions[i] + ",申請結果:" + grantResults[i]);
}
}
// 通過 MediaStore 在圖片目錄新建一個圖片文件
private void createFile() {
// 需要創建的圖片對象
BitmapDrawable bitmapDrawable = (BitmapDrawable) getResources().getDrawable(R.drawable.son01, null);
Bitmap bitmap = bitmapDrawable.getBitmap();
ContentValues contentValues = new ContentValues();
// 指定文件保存的文件夾名稱
// Environment.DIRECTORY_PICTURES 值為 Pictures
// Environment.DIRECTORY_DCIM 值為 DCIM
// Environment.DIRECTORY_MOVIES 值為 Movies
// Environment.DIRECTORY_MUSIC 值為 Music
// Environment.DIRECTORY_DOWNLOADS 值為 Download
// 如果想獲取上述文件夾的真實地址可以通過這樣的方式 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() 獲取,他返回的值類似 /storage/emulated/0/Pictures
contentValues.put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/wanglei_test/");
// 指定文件名
contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, "son");
// 指定文件的 mime(比如 image/jpeg, application/vnd.android.package-archive 之類的)
contentValues.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/jpeg");
contentValues.put(MediaStore.Images.ImageColumns.WIDTH, bitmap.getWidth());
contentValues.put(MediaStore.Images.ImageColumns.HEIGHT, bitmap.getHeight());
ContentResolver contentResolver = this.getContentResolver();
// 通過 ContentResolver 在指定的公共目錄下按照指定的 ContentValues 創建文件,會返回文件的 content uri(類似這樣的地址 content://media/external/images/media/102)
// 可以通過 MediaStore 保存文件的公共目錄有:
// MediaStore.Images.Media.EXTERNAL_CONTENT_URI - 圖片目錄,只能保存 mime 為圖片類型的文件
// 圖片目錄包括 Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_DCIM 文件夾
// MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - 音頻目錄,只能保存 mime 為音頻類型的文件
// 音頻目錄包括 Environment.DIRECTORY_MUSIC, Environment.DIRECTORY_RINGTONES 等等文件夾
// MediaStore.Video.Media.EXTERNAL_CONTENT_URI - 視頻目錄,只能保存 mime 為視頻類型的文件
// 視頻目錄包括 DIRECTORY_MOVIES, Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_DCIM 文件夾
// MediaStore.Downloads.EXTERNAL_CONTENT_URI - 下載目錄,可以保存任意類型的文件
// 下載目錄包括 Environment.DIRECTORY_DOWNLOADS 文件夾
Uri uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
if (uri == null) {
Log.d(LOG_TAG, "創建文件失敗");
} else {
Log.d(LOG_TAG, "創建文件成功:" + uri.toString());
}
// 寫入圖片數據
OutputStream outputStream = null;
try {
outputStream = contentResolver.openOutputStream(uri);
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, outputStream);
} catch (Exception ex) {
Log.d(LOG_TAG, "寫入數據失敗:" + ex.toString());
} finally {
try {
if (outputStream != null) {
outputStream.flush();
outputStream.close();
}
} catch (Exception ex) {
}
}
}
// 通過 MediaStore 在圖片目錄遍歷圖片文件
// 注:
// 1、如果你想遍歷出非本 app 創建的文件,則需要先申請 READ_EXTERNAL_STORAGE 權限
// 2、如果你的 app 卸載后再重裝的話系統不會認為是同一個 app(也就是你卸載之前創建的文件,再次安裝 app 后必須先申請 READ_EXTERNAL_STORAGE 權限后才能獲取到)
private int queryFileFirst() {
int contentId = -1;
// 通過 ContentResolver 遍歷指定公共目錄中的文件
ContentResolver contentResolver = this.getContentResolver();
// 第 2 個參數(projection):指定需要返回的字段,null 會返回所有字段
// 第 3 個參數(selection):查詢的 where 語句,類似 xxx = ?
// 第 4 個參數(selectionArgs):查詢的 where 語句的值,類似 new String[] { xxx }
// 第 5 個參數(selectionArgs):排序語句,類似 xxx DESC
Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);
Log.d(LOG_TAG, "count:" + cursor.getCount());
while (cursor.moveToNext()) {
// 比如這個地址 content://media/external/images/media/102 它的后面的 102 就是它的 id
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
// 獲取文件名
String title = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.TITLE));
// 獲取文件的真實路徑,類似 /storage/emulated/0/Pictures/wanglei_test/son.jpg
String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
Log.d(LOG_TAG, String.format("id:%d, title:%s, path:%s", id, title, path));
contentId = id;
}
cursor.close();
// 返回指定的公共目錄中的第一個文件的 id
return contentId;
}
// 通過 MediaStore 讀取圖片目錄中的圖片,並顯示
private void showFile() {
// 先拿到需要顯示的圖片的 content uri(類似這樣的地址 content://media/external/images/media/102)
int contentId = queryFileFirst();
Uri contentUri = Uri.parse(MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "/" + contentId);
Log.d(LOG_TAG, "show uri:" + contentUri);
Cursor cursor = null;
try {
String[] projection = { MediaStore.Images.Media.DATA };
cursor = this.getContentResolver().query(contentUri, projection, null, null, null);
cursor.moveToFirst();
// 拿到指定的 content uri 的真實路徑
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
Log.d(LOG_TAG, String.format(Locale.ENGLISH, "contentUri:%s, path:%s", contentUri, path));
// 顯示指定路徑的圖片
Bitmap bitmap = BitmapFactory.decodeFile(path);
_imageView1.setImageBitmap(bitmap);
} catch (Exception ex) {
Log.e(LOG_TAG, ex.toString());
}
finally {
if (cursor != null) {
cursor.close();
}
}
}
// 通過 MediaStore 刪除圖片目錄中的圖片文件(只能刪除本 app 創建的文件)
// 注:如果你的 app 卸載后再重裝的話系統不會認為是同一個 app(也就是你卸載之前創建的文件,再次安裝 app 后是無法通過這種方式刪除它的)
private void deleteFileFirst() {
try {
// 先拿到需要刪除的圖片的 content uri(類似這樣的地址 content://media/external/images/media/102)
int contentId = queryFileFirst();
Uri contentUri = Uri.parse(MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "/" + contentId);
Log.d(LOG_TAG, "delete uri:" + contentUri);
// 通過 ContentResolver 刪除指定的 content uri 的文件
ContentResolver contentResolver = this.getContentResolver();
int deleteCount = contentResolver.delete(contentUri, null);
Log.d(LOG_TAG, "delete count:" + deleteCount);
} catch (Exception ex) {
// 如果你想刪除非本 app 創建的文件,就會收到類似這樣的異常 android.app.RecoverableSecurityException: com.webabcd.androiddemo has no access to content://media/external/images/media/102
Log.e(LOG_TAG, "delete error:" + ex.toString());
}
}
}
/layout/activity_storage_android11demo2.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/button0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="請求“通過 MediaStore 讀取非本 app 創建的文件”的權限" />
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="通過 MediaStore 在圖片目錄新建一個圖片文件" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="通過 MediaStore 在圖片目錄遍歷圖片文件" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="通過 MediaStore 讀取圖片目錄中的圖片,並顯示" />
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="通過 MediaStore 刪除圖片目錄中的圖片文件" />
<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp" />
</LinearLayout>