Android中不同系统的适配一直是应用开发中非常重要的问题,如果不能及时适配不同的系统版本,应用极有可能发生崩溃,给用户带来不好的体验。
随着Google发布 Android Q(API 29),我们需要及时的根据系统的变化做出相应的适配。Android Q中隐私权一块发生了较大的改变。特别是外部存储的访问权限的改变。Android Q中引入了分区存储的概念,应用默认拥有
分区存储的访问(读写)权限,而对于分区存储之外的文件,将无法直接通过文件路径以及File API来进行访问以及操作。
分区存储:
应用访问(读/写)分区存储下的文件不需要再获取任何权限,可以直接访问。
如果应用要访问其它应用创建的文件则必须满足以下两个条件
1.应用已获得READ_EXTERNAL_STORAGE权限
2.文件位于以下其中一个明确的媒体集合中
照片:MediaStore.Image
音频:MediaStore.Audio
视频:MediaStore.Video
为了访问其它应用创建的任何其它文件(包括“downloads”目录下的文件)
“downloads”目录是Android Q才引入的对应MediaStore.Download媒体集合,应用必须使用SAF(存储访问框架),也就是打开系统的文件选择器
可以访问任何文件。
下面我们来具体实践一下Android Q中文件操作;
1.分区存储中应用私有目录中文件的操作
外部存储中应用私有目录就是Context.getExternalFiles(“”)目录
该目录下的文件我们可以使用文件路径和File API进行操作,和以前的文件操作完全一样。
/**
* 创建文件
*/
private void createFile() {
String path = mActivity.getExternalFilesDir("").getAbsolutePath() + File.separator + System.currentTimeMillis() + ".txt";
File file = new File(path);
if (!file.exists()) {
try {
boolean success = file.createNewFile();
Log.e(TAG, "createFile: "+success );
} catch (IOException e) {
e.printStackTrace();
}
}
}
文件的写操作也是一样,通过File拿到文件的输出流,然后进行写操作,这里就不再赘述了
2.应用自己创建的媒体文件或者其他应用创建的媒体文件的操作
如果是自己应用创建的文件,访问无需再获取任何权限,如果是其他应用创建的文件,访问需要获取READ_EXTERNAL_STORAGE权限。操作不再可以使用文件路径和File API进行操作,必须使用MediaStore和ContentResolver来进行文件的访问和操作。
/**
* 新增文件
*/
private void createFile() {
String fileName = System.currentTimeMillis() + "";
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName);
contentValues.put(MediaStore.Video.VideoColumns.MIME_TYPE, "video/mp4");
Uri uri = mActivity.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues);
Log.e(TAG, "createFile: " + uri);
}
上面代码会向数据库里面插入一条记录,会返回一个Uri,但是并不会生成新的文件,只有我们通过
Uri往文件里面写入内容,才会生成文件。
/**
* 查询外部存储所有媒体文件
*/
private void queryFile() {
Cursor cursor = mActivity.getContentResolver().query(MediaStore.Files.getContentUri("external"), null, null, null, null);
while (cursor.moveToNext()) {
String id=cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID));
String data = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Downloads.DATA));
Log.e(TAG, "queryFile: " + data);
}
}
这里的查询出来的所有的媒体文件的data在Android Q之前是文件的实际路径,而在Android Q中该路径将不再有用,因为我们无法通过文件路径和File API来操作公共目录下的文件。只能通过Uri和ContentResolver来获取文件的输出流来进行操作,而媒体文件的Uri我们可以通过文件的ID来获取
/**
* 根据媒体文件的ID来获取文件的Uri
* @param id
* @return
*/
public String getMediaFileUriFromID(String id) {
return MediaStore.Files.getContentUri("external").buildUpon().appendPath(String.valueOf(id)).build().toString();
}
获取到文件的Uri之后我们就能够通过ContentResolver来获取文件的输出流,来对文件进行写操作
/**
* 对文件进行写操作
* @param uri
*/
private void writeFile(Uri uri) {
try {
AssetFileDescriptor assetFileDescriptor = mActivity.getContentResolver().openAssetFileDescriptor(uri, "rw");
FileOutputStream outputStream = assetFileDescriptor.createOutputStream();
String str = "123";
outputStream.write(str.getBytes());
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
bufferedWriter.write(str);
bufferedWriter.close();
outputStream.close();
Log.e(TAG, "updateFile: " + uri);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 删除文件
*/
private void deleteFile(Uri uri) {
int deleteNum = mActivity.getContentResolver().delete(uri, null, null);
Log.e(TAG, "deleteFile: " + deleteNum);
}
以上就是分区存储中媒体文件增删改查的操作
3.非私有目录非媒体文件的操作
对于非私有目录,非媒体文件,也就是说如果我们想访问一个外部存储中非私有目录的非媒体文件,或者说是非私有目录的任何类型文件,只能通过SAF(存储访问框架)来进行访问。
可以使用SAF(存储访问框架)访问任何一个文件,而无需请求任何权限。所谓的SAF(存储访问框架)就是
打开系统的文件选择器来进行操作。
/**
* 打开文件选择器选择文件
*/
private void SAF() {
// ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
// browser.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
// Filter to only show results that can be "opened", such as a
// file (as opposed to a list of contacts or timezones)
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Filter to show only images, using the image MIME data type.
// If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
// To search for all documents available via installed storage providers,
// it would be "*/*".
intent.setType("*/*");
startActivityForResult(intent, READ_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
// The document selected by the user won't be returned in the intent.
// Instead, a URI to that document will be contained in the return intent
// provided to this method as a parameter.
// Pull that URI using resultData.getData().
Uri uri = null;
if (data != null) {
uri = data.getData();
Log.e(TAG, "Uri: " + uri.toString());
}
}
}
打开文件选择器选择文件后,会回调onActivityResult,在返回的data中可以拿到文件的Uri,然后对文件进行操作
/**
* 创建文件
* @param mimeType
* @param fileName
*/
private void SAFCreateFile(String mimeType, String fileName) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
// Filter to only show results that can be "opened", such as
// a file (as opposed to a list of contacts or timezones).
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Create a file with the requested MIME type.
intent.setType(mimeType);
intent.putExtra(Intent.EXTRA_TITLE, fileName);
startActivityForResult(intent, SAF_CREATE_FILE_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == SAF_CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
Uri uri = null;
if (data != null) {
uri = data.getData();
Log.e(TAG, "Uri: " + uri.toString());
}
}
}
打开文件选择器创建好文件后,会回调onActivityResult在返回的data中可以拿到创建的文件的Uri,然后进行
操作。
/**
* 删除文件
*/
private void SAFDeleteFile(Uri uri) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
boolean success = DocumentsContract.deleteDocument(getContentResolver(), uri);
Log.e(TAG, "SAFDeleteFile: " + success);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
如果获得了文件的 URI,并且文件的 Document.COLUMN_FLAGS 包含 SUPPORTS_DELETE,则便可通过上述代码删除该文件。
至于通过SAF对文件进行编辑,就是通过SAF选择文件后,拿到文件的Uri,然后和上面对媒体文件进行写操作一样的方式通过文件Uri和ContentResolver拿到文件的输出流,然后进行写操作。
以上就是通过SAF(存储访问框架)来对外部存储中任意类型文件进行增删改查操作。
总结:Android 10的分区存储极大的控制了应用混乱的使用设备的外部存储,从Android 6.0开始的动态权限,Android 7.0的应用私有文件的共享,再到Android 10的分区存储等等关于用户隐私权限的变更,Google无一不在把控制权更多的交给用户,让用户更好的管理和使用自己的设备和应用。