Using the Android Storage Access Framework

In Android 4.4 SDK Google introduced the Storage Access Framework. The Storage Access Framework provides an intuitive user interface that allows the user to browse, select, delete and create files hosted by storage services (also referred to as document providers) from within Android applications. Using this browsing interface (also referred to as the picker), users can, for example, browse through the files (such as documents, audio, images and videos) hosted by their chosen document providers.

Document providers can range from cloud-based services to local document providers running on the same device as the client application. In addition to cloud based document providers the picker also provides access to internal storage on the device, providing a range of file storage options to the application user.

Through a set of Intents included with Android 4.4, Android application developers can incorporate these storage capabilities into applications with just a few lines of code. A particularly compelling aspect of the Storage Access Framework from the point of view of the developer is that the underlying document provider selected by the user is completely transparent to the application. Once the storage functionality has been implemented using the framework within an application, it will work with all document providers without any code modifications.

Android 4.4 introduced a new set of Intents designed to integrate the features of the Storage Access Framework into Android applications. These intents display the Storage Access Framework picker user interface to the user and return the results of the interaction to the application via a call to the onActivityResult() method of the activity that launched the intent. When the onActivityResult() method is called, it is passed the Uri of the selected file together with a value indicating the success or otherwise of the operation.

The Storage Access Framework intents can be summarized as follows:

  • ACTION_OPEN_DOCUMENT – Provides the user with access to the picker user interface so that files may be selected from the document providers configured on the device. Selected files are passed back to the application in the form of Uri objects.
  • ACTION_CREATE_DOCUMENT – Allows the user to select a document provider, a location on that provider’s storage and a file name for a new file. Once selected, the file is created by the Storage Access Framework and the Uri of that file returned to the application for further processing.

The files listed within the picker user interface when an intent is started may be filtered using a variety of options. Consider, for example, the following code to start an ACTION_OPEN_DOCUMENT intent:

private static final int OPEN_REQUEST_CODE = 41;
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
startActivityForResult(intent, OPEN_REQUEST_CODE);

When executed, the above code will cause the picker user interface to be displayed, allowing the user to browse and select any files hosted by available document providers. Once a file has been selected by the user, a reference to that file will be provided to the application in the form of a Uri object. The application can then open the file using the openFileDescriptor(Uri, String) method. There is some risk, however, that not all files listed by a document provider can be opened in this way. The exclusion of such files within the picker can be achieved by modifying the intent using the CATEGORY_OPENABLE option. For example:

private static final int OPEN_REQUEST_CODE = 41;
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, OPEN_REQUEST_CODE);

When the picker is now displayed, files which cannot be opened using the openFileDescriptor() method will be listed but not selectable by the user.

Another useful approach to filtering allows the files available for selection to be restricted by file type. This involves specifying the types of the files the application is able to handle. An image editing application might, for example, only want to provide the user with the option of selecting image files from the document providers. This is achieved by configuring the intent object with the MIME types of the files that are to be selectable by the user.

The following code, for example, specifies that only image files are suitable for selection in the picker:

Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, OPEN_REQUEST_CODE);

This could be further refined to limit selection to JPEG images:

intent.setType("image/jpeg");

Alternatively, an audio player app might only be able to handle audio files:

intent.setType("audio/*");

Handling Intent Results

When an intent returns control to the application, it does so by calling the onActivityResult() method of the activity which started the intent. This method is passed the request code that was handed to the intent at launch time, a result code indicating whether or not the intent was successful and aresult data object containing the Uri of the selected file. The following code, for example, might be used as the basis for handling the results from the ACTION_OPEN_DOCUMENT intent outlined in the previous section:

public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
    Uri currentUri = null;
    if (resultCode == Activity.RESULT_OK) {
        if (requestCode == OPEN_REQUEST_CODE) {
            if (resultData != null) {
                currentUri = resultData.getData();
                readFileContent(currentUri);
            }
        }
    }
}

The above method verifies that the intent was successful, checks that the request code matches that for a file open request and then extracts the Uri from the intent data. The Uri can then be used to read the content of the file.

The exact steps required to read the content of a file hosted by a document provider will depend to a large extent on the type of the file. The steps to read lines from a text file, for example, differ from those for image or audio files.

An image file can be assigned to a Bitmap object by extracting the file descriptor from the Uri object and then decoding the image into a BitmapFactory instance. For example:

ParcelFileDescriptor pfd =
    getContentResolver().openFileDescriptor(uri, "r");
FileDescriptor fd = pfd.getFileDescriptor();
Bitmap bmp = BitmapFactory.decodeFileDescriptor(fd);
pdf.close();
imageView.setImageBitmap(bmp);

Note that the file descriptor is opened in "r" mode. This indicates that the file is to be opened for reading. Other options are "w" for write access and "rwt" for read and write access, where existing content in the file is truncated by the new content.

Reading the Content of a File

Reading the content of a text file requires slightly more work and the use of an InputStream object. The following code, for example, reads the lines from a text file:

InputStream inputStream = getContentResolver().openInputStream(uri);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String readline;
while ((readline = reader.readLine()) != null) {
    // Do something with each line in the file
}
inputStream.close();

Writing Content to a File

Writing to an open file hosted by a document provider is similar to reading with the exception that an output stream is used instead of an input stream. The following code, for example, writes text to the output stream of the storage based file referenced by the specified Uri:

try{ ParcelFileDescriptor pfd = this.getContentResolver() .openFileDescriptor(uri, "w"); FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor()); String txt = "Some sample text"; fos.write(txt.getBytes()); fos.close(); pfd.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }

Gaining Persistent Access to a File

When an application gains access to a file via the Storage Access Framework, the access will remain valid until the Android device on which the application is running is restarted. Persistent access to a specific file can be obtained by "taking" the necessary permissions for the Uri. The following code, for example, persists read and write permissions for the file referenced by the fileUri Uri instance:

final int takeFlags = intent.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

getContentResolver().takePersistableUriPermission(fileUri, takeFlags);

Once the permissions for the file have been taken by the application, and assuming the Uri has been saved by the application, the user should be able to continue accessing the file after a device restart without the user having to reselect the file from the picker interface.

If, at any time, the persistent permissions are no longer required, they can be released via a call to the releasePersistableUriPermission() method of the content resolver:

final int releaseFlags = intent.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

getContentResolver().releasePersistableUriPermission(fileUri,releaseFlags);

Example

public class MainActivity extends AppCompatActivity {
    private Button btn;

    private static final int OPEN_REQUEST_CODE = 41;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btn = findViewById(R.id.btn);

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
                intent.setType("*/*");
                startActivityForResult(intent, OPEN_REQUEST_CODE);
            }
        });
    }

    public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
        Uri currentUri = null;
        if (resultCode == Activity.RESULT_OK) {
            if (requestCode == OPEN_REQUEST_CODE) {
                if (resultData != null) {
                    currentUri = resultData.getData();
                    Log.d("TAG", "onActivityResult: " + currentUri);;
                }
            }
        }
    }
}

Result

android_storage_access_framework.png