赞
踩
漫长的假期,在家整理了一下Android 10的适配内容。因为适配篇的篇幅问题,就将这一部本单独出来,也先放出来。
Android 4.4 就引入了存储访问框架 (SAF)。借助 SAF,用户可轻松在其所有首选文档存储提供程序中浏览并打开文档、图像及其他文件。用户可通过易用的标准界面,以统一方式在所有应用和提供程序中浏览文件,以及访问最近使用的文件。
SAF 提供的部分功能:
虽说早在Android 4.4就已经引入了,但是我却从未使用过。。。然而在适配Android 10中它却是一个无法忽略的存在。因为Android 10的外部存储访问限制,我们无法像以前一样自由的操作文件。SAF就是应对这一限制的方法之一。
使用Intent.ACTION_OPEN_DOCUMENT
可以调起文件选择页面,选择一个文件。我以选择图片文件为例:
//通过系统的文件浏览器选择一个文件
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
//筛选,只显示可以“打开”的结果,如文件(而不是联系人或时区列表)
intent.addCategory(Intent.CATEGORY_OPENABLE);
//过滤只显示图像类型文件
intent.setType("image/*");
startActivityForResult(intent, REQUEST_CODE_FOR_SINGLE_FILE);
文件选择页面如下(系统MIUI 11):
在onActivityResult
获取文件Uri,同时也可以通过ContentResolver
查询文件信息:
private final String[] IMAGE_PROJECTION = { MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media._ID }; @Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) { Uri uri = null; if (resultData != null) { // 获取选择文件Uri uri = resultData.getData(); // 获取图片信息 Cursor cursor = this.getContentResolver() .query(uri, IMAGE_PROJECTION, null, null, null, null); if (cursor != null && cursor.moveToFirst()) { String displayName = cursor.getString(cursor.getColumnIndexOrThrow(IMAGE_PROJECTION[0])); String size = cursor.getString(cursor.getColumnIndexOrThrow(IMAGE_PROJECTION[1])); Log.i(TAG, "Uri: " + uri.toString()); Log.i(TAG, "Name: " + displayName); Log.i(TAG, "Size: " + size); } cursor.close(); } } }
这部分的用法我暂时也只在淘宝App -> 商品评论 -> 保存评论图片的地方看到过。有兴趣的可以去试试。
具体用法(我以创建txt文件为例):
public void createFile() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
// 文件类型
intent.setType("text/plain");
// 文件名称
intent.putExtra(Intent.EXTRA_TITLE, System.currentTimeMillis() + ".txt");
startActivityForResult(intent, WRITE_REQUEST_CODE);
}
交互页面如下:
获得文件的 Uri 后,就可以对其执行任何操作。
private Bitmap getBitmapFromUri(Uri uri) throws IOException {
ParcelFileDescriptor parcelFileDescriptor =
getContentResolver().openFileDescriptor(uri, "r");
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
parcelFileDescriptor.close();
return image;
}
InputStream
private String readTextFromUri(Uri uri) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
try (InputStream inputStream = getContentResolver().openInputStream(uri);
BufferedReader reader = new BufferedReader(
new InputStreamReader(Objects.requireNonNull(inputStream)))) {
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
}
return stringBuilder.toString();
}
private void alterDocument(Uri uri) { if (uri != null) { OutputStream outputStream = null; try { // 获取 OutputStream outputStream = getContentResolver().openOutputStream(uri); outputStream.write("Storage Access Framework Example".getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { Toast.makeText(this, "修改文件失败!", Toast.LENGTH_SHORT).show(); } finally { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.fillInStackTrace(); } } } } }
或
private void alterDocument(Uri uri) {
try {
ParcelFileDescriptor pfd = getContentResolver().
openFileDescriptor(uri, "w");
FileOutputStream fileOutputStream =
new FileOutputStream(pfd.getFileDescriptor());
fileOutputStream.write(("Storage Access Framework Example").getBytes());
fileOutputStream.close();
pfd.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
使用DocumentsContract.deleteDocument
方法进行删除。
public void deleteFile(Uri uri) {
if (uri != null) {
try {
DocumentsContract.deleteDocument(getContentResolver(), uri);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
使用Intent.ACTION_OPEN_DOCUMENT_TREE
可以调起文件目录选择页面,选择一个目录,并将其子文件夹的读写权限授予APP。
private void selectDir() {
// 用户可以选择任意文件夹,将它及其子文件夹的读写权限授予APP。
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_FOR_DIR);
}
交互页面如下:
在onActivityResult
获取目录的Uri,并创建DocumentFile
来进行文件操作:
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (requestCode == REQUEST_CODE_FOR_DIR && resultCode == Activity.RESULT_OK) {
Uri uriTree = null;
if (data != null) {
uriTree = data.getData();
}
if (uriTree != null) {
// 创建所选目录的DocumentFile,可以使用它进行文件操作
DocumentFile root = DocumentFile.fromTreeUri(this, uriTree);
// 比如使用它创建文件夹
DocumentFile dir = root.createDirectory(”Test“);
}
}
}
当然每次这样选择授权会很麻烦,所以我们也可以在首次授权时保存获取的目录权限:
// 获取权限
final int takeFlags = resultData.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(uri, takeFlags);
// 保存获取的目录权限
SharedPreferences sp = getSharedPreferences("DirPermission", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("uriTree", uri.toString());
editor.apply();
使用时从SharedPreferences
获取uriTree
,不存在或是无权限则重新授权:
SharedPreferences sp = getSharedPreferences("DirPermission", Context.MODE_PRIVATE); String uriTree = sp.getString("uriTree", ""); if (TextUtils.isEmpty(uriTree)) { // 重新授权 } else { try { Uri uri = Uri.parse(uriTree); final int takeFlags = getIntent().getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); getContentResolver().takePersistableUriPermission(uri, takeFlags); DocumentFile root = DocumentFile.fromTreeUri(this, uri); } catch (SecurityException e) { // 重新授权 } }
上面代码中使用到的takePersistableUriPermission
方法是为了检查最新的数据。防止另一个应用可能删除或修改了文件导致Uri失效。
有了授权就有撤销授权,使用releasePersistableUriPermission
或revokeUriPermission
方法就可以实现权限的撤销。
public void releasePermission(View view) { SharedPreferences sp = getSharedPreferences("DirPermission", Context.MODE_PRIVATE); String uriTree = sp.getString("uriTree", ""); if (!TextUtils.isEmpty(uriTree)) { Uri uri = Uri.parse(uriTree); final int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; getContentResolver().releasePersistableUriPermission(uri, takeFlags); // 或 this.revokeUriPermission(uri, takeFlags); // 重启才会生效,所以可以清除uriTree SharedPreferences.Editor editor = sp.edit(); editor.putString("uriTree", ""); editor.apply(); } }
或者在应用设置页面点击取消访问权限
手动删除(MIUI 11 上未发现此按钮):
本篇都是具体场景的的使用示例,完整的代码我已上传GitHub。可以去自行查看体验。
2021.02.02更新
Android 11对SAF添加以下限制:
ACTION_OPEN_DOCUMENT_TREE
或 ACTION_OPEN_DOCUMENT
,无法浏览到Android/data/
和 Android/obb/
目录。ACTION_OPEN_DOCUMENT_TREE
无法授权访问存储根目录、Download
文件夹。Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。