SAF(Storage Access Framework)使用攻略

漫长的假期,在家整理了一下Android 10的适配内容。由于适配篇的篇幅问题,就将这一部本单独出来,也先放出来。java

1.介绍

Android 4.4 就引入了存储访问框架 (SAF)。借助 SAF,用户可轻松在其全部首选文档存储提供程序中浏览并打开文档、图像及其余文件。用户可经过易用的标准界面,以统一方式在全部应用和提供程序中浏览文件,以及访问最近使用的文件。android

SAF 提供的部分功能:git

  • 让用户浏览全部文档提供程序的内容,而不单单是单个应用的内容。
  • 让您的应用得到对文档提供程序所拥有文档的长期、持续性访问权限。用户可经过此访问权限添加、编辑、保存和删除提供程序上的文件。
  • 支持多个用户账户和临时根目录,如只有在插入驱动器后才会出现的 USB 存储提供程序。

虽然说早在Android 4.4就已经引入了,可是我却从未使用过。。。然而在适配Android 10中它倒是一个没法忽略的存在。由于Android 10的外部存储访问限制,咱们没法像之前同样自由的操做文件。SAF就是应对这一限制的方法之一。github

2.使用

选择文件

使用Intent.ACTION_OPEN_DOCUMENT能够调起文件选择页面,选择一个文件。我以选择图片文件为例:c#

//经过系统的文件浏览器选择一个文件
    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查询文件信息:app

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文件为例):ide

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);
    }
复制代码

交互页面以下:ui

在这里插入图片描述

读取文件

得到文件的 Uri 后,就能够对其执行任何操做。

  1. 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;
    }
复制代码
  1. 获取 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失效。

有了受权就有撤销受权,使用releasePersistableUriPermissionrevokeUriPermission方法就能够实现权限的撤销。

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。能够去自行查看体验。

3.参考

相关文章
相关标签/搜索