Scoped Storage in Android 10: Getting Started
In this tutorial, you’ll learn how to use scoped storage in your Android 10 app by building a simple image gallery. By Anshdeep Singh.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Scoped Storage in Android 10: Getting Started
20 mins
- Getting Started
- What Is Scoped Storage?
- Why Do You Need Scoped Storage?
- Implementing Non-Scoped Storage in Android 10
- Adding the Required Permissions
- Fetching Images Using MediaStore
- Deleting an Image From MediaStore
- Listening for Changes With ContentObserver
- Registering the ContentObserver
- Unregistering the ContentObserver
- Where to Go From Here?
Working with the file system is an important part of developing any Android app. Up until Android 10, when you gave an app storage permission, it could access any file on the device. However, most apps don’t need access to the whole storage system. They usually perform tasks on a single file or a small group of files. This created a threat to user privacy.
In Android 10, Google introduced the concept of scoped storage, which enhances user control and privacy while cutting back the file clutter that removed apps leave behind.
In this tutorial, you’ll build a simple gallery app called Scopeo, which displays the photos from the device’s shared storage. Along the way, you’ll learn about:
- What scoped storage is and why you need it.
- Adding the correct permissions when working with files.
- Using
MediaStore
APIs to access the files. - How to opt out of scoped storage, if required.
-
Deleting files in
MediaStore
directly by using anIntentSender
.
You should also have some familiarity with Google’s architecture components, such as LiveData and ViewModel. Go through Android Jetpack Architecture Components: Getting Started to learn more about them.
Getting Started
Click the Download Materials button at the top or bottom of the page to download the starter and final projects.
Open Android Studio 3.6.1 or later and choose Open an existing Android Studio project. Then navigate to the starter project directory inside the main folder and click Open. Wait for Gradle to sync successfully and take some time to familiarize yourself with the code.
As you see from the screenshot above, the starter project contains the following files:
-
Image.kt is a simple data class that contains some properties of the image. It also includes a
DiffCallback
to efficiently update the image list when the user deletes an image. -
MainActivity.kt is where all the UI interactions occur. It contains a lot of boilerplate code already implemented for you. For simplicity, it also contains
GalleryAdapter
andImageViewHolder
in the same file. -
MainActivityViewModel.kt contains all the business logic of the app. You’ll add code in this class to perform time-consuming operations in the background. You’ll observe the changes using
LiveData
. All the background tasks use the recommended approach, Kotlin coroutines, which work well withViewModel
.
DiffUtil.Callback
, look at Android’s callback documentation or the official Kotlin coroutines documentation.
Now that you’ve looked over the codebase, build and run. You’ll see the following screen:
OPEN ALBUM doesn’t do anything right now. As you progress through the tutorial, you’ll build a completely functional gallery app.
What Is Scoped Storage?
The Storage Access Framework (SAF), introduced in Android 4.4, made it easy for developers to browse and open files on the device. Intent actions like ACTION_CREATE_DOCUMENT
, ACTION_OPEN_DOCUMENT
and ACTION_OPEN_DOCUMENT_TREE
perform the required operations.
Although it works, SAF is slow and highly unpopular among the developer community.
Scoped storage came on the scene when Android 10 officially launched on September 3, 2019. It changed things a bit. As the name suggests, it provides scoped — or limited — access to the file system. Apps that use scoped storage have access only to their app directory on external storage plus any media the app created.
Imagine you’re creating a voice recorder app. If you implement scoped storage in your app for Android 10 and above, you’ll have a limited scope for reading and writing files. Since your audio files reside in the app directory, you don’t need permission to access or modify them.
Why Do You Need Scoped Storage?
There are three main reasons for using scoped storage:
- Improving security: Developers should have control over the files their apps create. Scoped storage provides this control, letting developers work with files without having to request storage permissions At the same time, scoped storage also provides better app data protection because other apps can’t access these files easily.
- Reducing leftover app data: When a user uninstalls an app from their device, some app data still remains in the shared storage. With scoped storage, this problem disappears as all the app data resides exclusively in the app directory.
- Limiting the abuse of READ_EXTERNAL_STORAGE permission: Developers have abused this permission heavily because it gave them access to the entire external storage. Using scoped storage, apps can access only their own files, folders and other media file types using storage APIs.
Even though scoped storage is useful, there may be times when you don’t want to use it. Luckily, you can still opt out of using it in Android 10.
Implementing Non-Scoped Storage in Android 10
An easy way to opt out of scoped storage is to set requestLegacyExternalStorage
in your application
in AndroidManifest.xml to true
.
This attribute is false
by default on apps targeting Android 10 and higher. If you set it to true
, it will make an exception for your app, allowing you to use legacy storage solutions. This grants access to different directories and media files without any issues.
This flag gives developers more time to test their apps before migrating to scoped storage. However, this isn’t recommended because, from Android 11 onwards, this attribute will no longer be available.
Now, it’s time to get started with using scoped storage in an actual app.
Adding the Required Permissions
For your first step, you’ll add permissions to let your app access and modify images from other apps.
Open AndroidManifest.xml and paste the following code just below the TODO
:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
Here, you use READ_EXTERNAL_STORAGE
permission to access images taken by other apps. WRITE_EXTERNAL_STORAGE
permission lets you delete these images. You set android:maxSdkVersion
to 28
because in Android 10 and above, you don’t need this permission anymore.
Later in the tutorial, you’ll explicitly ask the user’s permission to handle image deletion in Android 10.
Next, open MainActivity.kt and add the following permissions inside requestPermissions()
:
if (!haveStoragePermission()) {
val permissions = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
ActivityCompat.requestPermissions(
//1
this,
//2
permissions,
//3
READ_EXTERNAL_STORAGE_REQUEST
)
}
The code above requests runtime permissions, which are mandatory from Android 6.0 and higher.
requestPermissions()
— from the support library — takes three parameters:
- The reference of the
Activity
requesting permissions. - A string array of the required permissions.
- The
requestCode
, which must be unique sinceonRequestPermissionsResult()
uses this same code to handle various user actions.
Build and run. Now, tap OPEN ALBUM, which shows a dialog asking the user for storage permission:
Tap Deny and the app will show the rationale for the permission.
Now, when you tap GRANT PERMISSION and then tap Allow, it will show an empty screen as below:
The screen is empty because it doesn’t contain any images yet. You’ll add them next!
Fetching Images Using MediaStore
Open MainActivityViewModel.kt and add the following inside queryImages()
, just after the // TODO
comment.
// 1
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_TAKEN
)
// 2
val selection = "${MediaStore.Images.Media.DATE_TAKEN} >= ?"
// 3
val selectionArgs = arrayOf(
dateToTimestamp(day = 1, month = 1, year = 2020).toString()
)
// 4
val sortOrder = "${MediaStore.Images.Media.DATE_TAKEN} DESC"
// 5
getApplication<Application>().contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)?.use { cursor ->
imageList = addImagesFromCursor(cursor)
}
Add any missing imports by pressing Option-Enter on Mac or Alt-Enter on PC.
Here’s a step-by-step breakdown:
- projection: An array that contains all the information you need. It’s similar to the SELECT clause of an SQL statement.
-
selection: Similar to the WHERE clause in SQL, this lets you specify any condition. The
?
in the statement is a placeholder that will get its value fromselectionArgs
. -
selectionArgs: An array containing the value that will replace
?
in the statement stored inselection
. In this case, you’re requesting all the images from this year.dateToTimestamp()
is a utility function that accepts a day, a month and a year. It returns the corresponding timestamp value, whichselectionArgs
requires. - sortOrder: As the name suggests, this contains the order to return the images. The default order is ascending, but here you add the DESC keyword after the variable name to switch to descending order.
-
query(): A method of
ContentResolver
that takes in all the above as parameters as well as an additionalUri
parameter that maps to the required table in the provider. In this case, the requiredUri
isEXTERNAL_CONTENT_URI
since you are requesting images from outside the app.Uri
is always mandatory. Hence, it is a non-nullable parameter while the rest of the parameters are nullable.
Phew! That was a lot to take in. Keep it up.
Build and run to see what you’ve achieved so far. Assuming you granted the permission earlier, you’ll now see some photos instead of the blank screen:
The images will be different on your device. If you haven’t taken a picture in the year 2020, then the screen will still be blank. In that case, go ahead and take a selfie with your cat or dog! When you open the app again, you’ll see your picture there. Tap any image in the grid and it will show a delete dialog.
Tapping the DELETE button won’t do anything yet. Get ready to delete an image!
Deleting an Image From MediaStore
Jump to performDeleteImage()
inside MainActivityViewModel.kt and add the following code:
try {
// 1
getApplication<Application>().contentResolver.delete(
image.contentUri,"${MediaStore.Images.Media._ID} = ?",
arrayOf(image.id.toString())
)
}
// 2
catch (securityException: SecurityException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val recoverableSecurityException =
securityException as? RecoverableSecurityException
?: throw securityException
pendingDeleteImage = image
_permissionNeededForDelete.postValue(
recoverableSecurityException.userAction.actionIntent.intentSender
)
} else {
throw securityException
}
}
Add any missing imports by pressing Option-Enter on Mac or Alt-Enter on PC.
There are a few important things you should know about this block of code:
- Here, you call
contentResolver.delete()
inside atry
block since this method can throw aSecurityException
at runtime. The method requires theContentUri
of the image you want to delete. In thewhere
parameter, you specify that you want to delete an image based on its_ID
. In the final parameter, you pass the string version of the_ID
in an array. - In Android 10 and above, it isn’t possible to delete or modify items from
MediaStore
directly. You need permission for these actions. The correct approach is to first catchRecoverableSecurityException
, which contains anintentSender
that can prompt the user to grant permission. You passintentSender
to the activity by callingpostValue()
on yourMutableLiveData
.
MutableLiveData
and postValue()
from Android’s MutableLiveData documentation.
Now, go to MainActivity.kt and add the following code to viewModel.permissionNeededForDelete.observe()
, just after the // TODO
comment.
intentSender?.let {
startIntentSenderForResult(
intentSender,
DELETE_PERMISSION_REQUEST,
null,
0,
0,
0,
null
)
}
startIntentSenderForResult()
launches intentSender
, which you passed to it. DELETE_PERMISSION_REQUEST
is a unique request code used to identify and handle the action when the request completes.
Before you try the new delete feature, Scopeo needs a few more finishing touches!
Listening for Changes With ContentObserver
ContentObserver
is a class that listens for changes whenever the data in the content provider changes. Since data will change whenever you delete any image in the app, you need to use a ContentObserver
.
Registering the ContentObserver
Start by registering the ContentObserver
.
Open MainActivityViewModel.kt and add the following code inside loadImages()
, just after the // TODO
comment.
contentObserver = getApplication<Application>().contentResolver.registerObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
) {
loadImages()
}
The code above just calls the extension method ContentResolver.registerObserver(uri: Uri, observer: (selfChange: Boolean) -> Unit)
, which is already implemented as shown below:
/**
* Extension method to register a [ContentObserver]
*/
private fun ContentResolver.registerObserver(
uri: Uri,
observer: (selfChange: Boolean) -> Unit
): ContentObserver {
// 1
val contentObserver = object : ContentObserver(Handler()) {
override fun onChange(selfChange: Boolean) {
observer(selfChange)
}
}
// 2
registerContentObserver(uri, true, contentObserver)
return contentObserver
}
Look closely at the code, and you’ll notice two things are happening:
-
contentObserver
overridesonChange()
. This method defines what happens if the data in the provider changes. In this case, it will callloadImages()
passed as a lambda. A best practice is to always use aHandler()
when creatingContentObserver
. - Next, the extension method registers the
ContentObserver
using theuri
passed to it. The second parameter passed astrue
indicates that all the other descendant URIs, starting with the given URI, should trigger the method call. The final parameter is the instance of theContentObserver
you created earlier.
Now that you’ve learned how to register the ContentObserver
, take a moment to find out how and why to unregister it again.
Unregistering the ContentObserver
Being a good Android citizen, you should also unregister your ContentObserser
to prevent memory leaks. Add the following code to onCleared()
inside MainActivityViewModel.kt.
contentObserver?.let {
getApplication<Application>().contentResolver.unregisterContentObserver(it)
}
This code just calls unregisterContentObserver()
of the ContentResolver
. ViewModel
calls onCleared()
when it’s not used anymore, so it’s the perfect place to unregister.
Well, it’s finally done! Go ahead and run your app to check out the delete feature. If you try deleting an image on an Android 10 device, it will now ask for permission:
The dialog won’t show on devices running older versions of Android since scoped storage is only available on devices running Android 10 or above.
If you deny permission, nothing happens. But if you grant permission, you’ll delete the image permanently and the app will load the updated list.
Where to Go From Here?
Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.
In this tutorial, you learned about scoped storage and worked on the most popular use case where it applies. If you want to learn about other use cases, check out this video from Android Developers Summit 2019.
This is a great time to get your apps working with scoped storage as it will be mandatory for all new apps in August 2020 and every app targeting Android 11.
Many new scoped storage changes will come into effect with Android 11. Check them out here: Storage Updates in Android 11.
Also, take a look at Data Privacy for Android to learn more about user privacy and security.
I hope you liked this tutorial. If you have any questions or comments, please join the forum discussion below.