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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

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.

Update image

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.

Delete image

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.