Android 4.4 (API level 19) introduces the Storage Access Framework (SAF), which enables users to easily browse and open documents, images, and other files in all of their preferred document Storage providers. Users can browse files and access recently used files in a uniform manner across all applications and providers through an easy-to-use standard UI.

Cloud storage services or local storage services can participate in this ecosystem by implementing documentsProviders that encapsulate their services. It takes just a few lines of code to integrate client applications that need access to provider documents with SAF.

The SAF includes the following:

  • Document provider: A content provider that allows a storage service, such as Google Drive, to display the files it manages. The document provider is implemented as a subclass of the DocumentsProvider class. The document provider’s architecture is based on a traditional file hierarchy, but the actual data storage is up to you. The Android platform includes several built-in document providers, such as Downloads, Images, and Videos.
  • Client application: A custom application that calls ACTION_OPEN_DOCUMENT and/or ACTION_CREATE_DOCUMENT Intent and receives the file returned by the document provider;
  • Picker: A system UI that allows the user to access documents within all document providers that meet the search criteria of the client application. Here is a picker on Google’s N6

The specific storage access framework flow is shown below

  • Allow users to browse through all document providers, not just content in a single application;
  • Give your application long-term, persistent access to documents owned by document providers. This access allows users to add, edit, save, and delete files on the provider;
  • Support for multiple user accounts and temporary root directories, such as USB storage providers that only appear after a drive is inserted.

How to use SAF

On Android 4.3 and later, if an app wants to retrieve a file from another app, it must call an Intent with ACTION_PICK or ACTION_GET_CONTENT. For Android 4.4 and later, an Intent with ACTION_OPEN_DOCUMENT is used. This triggers a Picker, which then finds the data source that matches the Intent request from all the registered providers. After the user selects a file/directory, the Picker will return the data to the client in the form of A Uri, so that the client can get the read and write permission of these files/directories and do the desired processing. Note that ACTION_OPEN_DOCUMENT is not designed to replace ACTION_GET_CONTENT. The Intent to use depends on the needs of the application:

  • If you only want your application to read/import data, use ACTION_GET_CONTENT. When using this method, the application imports a copy of the data (such as an image file);
  • If you want your application to gain long-term, persistent access to documents owned by the document provider, use ACTION_OPEN_DOCUMENT, for example, a photo editing application that allows users to edit images stored in the document provider.

Get the document

private static final int READ_REQUEST_CODE = 42; . public voidperformFileSearch() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); Intent.addcategory (intent.category_openable); Intent.settype (intent.intent.setType (intent.intent.setType))"image/*"); // Intent.setType (intent.setType)"* / *");
    startActivityForResult(intent, READ_REQUEST_CODE);
}
Copy the code

The processing results

@Override public void onActivityResult(int requestCode, Int resultCode,Intent resultData) {// Use resultData.getData () to retrieve the URIif (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        Uri uri = null;
        if(resultData ! = null) { uri = resultData.getData(); Log.i(TAG,"Uri: "+ uri.toString()); showImage(uri); }}}Copy the code

InputStream = InputStream = InputStream = InputStream = InputStream = InputStream

private String readTextFromUri(Uri uri) throws IOException {
    InputStream inputStream = getContentResolver().openInputStream(uri);
    BufferedReader reader = new BufferedReader(new InputStreamReader(
            inputStream));
    StringBuilder stringBuilder = new StringBuilder();
    String line;
    while((line = reader.readLine()) ! = null) { stringBuilder.append(line); } fileInputStream.close(); parcelFileDescriptor.close();return stringBuilder.toString();
}
Copy the code

Preserve permissions

When an application opens a file for reading or writing, the system provides our application with URI authorization for the file. This authorization lasts until the user’s device is restarted. But let’s say our application is an image editing application, and we want users to be able to access the last five images they edit directly from the application. If the user’s device has been restarted, you need to refer the user back to the system picker to find these files, which is obviously not ideal.

To prevent this, you can preserve the permissions granted to your application. The application actually obtains the persistent URI authorization provided by the system. This allows users to continuously access files through your application, even if the device has been restarted:

final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); / / check the latest data access getContentResolver () takePersistableUriPermission (uri, takeFlags);Copy the code

Gets the real path to the file

The SAF application can only get the Uri of the file selected by the user. Sometimes we need the absolute path of the file. To get the absolute path of the file, see Stackoverflow

public static String getPath(Context context, Uri uri) { String path = null; / / file: at the beginningif (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
            path = uri.getPath();
            returnpath; } / / to the content: / / at the beginning of, such as the content: / / media/extenral/images/media / 17766if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            Cursor cursor = context.getContentResolver().query(uri, new String[]{MediaStore.Images.Media.DATA}, null, null, null);
            if(cursor ! = null) {if (cursor.moveToFirst()) {
                    int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
                    if (columnIndex > -1) {
                        path = cursor.getString(columnIndex);
                    }
                }
                cursor.close();
            }
            returnpath; } / / 4.4if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            if (DocumentsContract.isDocumentUri(context, uri)) {
                if(isExternalStorageDocument(uri)) { // ExternalStorageProvider final String docId = DocumentsContract.getDocumentId(uri);  final String[] split = docId.split(":");
                    final String type = split[0];
                    if ("primary".equalsIgnoreCase(type)) {
                        path = Environment.getExternalStorageDirectory() + "/" + split[1];
                        returnpath; }}else if (isDownloadsDocument(uri)) {
                    // DownloadsProvider
                    final String id = DocumentsContract.getDocumentId(uri);
                    final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"),
                            Long.valueOf(id));
                    path = getDataColumn(context, contentUri, null, null);
                    return path;
                } else if (isMediaDocument(uri)) {
                    // MediaProvider
                    final String docId = DocumentsContract.getDocumentId(uri);
                    final String[] split = docId.split(":");
                    final String type = split[0];
                    Uri contentUri = null;
                    if ("image".equals(type)) {
                        contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                    } else if ("video".equals(type)) {
                        contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                    } else if ("audio".equals(type)) {
                        contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                    }
                    final String selection = "_id=?";
                    final String[] selectionArgs = new String[]{split[1]};
                    path = getDataColumn(context, contentUri, selection, selectionArgs);
                    returnpath; }}}return null;
    }

    private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
        Cursor cursor = null;
        final String column = "_data";
        final String[] projection = {column};
        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
            if(cursor ! = null && cursor.moveToFirst()) { final int column_index = cursor.getColumnIndexOrThrow(column);return cursor.getString(column_index);
            }
        } finally {
            if(cursor ! = null) cursor.close(); }return null;
    }

    private static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    private static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    private static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }
Copy the code