SAF(Storage Access Framework)使用攻略
Posted 唯鹿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SAF(Storage Access Framework)使用攻略相关的知识,希望对你有一定的参考价值。
漫长的假期,在家整理了一下android 10的适配内容。因为适配篇的篇幅问题,就将这一部本单独出来,也先放出来。
1.介绍
Android 4.4 就引入了存储访问框架 (SAF)。借助 SAF,用户可轻松在其所有首选文档存储提供程序中浏览并打开文档、图像及其他文件。用户可通过易用的标准界面,以统一方式在所有应用和提供程序中浏览文件,以及访问最近使用的文件。
SAF 提供的部分功能:
- 让用户浏览所有文档提供程序的内容,而不仅仅是单个应用的内容。
- 让您的应用获得对文档提供程序所拥有文档的长期、持续性访问权限。用户可通过此访问权限添加、编辑、保存和删除提供程序上的文件。
- 支持多个用户帐户和临时根目录,如只有在插入驱动器后才会出现的 USB 存储提供程序。
虽说早在Android 4.4就已经引入了,但是我却从未使用过。。。然而在适配Android 10中它却是一个无法忽略的存在。因为Android 10的外部存储访问限制,我们无法像以前一样自由的操作文件。SAF就是应对这一限制的方法之一。
2.使用
选择文件
使用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 后,就可以对其执行任何操作。
- Bitmap
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();
选择目录(Android 5.0以上支持)
使用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
文件夹。
3.参考
以上是关于SAF(Storage Access Framework)使用攻略的主要内容,如果未能解决你的问题,请参考以下文章
Android API Guides---Storage Access Framework
Uncaught InvalidArgumentException: Please provide a valid cache path. in /apps/vendor/laravel/framew
无法完成安装:'Cannot access storage file '/
使用 MTP 通过 Android Storage Access Framework / DocumentProvider 遍历目录层次结构的问题
ClickHouse: ACCESS_STORAGE_FOR_INSERTION_NOT_FOUND when creating new user.
gsutil ServiceException: 401 Anonymous caller does not have storage.objects.list access to bucket 即使