Implementing Scoped Storage in Android 10

If you follow my writing (here or here), then you know I’ve been working on a new Android app called AfterShoot — it’s an AI-powered Android app that helps users take better pictures while also managing their digital waste.

One of the major features that I had to implement in this app was the ability to query a user’s internal storage for all the images—only then will I be able to run any ML inference on them.

Scoped Storage: What is it?

The task of fetching all the images from a user’s storage might seem trivial; after all, multiple StackOverflow links come up as soon as you search this query on google (here, here, here, and here).

But storage on Android has a checkered past, with the implementation being changed multiple times throughout its lifetime. And if you try to run the code provided in any of the answers above on a device running Android 10, you simply won’t get any images! Why you may ask? The answer is scoped storage, which was introduced in Android 10.

Scoped storage essentially limits your app’s access to the user’s internal storage, especially in terms of where it can write its files. You might be wondering, what was the need for such a system? Well, if an app is granted storage permissions, then by default it can write its files anywhere throughout a device’s entire internal storage.

While a best practice is saving your app’s data in the Android/data folder of the user’s internal storage, a lot of apps abuse this by storing their app’s contents all over the place, which leads to bad storage management, as uninstalling the app doesn’t remove these folders.

To give users more control over their files and to limit file clutter, apps that target Android 10 (API level 29) and higher are given scoped access into external storage, or scoped storage, by default. Such apps have access only to the app-specific directory on external storage, as well as specific types of media that the app has created.

Migrating AfterShoot to Scoped Storage

With AfterShoot, I’m trying to access files not created by my app (the images that the user captures are created by the camera app and not my app). So using a traditional method won’t cut it on Android 10, and I won’t get any images back.

Here’s how my current code looks:

val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI

// DATA is deprecated and might be removed in upcoming Android Versions
val projection = arrayOf(MediaStore.MediaColumns.DATA)

private fun queryStorage() {
        val query = contentResolver.query(
                uri,
                projection,
                null,
                null,
                null
        )
        query.use { cursor ->

            cursor?.let {

                while (cursor.moveToNext()) {
                    val absolutePathOfImage = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA));
                    imageList.add(Image(File(absolutePathOfImage)))
                }
            }
        }
    }

While it looks simple, this code doesn’t work on Android 10. Let’s see how we can modify this code to make the app compatible with Android 10!

This is the easiest way to make the app run on Android 10. You just open your AndroidManifest.xml file and add the following line to your <application> tag:

This will mark an exception for your app in the Android OS for scoped storage, allowing you to use legacy solutions in your app without issues. However, this is temporary, and it might not work in upcoming Android versions. This tag is there only to give developers some time before they eventually migrate to scoped storage.

Method 2: Doing it correctly

While the first method is temporary and gives you some time until you actually implement scoped storage, let’s see how it can be done properly.

Step 1: Add and request storage permissions

Before doing anything else, it’s essential that we add and request storage permissions in the app. If you’re new to Android and runtime permissions, this is how you can do it:

Step 2: Define the details you want to retrieve

Before fetching the images, we need to decide what details about the image we need to get. For example, we can ask for its name, the date on which it was taken, its size, width, height, etc.

Once we’ve decided upon the details we need to query for, let’s create a Kotlin class that will be used to store these details. For AfterShoot, this is what it looks like:

data class Image(val uri: Uri,
                 val name: String,
                 val size: String,
                 val dateTaken: String?)

Once this is done, we then need to create a method called queryStorage() that will query the internal storage and return the requested details for us.

We’ll be running this query on the contentResolver, and it looks very similar to the SQLiteDatabase’s query method that you might have used while implementing a SQLite database in Android.

Before we call the query method, we need to define which columns we want in return; along with an optional WHERE query and SORT order.

// method that fetches all the images from the internal storage and returns an arrayList of images    
fun queryScopedStorage() : ArrayList<Image> {
        // Projection is an array that returns what all info we want to get back
        val projection = arrayOf(
                // Name of the image
                MediaStore.Images.Media.DISPLAY_NAME,
                // Size of the image
                MediaStore.Images.Media.SIZE,
                // The date on which the image was taken
                MediaStore.Images.Media.DATE_TAKEN,
                // A unique ID of the image, we'll be using this to create our Image URI later
                MediaStore.Images.Media._ID
        )

        // Sort order that tells that our images should be sorted in the descending order of the date on which they were taken
        val sortOrder = "${MediaStore.Images.Media.DATE_TAKEN} DESC"
      
        ...
}

Step 3: Running query on the contentResolver

The next step for us would be to run the query on our contentResolver and obtain the images we want.

This is what the code looks like:

// method that fetches all the images from the internal storage and returns an arrayList of images    
fun queryScopedStorage() : ArrayList<Image> {
        // Projection is an array that returns what all info we want to get back
        val projection = arrayOf(
                // Name of the image
                MediaStore.Images.Media.DISPLAY_NAME,
                // Size of the image
                MediaStore.Images.Media.SIZE,
                // The date on which the image was taken
                MediaStore.Images.Media.DATE_TAKEN,
                // A unique ID of the image, we'll be using this to create our Image URI later
                MediaStore.Images.Media._ID
        )

        // Sort order that tells that our images should be sorted in the descending order of the date on which they were taken
        val sortOrder = "${MediaStore.Images.Media.DATE_TAKEN} DESC"
        
        // running the query on our contentResolver
        val cursor = contentResolver.query(
                // In order to access the files created by my application, I have to query MediaStore.Images.Media.INTERNAL_CONTENT_URI
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                scopedProjection,
                // no selection args
                null,
                null,
                sortOrder
        )

        ...
}

The query() method will return a Cursor we can use to access the data of the columns listed in the projection.

Step 4: Extracting data from the cursor

Once you’ve obtained the cursor, the last step is to extract the necessary content from it.

Here’s what the code for this will look like:

// method that fetches all the images from the internal storage and returns an arrayList of images    
fun queryScopedStorage() : ArrayList<Image> {
        ...
        
        val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
        val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
        val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)
        val dateColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_TAKEN)
        
        // iterate all the images
        while (cursor.moveToNext()) {
             val id = cursor.getLong(idColumn)
             val name = cursor.getString(nameColumn)
             val size = cursor.getString(sizeColumn)
             val date = cursor.getString(dateColumn)
              
             // append the ID to the content:// URI for the image
             val contentUri = ContentUris.withAppendedId(
                              MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                              id)
             // create an Image object and add it to the list of images 
             imageList.add(Image(contentUri, name, size, date))
          
        }
        
        // return the list of images
        return imageList
}

If you run this code, you’ll see that you’re able to retrieve all the images from your phone’s internal storage, sorted in descending order by their captured dates.

One last optional step is to also extract the thumbnail for each image and store that in the Image class as well. Loading a thumbnail instead of the full-res image is always a good option, as it helps cut down on an app’s memory footprint.

Here’s how this can be done:

// method that fetches all the images from the internal storage and returns an arrayList of images    
fun queryScopedStorage() : ArrayList<Image> {
        ...
        
        val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
        val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
        val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)
        val dateColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_TAKEN)
        
        // iterate all the images
        while (cursor.moveToNext()) {
             val id = cursor.getLong(idColumn)
             val name = cursor.getString(nameColumn)
             val size = cursor.getString(sizeColumn)
             val date = cursor.getString(dateColumn)
              
             // append the ID to the content:// URI for the image
             val contentUri = ContentUris.withAppendedId(
                              MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                              id)
         
             // generate the thumbnail
             val thumbnail = (this as Context).contentResolver.loadThumbnail(contentUri, Size(480, 480), null)
             imageList.add(Image(contentUri, thumbnail, name, size, date))
             
             imageList.add(Image(contentUri, name, size, date))
          
        }
        
        // return the list of images
        return imageList
}

And that’s it! With just this simple fix, AfterShoot is now able to fetch images from internal storage without any issues on both Android 10 and any Android versions below it.

If you want to explore the complete source code, you can find it in the GitHub repository for AfterShoot (look at the MainActivity.kt file to find code relevant to this blog post):

Thanks for reading! If you enjoyed this story, please click the 👏 button and share it to help others find it! Feel free to leave a comment 💬 below.

Have feedback? Let’s connect on Twitter.

Avatar photo

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *