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?
Handling the Permission Request
You need to properly handle the permission request. Here’s how you do it.
In DetailsFragment.kt, update the TODO in requestScopedPermission
:
startIntentSenderForResult(intentSender, REQUEST_PERMISSION_UPDATE,
null, 0, 0, 0, null)
This triggers the process of asking for specific file permissions, you must call startIntentSenderForResult
which will launch a new activity for the user to grant access.
Given that the user may deny this request, you need to analyze the intent return state. You’ll need to send REQUEST_PERMISSION_UPDATE
along with startIntentSenderForResult
. For that, add the following constant to DetailsActivity
before the class declaration:
private const val REQUEST_PERMISSION_UPDATE = 200
After this, add this second condition to the when
statement in onActivityResult
:
when (requestCode) {
...
REQUEST_PERMISSION_UPDATE -> {
if (resultCode == Activity.RESULT_OK) {
updateMeme()
} else {
Toast.makeText(requireContext(),
R.string.toast_image_fail, Toast.LENGTH_SHORT).show()
}
}
}
This will update the image when the user grants you permission.
Now compile and run the app and create another new meme.
Deleting an Image
Now, what if the user wants to delete a meme? You can give them that option, too.
The delete operation is similar to the storage process. If you try to delete a file that you don’t own, the system will throw a SecurityException
. You’ll need to request permission from the user to do this.
In FileOperations replace the body ofdeleteImage
with this:
var result: IntentSender? = null
withContext(Dispatchers.IO) {
try {
context.contentResolver.delete(image.uri, "${MediaStore.Images.Media._ID} = ?",
arrayOf(image.id.toString()))
} catch (securityException: SecurityException) {
if (Utils.hasSdkHigherThan(Build.VERSION_CODES.P)) {
val recoverableSecurityException =
securityException as? RecoverableSecurityException ?: throw securityException
result = recoverableSecurityException.userAction.actionIntent.intentSender
} else {
throw securityException
}
}
}
return result
Now deleteImage
will try to delete the image and request permission if you try to delete a file that the app doesn’t own. As you can see, this logic is similar to updateImage
. The difference is that you’ll call the delete method from contentResolver
to remove the image and all its references from the MediaStore.Images table.
You’ll also need to update the contents of deleteImage in DetailsViewModel within viewModelScope.launch
:
val intentSender = FileOperations.deleteImage(getApplication(), image)
if (intentSender == null) {
_actions.postValue(ImageDetailAction.ImageDeleted)
} else {
_actions.postValue(
ImageDetailAction.ScopedPermissionRequired(
intentSender,
ModificationType.DELETE
)
)
}
This will take care of any missing permissions just as you did before with updateImage
:
Then in DetailsFragment.kt modify requestScopedPermission
by adding ModificationType.DELETE -> REQUEST_PERMISSION_DELETE
as a condition in the when
statement. This condition will now account for this new request type.
Add the REQUEST_PERMISSION_DELETE
constant before the DetailFragement class declaration:
private const val REQUEST_PERMISSION_DELETE = 100
Once again, since the user can deny permission, it’s important to send a request code.
Add a third condition to the when
statement in onActivityResult
below the one you added for REQUEST_PERMISSION_UPDATE:
...
REQUEST_PERMISSION_DELETE -> {
if (resultCode == Activity.RESULT_OK) {
viewModel.deleteImage(image)
} else {
Toast.makeText(requireContext(),
R.string.toast_deleted_fail, Toast.LENGTH_SHORT).show()
}
}
...
Call viewModel.deleteImage
if the user gives you permission to delete. Otherwise, display a toast warning to inform the user that it’s not possible to remove an image without granting access.
That’s it! You updated the app for scoped storage. Build and run and see what you’ve accomplished.
Where to Go From Here?
Congratulations! You completed this tutorial. You prepared your app for scoped storage and you’re ready for Android 11.
Want to know more about protecting users’ privacy? Take a look at this tutorial: Data Privacy for Android. You can also dive into the world of advanced data persistence with this tutorial: Room DB Advanced Data Persistence.
Or if you’re not sure what you want to study or develop next, take a look at this list of all the Android articles on the Ray Wenderlich website: List of Android Articles. Pick one and start from there. :]
You probably made some really cool memes as you worked on Le Memeify. So, why not share some with everyone? Feel free to tag us @rwenderlich with your best creations.
If you have any questions from this tutorial, please post them in the discussion below.