Android 增删改查媒体商店 API 26+
Android CRUD MediaStore API 26+
我申请了 API26+
使用 Android 10 及以上 (API29+) 应该使用 MediaStore
来访问文件,而不是 Environment.getExternalStoragePublicDirectory
通常在创建 if-block 时会创建新方法
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// deprecated code
} else {
// new approach
}
但是:
MediaStorage
使用 Uri
请求文件。 API28 及以下使用 File
或 String
作为文件路径
MediaStore
不需要手动创建文件夹。 API 28 及以下应创建一个文件夹,如果不存在。
- 删除文件需要用户与
MediaStore
交互。
增删改查:
- 创建: 为任何 API 将数据写入文件是
OutputStream
所必需的。可以通过 if else
达到
- Read:如何创建读取文件的通用方法,其中 API 29+ 需要
Uri
,而 API28-需要 File|String
才能访问文件?
- Update: 首先Read检查是否存在,然后Create得到
OutputStream
- 删除: 如何用
MediaStore
创建批量确认?
是否可以与 MediaStorage
和 API26+ 一起工作?
如果是,怎么样?许多属性首先添加在 API29
经过一些自己的研究和测试,我找到了同时使用 MediaStore
和旧设备的方法。
首先需要一些助手class:
使用FileType
我们可以同时支持应用程序中的不同文件类型。
public enum FileType {
File(Environment.DIRECTORY_DOCUMENTS, Constants.VERSION_29_ABOVE ? MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "text/plain"),
Download(Environment.DIRECTORY_DOWNLOADS, Constants.VERSION_29_ABOVE ? MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "text/plain"),
Image(Environment.DIRECTORY_PICTURES, Constants.VERSION_29_ABOVE ? MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "image/jpeg"),
Audio(Environment.DIRECTORY_MUSIC, Constants.VERSION_29_ABOVE ? MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "audio/mpeg"),
Video(Environment.DIRECTORY_MOVIES, Constants.VERSION_29_ABOVE ? MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "video/mpeg");
private final String directory;
private final Uri contentUri;
private String mimeType;
FileType(String directory, Uri contentUri, String mimeType) {
this.directory = directory;
this.contentUri = contentUri;
this.mimeType = mimeType;
}
public String getDirectory() {
return directory;
}
public Uri getContentUri() {
return contentUri;
}
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
}
使用 setMimeType
我们可以更改当前文件扩展名并仍然使用其他设置
我们需要简单的回调 class 来获得不同的结果
public interface ObjectCallback<T> {
void result(T object);
}
当我们想在文件中写入一些数据时,我们需要一个OutputStream
使用内部存储,获取文件的方式没有改变
/**
* opens OutputStream to write data to file
*
* @param context activity context
* @param fileName relative file name
* @param fileType file type to get folder specific values for access
* @param fileStream callback with file output stream to requested file
* @return true if output stream successful opened, false otherwise
*/
public boolean openOutputStream(Context context, String fileName, FileType fileType, ObjectCallback<OutputStream> fileStream) {
File internalFolder = context.getExternalFilesDir(fileType.getDirectory());
File absFileName = new File(internalFolder, fileName);
try {
FileOutputStream fOut = new FileOutputStream(absFileName);
fileStream.result(fOut);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
通过外部存储,在API 28-和API 29+上访问文件的方式是完全不同的。
在 API 28- 我们可以只使用一个文件。
在 API 29+ 我们应该首先检查文件是否已经存在。如果它(比如 text.txt
)存在并且我们将创建 ContentResolver.insert
,它将创建 text-1.txt
,在最坏的情况下(无限循环)可能会快速填满整个智能手机存储空间。
/**
* @param context application context
* @param fileName relative file name
* @return uri to file or null if file not exist
*/
public Uri getFileUri(Context context, String fileName) {
// remove folders from file name
fileName = fileName.contains("/") ? fileName.substring(fileName.lastIndexOf("/") + 1) : fileName;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// Deprecated in API 29
File storageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), Constants.PUBLIC_STORAGE_FOLDER);
File file = new File(storageDir, fileName);
return file.exists() ? Uri.fromFile(file) : null;
} else {
String folderName = Environment.DIRECTORY_PICTURES + File.separator + Constants.PUBLIC_STORAGE_FOLDER + File.separator;
// get content resolver that can interact with public storage
ContentResolver resolver = context.getContentResolver();
String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?";
String[] selectionArgs = new String[]{folderName};
Cursor cursor = resolver.query(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL), null, selection, selectionArgs, null);
Uri uri = null;
if (cursor.getCount() > 0) {
while (cursor.moveToNext()) {
String itemFileName = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));
if (itemFileName.equals(fileName)) {
long id = cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
uri = ContentUris.withAppendedId(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL), id);
break;
}
}
}
cursor.close();
return uri;
}
}
/**
* opens OutputStream to write data to file
*
* @param context activity context
* @param fileName relative file name
* @param fileType file type to get folder specific values for access
* @param fileStream callback with file output stream to requested file
* @return true if output stream successful opened, false otherwise
*/
public boolean openOutputStream(Context context, String fileName, FileType fileType, ObjectCallback<OutputStream> fileStream) {
// remove folders from file name
fileName = fileName.contains("/") ? fileName.substring(fileName.lastIndexOf("/") + 1) : fileName;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
File storageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), Constants.PUBLIC_STORAGE_FOLDER);
if (!storageDir.exists() && !storageDir.mkdir()) {
// directory for file not exists and not created. return false
return false;
}
File file = new File(storageDir, fileName);
try {
FileOutputStream fOut = new FileOutputStream(file);
fileStream.result(fOut);
} catch (Exception e) {
e.printStackTrace();
return false;
}
} else {
// get content resolver that can interact with public storage
ContentResolver resolver = context.getContentResolver();
// always check first if file already exist
Uri existFile = getFileUri(context, fileName);
if (existFile == null) {
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.MIME_TYPE, fileType.getMimeType());
// absolute folder name
values.put(MediaStore.MediaColumns.RELATIVE_PATH, fileType.getDirectory() + File.separator + Constants.PUBLIC_STORAGE_FOLDER);
// file name
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
// create uri to the file. If folder not exists, it would be created automatically
Uri uri = resolver.insert(fileType.getContentUri(), values);
// open stream from uri and write data to file
try (OutputStream outputStream = resolver.openOutputStream(uri)) {
fileStream.result(outputStream);
// end changing of file
// when this point reached, no exception was thrown and file write was successful
values.put(MediaStore.MediaColumns.IS_PENDING, false);
resolver.update(uri, values, null, null);
} catch (Exception e) {
e.printStackTrace();
if (uri != null) {
// Don't leave an orphan entry in the MediaStore
resolver.delete(uri, null, null);
}
return false;
}
} else {
// open stream from uri and write data to file
try (OutputStream outputStream = resolver.openOutputStream(existFile)) {
fileStream.result(outputStream);
} catch (Exception e) {
e.printStackTrace();
// Don't leave an orphan entry in the MediaStore
resolver.delete(existFile, null, null);
return false;
}
}
}
return true;
}
可以在所有设备和不同存储上以相同方式打开InputStream
。
要删除文件:
- 内部仍然可以在每个 API
上用 file.delete
删除
- 具有 API29+ 的外部存储需要用户交互才能删除一些文件
文件提供者
要使用 Intent
显示一些图片、裁剪图片或其他任何东西,我们使用 setDataAndType
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(photoUri, "image/*");
// allow to read the file
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
在某些情况下我们需要 FileProvider。特别地:
要显示使用该应用程序创建的外部文件,photoUri
看起来像这样
Uri photoUri = Constants.VERSION_29_ABOVE ? uriToFile :
FileProvider.getUriForFile(context,
context.getApplicationContext().getPackageName() + ".provider",
new File(uriToFile.getPath()));
我申请了 API26+
使用 Android 10 及以上 (API29+) 应该使用 MediaStore
来访问文件,而不是 Environment.getExternalStoragePublicDirectory
通常在创建 if-block 时会创建新方法
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// deprecated code
} else {
// new approach
}
但是:
MediaStorage
使用Uri
请求文件。 API28 及以下使用File
或String
作为文件路径MediaStore
不需要手动创建文件夹。 API 28 及以下应创建一个文件夹,如果不存在。- 删除文件需要用户与
MediaStore
交互。
增删改查:
- 创建: 为任何 API 将数据写入文件是
OutputStream
所必需的。可以通过if else
达到
- Read:如何创建读取文件的通用方法,其中 API 29+ 需要
Uri
,而 API28-需要File|String
才能访问文件? - Update: 首先Read检查是否存在,然后Create得到
OutputStream
- 删除: 如何用
MediaStore
创建批量确认?
是否可以与 MediaStorage
和 API26+ 一起工作?
如果是,怎么样?许多属性首先添加在 API29
经过一些自己的研究和测试,我找到了同时使用 MediaStore
和旧设备的方法。
首先需要一些助手class:
使用FileType
我们可以同时支持应用程序中的不同文件类型。
public enum FileType {
File(Environment.DIRECTORY_DOCUMENTS, Constants.VERSION_29_ABOVE ? MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "text/plain"),
Download(Environment.DIRECTORY_DOWNLOADS, Constants.VERSION_29_ABOVE ? MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "text/plain"),
Image(Environment.DIRECTORY_PICTURES, Constants.VERSION_29_ABOVE ? MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "image/jpeg"),
Audio(Environment.DIRECTORY_MUSIC, Constants.VERSION_29_ABOVE ? MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "audio/mpeg"),
Video(Environment.DIRECTORY_MOVIES, Constants.VERSION_29_ABOVE ? MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "video/mpeg");
private final String directory;
private final Uri contentUri;
private String mimeType;
FileType(String directory, Uri contentUri, String mimeType) {
this.directory = directory;
this.contentUri = contentUri;
this.mimeType = mimeType;
}
public String getDirectory() {
return directory;
}
public Uri getContentUri() {
return contentUri;
}
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
}
使用 setMimeType
我们可以更改当前文件扩展名并仍然使用其他设置
我们需要简单的回调 class 来获得不同的结果
public interface ObjectCallback<T> {
void result(T object);
}
当我们想在文件中写入一些数据时,我们需要一个OutputStream
使用内部存储,获取文件的方式没有改变
/**
* opens OutputStream to write data to file
*
* @param context activity context
* @param fileName relative file name
* @param fileType file type to get folder specific values for access
* @param fileStream callback with file output stream to requested file
* @return true if output stream successful opened, false otherwise
*/
public boolean openOutputStream(Context context, String fileName, FileType fileType, ObjectCallback<OutputStream> fileStream) {
File internalFolder = context.getExternalFilesDir(fileType.getDirectory());
File absFileName = new File(internalFolder, fileName);
try {
FileOutputStream fOut = new FileOutputStream(absFileName);
fileStream.result(fOut);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
通过外部存储,在API 28-和API 29+上访问文件的方式是完全不同的。
在 API 28- 我们可以只使用一个文件。
在 API 29+ 我们应该首先检查文件是否已经存在。如果它(比如 text.txt
)存在并且我们将创建 ContentResolver.insert
,它将创建 text-1.txt
,在最坏的情况下(无限循环)可能会快速填满整个智能手机存储空间。
/**
* @param context application context
* @param fileName relative file name
* @return uri to file or null if file not exist
*/
public Uri getFileUri(Context context, String fileName) {
// remove folders from file name
fileName = fileName.contains("/") ? fileName.substring(fileName.lastIndexOf("/") + 1) : fileName;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// Deprecated in API 29
File storageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), Constants.PUBLIC_STORAGE_FOLDER);
File file = new File(storageDir, fileName);
return file.exists() ? Uri.fromFile(file) : null;
} else {
String folderName = Environment.DIRECTORY_PICTURES + File.separator + Constants.PUBLIC_STORAGE_FOLDER + File.separator;
// get content resolver that can interact with public storage
ContentResolver resolver = context.getContentResolver();
String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?";
String[] selectionArgs = new String[]{folderName};
Cursor cursor = resolver.query(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL), null, selection, selectionArgs, null);
Uri uri = null;
if (cursor.getCount() > 0) {
while (cursor.moveToNext()) {
String itemFileName = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));
if (itemFileName.equals(fileName)) {
long id = cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
uri = ContentUris.withAppendedId(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL), id);
break;
}
}
}
cursor.close();
return uri;
}
}
/**
* opens OutputStream to write data to file
*
* @param context activity context
* @param fileName relative file name
* @param fileType file type to get folder specific values for access
* @param fileStream callback with file output stream to requested file
* @return true if output stream successful opened, false otherwise
*/
public boolean openOutputStream(Context context, String fileName, FileType fileType, ObjectCallback<OutputStream> fileStream) {
// remove folders from file name
fileName = fileName.contains("/") ? fileName.substring(fileName.lastIndexOf("/") + 1) : fileName;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
File storageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), Constants.PUBLIC_STORAGE_FOLDER);
if (!storageDir.exists() && !storageDir.mkdir()) {
// directory for file not exists and not created. return false
return false;
}
File file = new File(storageDir, fileName);
try {
FileOutputStream fOut = new FileOutputStream(file);
fileStream.result(fOut);
} catch (Exception e) {
e.printStackTrace();
return false;
}
} else {
// get content resolver that can interact with public storage
ContentResolver resolver = context.getContentResolver();
// always check first if file already exist
Uri existFile = getFileUri(context, fileName);
if (existFile == null) {
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.MIME_TYPE, fileType.getMimeType());
// absolute folder name
values.put(MediaStore.MediaColumns.RELATIVE_PATH, fileType.getDirectory() + File.separator + Constants.PUBLIC_STORAGE_FOLDER);
// file name
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
// create uri to the file. If folder not exists, it would be created automatically
Uri uri = resolver.insert(fileType.getContentUri(), values);
// open stream from uri and write data to file
try (OutputStream outputStream = resolver.openOutputStream(uri)) {
fileStream.result(outputStream);
// end changing of file
// when this point reached, no exception was thrown and file write was successful
values.put(MediaStore.MediaColumns.IS_PENDING, false);
resolver.update(uri, values, null, null);
} catch (Exception e) {
e.printStackTrace();
if (uri != null) {
// Don't leave an orphan entry in the MediaStore
resolver.delete(uri, null, null);
}
return false;
}
} else {
// open stream from uri and write data to file
try (OutputStream outputStream = resolver.openOutputStream(existFile)) {
fileStream.result(outputStream);
} catch (Exception e) {
e.printStackTrace();
// Don't leave an orphan entry in the MediaStore
resolver.delete(existFile, null, null);
return false;
}
}
}
return true;
}
可以在所有设备和不同存储上以相同方式打开InputStream
。
要删除文件:
- 内部仍然可以在每个 API 上用
- 具有 API29+ 的外部存储需要用户交互才能删除一些文件
file.delete
删除
文件提供者
要使用 Intent
显示一些图片、裁剪图片或其他任何东西,我们使用 setDataAndType
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(photoUri, "image/*");
// allow to read the file
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
在某些情况下我们需要 FileProvider。特别地:
要显示使用该应用程序创建的外部文件,photoUri
看起来像这样
Uri photoUri = Constants.VERSION_29_ABOVE ? uriToFile :
FileProvider.getUriForFile(context,
context.getApplicationContext().getPackageName() + ".provider",
new File(uriToFile.getPath()));