Android Native - Open documents via the Storage Access Framework

dimitrilc 1 Tallied Votes 391 Views Share

Introduction

The Storage Access Framework (SAF) provides a great way to access files exposed by other applications via their own DocumentProviders. In this tutorial, we will learn how to use the SAF in our App.

Goals

At the end of the tutorial, you would have learned:

  1. How to open documents via the Storage Access Framework.

Tools Required

Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.

Prerequisite Knowledge

  1. Intermediate Android.
  2. ActivityResult API.
  3. Intents

Project Setup

To follow along with the tutorial, perform the steps below:

  1. You are commended to use a test device or AVD image with the Google Play Store pre-installed.

  2. Install Adobe Acrobat Reader: Edit PDF

  3. Using the default Chrome browser on your AVD, download a sample PDF file from this project’s Github repository here. This ensures that the downloaded file will be written to the MediaStore.Downloads table.

  4. Create a new Android project with the default Empty Activity.

  5. Delete the default “Hello World!” TextView.

  6. Add a new Button to ConstraintView.

  7. Constraint the Button to the center of the screen.

  8. Extract the android:text of the Button to strings.xml with the value of Open PDF.

  9. Your acitivty_main.xml file should now look similar to this.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/open_pdf"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>

Project Overview

Our project is quite simple. It only has a single Button View. When completed, we expect our App to successfully provide the workflow below:

  1. The end user taps on the button.
  2. Android provides the user with a Picker to select PDF files.
  3. The end user selects a PDF file in the Picker.
  4. Our App starts an implicit Intent to let Android decide which App to handle the Intent.
  5. The user selects the Adobe PDF reader to read the PDF file.
  6. Android opens the Adobe PDF reader, passing in the URI for the file selected previously at step 3.
  7. The user can now view the PDF file.

Storage Access Framework Concepts

The SAF has three components:

  1. Document provider: a class that implements DocumentsProvider. This class can provide read and write access to files either locally or cloud-based. It must also include the proper <provider> tag in its manifest. Android already provides some default DocumentsProvider for us to use, so we do not have to implement them by ourselves in this tutorial. When the file Picker is shown, all registered DocumentProviders will be listed. In the screenshot below, each of the listings in red is a DocumentProvider.
    1.jpg
  2. Client: an App that will make use of the Document Provider via the intent actions ACTION_CREATE_DOCUMENT, ACTION_OPEN_DOCUMENT, or ACTION_OPEN_DOCUMENT_TREE. For this tutorial, we will not use Intents and these intent actions directly, instead we will leverage the premade AcitvityResultContract implementations from ActivityResultContracts.
  3. Picker: A file Picker UI provided by the system, so we do not have to implement it by ourselves either.

Open the file Picker

With the basic concepts out of the way, it is time to write some code. The first thing that we would need to do is to launch the file Picker with our App.

  1. Append to MainActivity#onCreate with the code below to create an ActivityResultLauncher and an empty callback. Notice that we used the premade ActivityResultContract implementation ActivityResultContracts.OpenDocument here.

     val openPdfLauncher = registerForActivityResult(OpenDocument()) { uri ->
    
     }
  2. Retrieve the Button object.

     //Gets a reference to the Button object
     val button = findViewById<Button>(R.id.button)
  3. Binds the Button’s onClickListener to launch(). You can pass a String array to filter out files by MIME types. The first two commented out array declarations are just examples to give you some ideas.

     //Binds button to start the file Picker
     button.setOnClickListener {
        //val input = emptyArray<String>() //this will match nothing.
        //val input = arrayOf("text/plain") //if you want to filter .txt files
        val input = arrayOf("application/pdf")
        openPdfLauncher.launch(input)
     }

Performs an action on the returned Uri

The code provided so far will only launch the file Picker and then return to our App, doing nothing else. To read the PDF file with an external App, add the code below inside of the openPdfLauncher callback. Notice my comment regarding the use of the FLAG_GRANT_READ_URI_PERMISSION below, so you should be careful when passing the permission via an implicit intent.

//Creates the Intent object, specifying the uri and mime type
val openPdfIntent = Intent().apply {
   action = Intent.ACTION_VIEW
   type = "application/pdf"
   data = uri
   //grant the handler permission to read the Uri.
   //Should sanitize to avoid leaking sensitive user information.
   flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}

//Asks the system to start another activity that can handle Intent
startActivity(openPdfIntent)

Run the App

We are now ready to run the App. You should be able to replicate the expected user flow like in the animation below.

OpenPDF.gif

Solution Code

MainActivity.kt

package com.example.daniwebandroidstorageaccessframework

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import androidx.activity.result.contract.ActivityResultContracts.OpenDocument

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val openPdfLauncher = registerForActivityResult(OpenDocument()) { uri ->


           //Creates the Intent object, specifying the uri and mime type
           val openPdfIntent = Intent().apply {
               action = Intent.ACTION_VIEW
               type = "application/pdf"
               data = uri
               //grant the handler permission to read the Uri.
               //Should sanitize to avoid leaking sensitive user information.
               flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
           }

           //Asks the system to start another activity that can handle Intent
           startActivity(openPdfIntent)
       }

       //Gets a reference to the Button object
       val button = findViewById<Button>(R.id.button)

       //Binds button to start the file Picker
       button.setOnClickListener {
           //val input = emptyArray<String>() //this will match nothing.
           //val input = arrayOf("text/plain") //if you want to filter .txt files
           val input = arrayOf("application/pdf")
           openPdfLauncher.launch(input)
       }

   }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/open_pdf"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Android Storage Access Framework</string>
   <string name="open_pdf">Open PDF</string>
</resources>

Summary

We have learned how to open a document using the Storage Access Framework. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidStorageAccessFramework

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.