- Android Q文件存儲機制修改成了沙盒模式,和IOS神似
- 應用只能訪問自己沙盒下的文件和公共媒體文件
- 對於Android Q以下,還是使用老的文件存儲方式
權限
Android Q不再需要申請文件讀寫權限,默認可以讀寫自己沙盒文件和公共媒體文件。所以,Q以上不需要再動態申請文件讀寫權限。
沙盒存儲/讀寫
獲取沙盒指定文件夾
//廢棄方法
//不再用以下代碼獲取文件根目錄了
Environment.getExternalStorageDirectory();
Environment.getExternalStoragePublicDirectory();
//獲取沙盒下的文件目錄
//沙盒下的圖片文件夾
File filePictures = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
//Environment下的文件夾名稱常量
//只需要調用,不需要創建,如果手機中沒有對應的文件夾,則系統會自動生成
//以下爲源碼中的各個文件夾名稱描述
/**
* Standard directory in which to place any audio files that should be
* in the regular list of music for the user.
* This may be combined with
* {@link #DIRECTORY_PODCASTS}, {@link #DIRECTORY_NOTIFICATIONS},
* {@link #DIRECTORY_ALARMS}, and {@link #DIRECTORY_RINGTONES} as a series
* of directories to categories a particular audio file as more than one
* type.
*/
public static String DIRECTORY_MUSIC = "Music";
/**
* Standard directory in which to place any audio files that should be
* in the list of podcasts that the user can select (not as regular
* music).
* This may be combined with {@link #DIRECTORY_MUSIC},
* {@link #DIRECTORY_NOTIFICATIONS},
* {@link #DIRECTORY_ALARMS}, and {@link #DIRECTORY_RINGTONES} as a series
* of directories to categories a particular audio file as more than one
* type.
*/
public static String DIRECTORY_PODCASTS = "Podcasts";
/**
* Standard directory in which to place any audio files that should be
* in the list of ringtones that the user can select (not as regular
* music).
* This may be combined with {@link #DIRECTORY_MUSIC},
* {@link #DIRECTORY_PODCASTS}, {@link #DIRECTORY_NOTIFICATIONS}, and
* {@link #DIRECTORY_ALARMS} as a series
* of directories to categories a particular audio file as more than one
* type.
*/
public static String DIRECTORY_RINGTONES = "Ringtones";
/**
* Standard directory in which to place any audio files that should be
* in the list of alarms that the user can select (not as regular
* music).
* This may be combined with {@link #DIRECTORY_MUSIC},
* {@link #DIRECTORY_PODCASTS}, {@link #DIRECTORY_NOTIFICATIONS},
* and {@link #DIRECTORY_RINGTONES} as a series
* of directories to categories a particular audio file as more than one
* type.
*/
public static String DIRECTORY_ALARMS = "Alarms";
/**
* Standard directory in which to place any audio files that should be
* in the list of notifications that the user can select (not as regular
* music).
* This may be combined with {@link #DIRECTORY_MUSIC},
* {@link #DIRECTORY_PODCASTS},
* {@link #DIRECTORY_ALARMS}, and {@link #DIRECTORY_RINGTONES} as a series
* of directories to categories a particular audio file as more than one
* type.
*/
public static String DIRECTORY_NOTIFICATIONS = "Notifications";
/**
* Standard directory in which to place pictures that are available to
* the user. Note that this is primarily a convention for the top-level
* public directory, as the media scanner will find and collect pictures
* in any directory.
*/
public static String DIRECTORY_PICTURES = "Pictures";
/**
* Standard directory in which to place movies that are available to
* the user. Note that this is primarily a convention for the top-level
* public directory, as the media scanner will find and collect movies
* in any directory.
*/
public static String DIRECTORY_MOVIES = "Movies";
/**
* Standard directory in which to place files that have been downloaded by
* the user. Note that this is primarily a convention for the top-level
* public directory, you are free to download files anywhere in your own
* private directories. Also note that though the constant here is
* named DIRECTORY_DOWNLOADS (plural), the actual file name is non-plural for
* backwards compatibility reasons.
*/
public static String DIRECTORY_DOWNLOADS = "Download";
/**
* The traditional location for pictures and videos when mounting the
* device as a camera. Note that this is primarily a convention for the
* top-level public directory, as this convention makes no sense elsewhere.
*/
public static String DIRECTORY_DCIM = "DCIM";
/**
* Standard directory in which to place documents that have been created by
* the user.
*/
public static String DIRECTORY_DOCUMENTS = "Documents";
/**
* Standard directory in which to place screenshots that have been taken by
* the user. Typically used as a secondary directory under
* {@link #DIRECTORY_PICTURES}.
*/
public static String DIRECTORY_SCREENSHOTS = "Screenshots";
/**
* Standard directory in which to place any audio files which are
* audiobooks.
*/
public static String DIRECTORY_AUDIOBOOKS = "Audiobooks";
沙盒裏新建文件夾和新建文件
private String signImage = "signImage";
//將文件保存到沙盒中
//注意:
//1. 這裏的文件操作不再需要申請權限
//2. 沙盒中新建文件夾只能再系統指定的子文件夾中新建
public void saveSignImageBox(String fileName, Bitmap bitmap) {
try {
//圖片沙盒文件夾
File PICTURES = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File imageFileDirctory = new File(PICTURES + "/" + signImage);
if (imageFileDirctory.exists()) {
File imageFile = new File(PICTURES + "/" + signImage + "/" + fileName);
FileOutputStream fileOutputStream = new FileOutputStream(imageFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream);
fileOutputStream.flush();
fileOutputStream.close();
} else if (imageFileDirctory.mkdir()) {//如果該文件夾不存在,則新建
//new一個文件
File imageFile = new File(PICTURES + "/" + signImage + "/" + fileName);
//通過流將圖片寫入
FileOutputStream fileOutputStream = new FileOutputStream(imageFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream);
fileOutputStream.flush();
fileOutputStream.close();
}
} catch (Exception e) {
}
}
沙盒中查詢指定文件
//查詢沙盒中的指定圖片
//先指定哪個沙盒子文件夾,再指定名稱
public Bitmap querySignImageBox(String environmentType,String fileName) {
if (TextUtils.isEmpty(fileName)) return null;
Bitmap bitmap = null;
try {
//指定沙盒文件夾
File picturesFile = getExternalFilesDir(environmentType);
if (picturesFile != null && picturesFile.exists() && picturesFile.isDirectory()) {
File[] files = picturesFile.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory() && file.getName().equals(signImage)) {
File[] signImageFiles = file.listFiles();
if (signImageFiles != null) {
for (File signImageFile : files) {
String signFileName = signImageFile.getName();
if (signImageFile.isFile() && fileName.equals(signFileName)) {
bitmap = BitmapFactory.decodeFile(signImageFile.getPath());
}
}
}
}
}
}
}
} catch (Exception e) {
}
return bitmap;
}
公共文件的增、刪、改、查
公共文件的操作需要用到ContentResolver和Cursor
向公共文件夾添加文件
//將文件保存到公共的媒體文件夾
//這裏的filepath不是絕對路徑,而是某個媒體文件夾下的子路徑,和沙盒子文件夾類似
//這裏的filename單純的指文件名,不包含路徑
public void saveSignImage(String filePath,String fileName, Bitmap bitmap) {
try {
//設置保存參數到ContentValues中
ContentValues contentValues = new ContentValues();
//設置文件名
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
//兼容Android Q和以下版本
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
//android Q中不再使用DATA字段,而用RELATIVE_PATH代替
//RELATIVE_PATH是相對路徑不是絕對路徑
//DCIM是系統文件夾,關於系統文件夾可以到系統自帶的文件管理器中查看,不可以寫沒存在的名字
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/signImage");
//contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Music/signImage");
} else {
contentValues.put(MediaStore.Images.Media.DATA, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath());
}
//設置文件類型
contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/JPEG");
//執行insert操作,向系統文件夾中添加文件
//EXTERNAL_CONTENT_URI代表外部存儲器,該值不變
Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
if (uri != null) {
//若生成了uri,則表示該文件添加成功
//使用流將內容寫入該uri中即可
OutputStream outputStream = getContentResolver().openOutputStream(uri);
if (outputStream != null) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
outputStream.flush();
outputStream.close();
}
}
} catch (Exception e) {
}
}
查詢公共文件夾下的文件
//在公共文件夾下查詢圖片
//這裏的filepath在androidQ中表示相對路徑
//在androidQ以下是絕對路徑
public Bitmap querySignImage(String filePath) {
try {
//兼容androidQ和以下版本
String queryPathKey = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q ? MediaStore.Images.Media.RELATIVE_PATH : MediaStore.Images.Media.DATA;
//查詢的條件語句
String selection = queryPathKey + "=? ";
//查詢的sql
//Uri:指向外部存儲Uri
//projection:查詢那些結果
//selection:查詢的where條件
//sortOrder:排序
Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media._ID, queryPathKey, MediaStore.Images.Media.MIME_TYPE, MediaStore.Images.Media.DISPLAY_NAME},
selection,
new String[]{filePath},
null);
//是否查詢到了
if (cursor != null && cursor.moveToFirst()) {
//循環取出所有查詢到的數據
do {
//一張圖片的基本信息
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));//uri的id,用於獲取圖片
String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.RELATIVE_PATH));//圖片的相對路徑
String type = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE));//圖片類型
String name = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME));//圖片名字
//根據圖片id獲取uri,這裏的操作是拼接uri
Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "" + id);
//官方代碼:
Uri contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
if (uri != null) {
//通過流轉化成bitmap對象
InputStream inputStream = getContentResolver().openInputStream(uri);
return BitmapFactory.decodeStream(inputStream);
}
} while (cursor.moveToNext());
}
if (cursor != null)
cursor.close();
} catch (Exception e) {
}
return null;
}
其他操作
增刪改查操作都是使用:ContentResolver去操作
官方文檔中也說明了,想要操作公共目錄,使用ContentResolver去進行一切增刪改查:
在 Android 9(API 級別 28)及更低版本中,保存到外部存儲設備上的所有文件都顯示在名爲 external 的單個卷下。但是,Android Q 爲每個外部存儲設備都提供唯一的卷名稱。這一新的命名系統可幫助您高效地整理內容並將內容編入索引,還可讓您控制新內容的存儲位置。
主要共享存儲設備始終稱爲 VOLUME_EXTERNAL_PRIMARY。您可以通過調用 MediaStore.getExternalVolumeNames() 發現其他卷。
要查詢、插入、更新或刪除特定卷,請將卷名稱傳遞到 MediaStore API 中的任何 getContentUri() 方法,如以下代碼段中所示:
// Publish an audio file onto a specific external storage device.
val values = ContentValues().apply {
put(MediaStore.Audio.Media.RELATIVE_PATH, “Music/My Album/My Song”)
put(MediaStore.Audio.Media.DISPLAY_NAME, “My Song.mp3”)
}
// Assumes that the storage device of interest is the 2nd one
// that your app recognizes.
val volumeNames = MediaStore.getExternalVolumeNames(context)
val selectedVolumeName = volumeNames[1]
val collection = MediaStore.Audio.Media.getContentUri(selectedVolumeName)
val item = resolver.insert(collection, values)