Preparing for Scoped Storage
Android apps targeting Android 11 will be required to use scoped storage to read and write files. In this tutorial, you’ll learn how to migrate your application and also why scoped storage is such a big improvement for the end user. By Carlos Mota.
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
Preparing for Scoped Storage
30 mins
- Le Memeify 👌
- Getting Started
- Understanding the Project Structure
- Running Le Memeify
- Grokking Scoped Storage
- Running on Android 10
- Targeting Android 11
- Updating to Support Scoped Storage
- Loading the Images
- Creating a New File
- Changing File IO to URIs
- Creating a New File in a Specific Location
- Updating an Existing Image
- Handling the Permission Request
- Deleting an Image
- Where to Go From Here?
Changing File IO to URIs
Now that you updated saveImage in DetailsFragment, it’s time to change the remaining logic to use URIs. First, go to DetailsViewModel.kt and update the body ofsaveImage to:
viewModelScope.launch {
//1
val type = getApplication<Application>().contentResolver.getType(image.uri)
val format = Utils.getImageFormat(type!!)
//2
FileOperations.saveImage(getApplication(), bitmap, format)
//3
_actions.postValue(ImageDetailAction.ImageSaved)
}
Let’s break down this logic into small steps:
- Determine the format of the image. First, retrieve the type from Uri and then the corresponding
Bitmap.CompressFormatused to compress the bitmap. - Call
saveImage, which you just updated, from FileOperations. - Construct
ImageDetailAction.ImageSavedand post it so the LiveData will automatically update the UI
Now go to the Utils class and update the contents ofgetImageFormat to:
return when (type) {
Bitmap.CompressFormat.PNG.name -> {
Bitmap.CompressFormat.PNG
}
Bitmap.CompressFormat.JPEG.name -> {
Bitmap.CompressFormat.JPEG
}
Bitmap.CompressFormat.WEBP.name -> {
Bitmap.CompressFormat.WEBP
}
else -> {
Bitmap.CompressFormat.JPEG
}
}
Now, getImageFormat no longer creates a file to get the image compress format from its extension, it uses type instead.
Finally, update saveMeme in DetailsFragment.kt by changing the line viewModel.saveImage(path, createBitmap()) to viewModel.saveImage(image, uri, createBitmap()).
After this update to saveMeme image and uri are passed to saveImage so it can save the image.
Compile and run the app. Try it out!

Creating a New File in a Specific Location
By default, scoped storage restricts storage of images to the DCIM/ and Pictures/ directories. However, the user can select a specific location to save a file.
To enable this, start by adding a new entry point that will trigger this action.
In menu_details.xml, add the following item:
<item
android:id="@+id/action_save_location"
android:title="@string/action_save_location"
app:showAsAction="never"/>
This will add an overflow menu option in DetailsFragment.kt with the text “Save as…”.
Next, go to DetailsFragment.kt and add the following to the when block in onOptionsItemSelected:
R.id.action_save_location -> {
hideKeyboard(null)
saveMemeAs()
true
}
The system will call this when the user selects the Save as option.
Next, declare saveMemeAs in DetailsFragment like this:
private fun saveMemeAs() {
//1
val format = Utils.getImageFormat(
requireActivity().contentResolver.getType(image.uri)!!)
//2
val extension = Utils.getImageExtension(format)
//3
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_TITLE, "${System.currentTimeMillis()}.$extension")
type = "image/*"
}
//4
startActivityForResult(intent, REQUEST_SAVE_AS)
}
The code above:
- Get the format of the image.
- Get the extension of the image.
- Call the native file explorer with the intent action,
ACTION_CREATE_DOCUMENTand set the extraIntent.EXTRA_TITLEwith the file name. When the native file explorer opens, the user will see the defined file name and will have the option to change it. - Call
startActivityForResultwith the intent andREQUEST_SAVE_ASto find out if the user selected a specific folder or canceled the operation.
Then add the following constant before the class declaration:
private const val REQUEST_SAVE_AS = 400
The requestCode enables the app to identify from which Intent it came back from.
After this, add this use case to onActivityResult right below super.onActivityResult(requestCode, resultCode, data) to save the meme in this specific directory:
when (requestCode) {
REQUEST_SAVE_AS -> {
if (resultCode == Activity.RESULT_OK) {
saveMeme(data?.data)
}
}
Now if the user selected a folder, the meme will be saved.
Add a new method to FileOperations to save an image directly with a defined URI:
suspend fun saveImage(context: Context, uri: Uri, bitmap: Bitmap,
format: Bitmap.CompressFormat) {
withContext(Dispatchers.IO) {
context.contentResolver.openOutputStream(uri, "w").use {
bitmap.compress(format, QUALITY, it)
}
}
}
This will write the compressed image to the file.
Now update saveImage in DetailsViewModel inside viewModelScope.launch:
val type = getApplication<Application>().contentResolver.getType(image.uri)
val format = Utils.getImageFormat(type!!)
if (uri == null) {
FileOperations.saveImage(getApplication(), bitmap, format)
} else {
FileOperations.saveImage(getApplication(), uri, bitmap, format)
}
_actions.postValue(ImageDetailAction.ImageSaved)
This will verify which saveImage method to call from FileOperations.
Time to build the app and create another meme!

SecurityException. You’ll need to ask for permission again.
Updating an Existing Image
What if the user wants to edit a file created by another application? To do this, you’ll need to ask for write permissions.
Add the following method to FileOperations:
suspend fun updateImage(context: Context, uri: Uri, bitmap: Bitmap,
format: Bitmap.CompressFormat): IntentSender? {
var result: IntentSender? = null
withContext(Dispatchers.IO) {
try {
//1
saveImage(context, uri, bitmap, format)
} catch (securityException: SecurityException) {
//2
if (Utils.hasSdkHigherThan(Build.VERSION_CODES.P)) {
val recoverableSecurityException =
securityException as?
RecoverableSecurityException ?: throw securityException
result = recoverableSecurityException.userAction.actionIntent.intentSender
} else {
//3
throw securityException
}
}
}
return result
}
Here’s what you’re doing in the code above:
- Try writing the edited bitmap to the URI you already defined since it’s an existing image.
- Ask for the user’s permission to edit the file. Since your app didn’t originally create the file, you’ll get a
SecurityExceptionand you’ll need to ask the user for permission to edit the file. This happens if your device has Android 10 (with scoped storage enabled) or later. - If the app is running on an older Android version without scoped storage throw a
SecurityException. This can occur if the file is set to read-only.
Next, replace updateImage in DetailsViewModel with the following inside viewModelScope.launch:
//1
val type = getApplication<Application>().contentResolver.getType(image.uri)
val format = Utils.getImageFormat(type!!)
//2
val intentSender = FileOperations.updateImage(
getApplication(), image.uri, bitmap, format)
//3
if (intentSender == null) {
_actions.postValue(ImageDetailAction.ImageUpdated)
} else {
_actions.postValue(
ImageDetailAction.ScopedPermissionRequired(
intentSender,
ModificationType.UPDATE
)
)
}
In the code above, you:
- Get the file format that you’ll use to update the existing image.
- Call the previously added method
updateImage. - Send an
ImageUpdatedaction to the view to process if the previous call didn’t trigger aSecurityExceptionand the returned result was null. If so, you know that you successfully updated the image. - Construct a
ScopedPermissionRequiredaction withintentSenderso the view can request permissions. IfintentSenderis not null, you know that you need to request permission manually in order to update the file.