SCOPED STORAGE

Shray Bhatia
hubbleconnected
Published in
8 min readJun 28, 2021

The newly introduced Storage System by Android known as the Scoped Storage is here. Let’s see what it brings and what has changed.

Up till Android 10, you could just ask the user for storage permission once and if granted, you can Create, Update or Read any file on the device. As we know, most apps don’t need access to all the files stored in the Android device. Thus, in order to counter the threat to user security and device storage, Google introduced a new concept called Scoped Storage starting from Android 10.

However, in Android 10 it is optional, and you can just opt out of it by setting requestLegacyExternalStorage to true in your in AndroidManifest.xml.
This attribute is by default set to False in devices supporting Android 10 and above, in order to make the application compatible with Scoped Storage.

NOTE: It is advised not to opt out of Scoped Storage and make your app compatible with it, as Scoped Storage is the way forward and from Android 11 this flag will have no effect.

Type of Permissions required with Scoped Storage

If you’re using Scoped Storage in Android 10 and above, the WRITE_EXTERNAL_STORAGE permission will be treated as READ_EXTERNAL_STORAGE permission. So, it is advised not to use it.

To read the data in the device, which is contributed by the app, there is no need to get any special permission from the user.

You need to get READ_STORAGE_PERMISSION from the user to access the media file that was not contributed by our app. To modify and delete the data contributed by another app, we need permission from the user against that data. The non-media files are handled by the Storage Access Framework API.

Getting permission against the data is introduced from Android Q. However, in Android Q, we are unable to get permission to do bulk operation (modifying and deleting multiple files). So, in order to do bulk operation, we need to get permission against each item through an alert dialog. But later, in Android R, permission for bulk operations have been added along with additional features like “Favourite” and “Trash”.

Favorite — As the name suggests, users can be allowed to select the data based on their preference. Using this, we can mark the data as favorite. The files marked favorite can be viewed under the favorite folder.

Trash — Trash is different from DELETE. On moving the files to trash, it will be added to the trash folder. The file will stay there for 30 days and if no further action is taken, the system will automatically delete it after that time.

Let’s get started with an example

  1. Saving an Image into Media Store
suspend fun saveImageFileInDevice(

filename: String,

bitmap: Bitmap,

format: Bitmap.CompressFormat

) {

withContext(Dispatchers.IO) {

val
outputStream: OutputStream?



val date = System.currentTimeMillis()

val extension = "jpg"

val
folder = fileUtils.getUserFolder() //the User folder to store the image





if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

try {

val collection = MediaStore.Images.Media.getContentUri(

MediaStore.VOLUME_EXTERNAL_PRIMARY

)



var folderDir = ""

val
appDir = File(Environment.DIRECTORY_PICTURES, appFolderName)

folderDir = "$appDir${File.separator}$folder"



val
newImageValues = ContentValues().apply {

put(MediaStore.MediaColumns.DISPLAY_NAME, filename)

put(MediaStore.MediaColumns.MIME_TYPE, "image/$extension")

put(MediaStore.MediaColumns.DATE_MODIFIED, date)

put(MediaStore.MediaColumns.DATE_ADDED, date)

put(MediaStore.MediaColumns.SIZE, bitmap.byteCount)

put(MediaStore.MediaColumns.WIDTH, bitmap.width)

put(MediaStore.MediaColumns.HEIGHT, bitmap.height)

put(MediaStore.MediaColumns.RELATIVE_PATH, folderDir)

put(MediaStore.MediaColumns.IS_PENDING, 1)

}

Android Q introduces a new field MediaStore.Images.Media.RELATIVE_PATH in which we can specify the path of the image (for example, “Pictures/DemoApp/” ).

Apart from setting the image attributes and its relative path as shown above, we also have to set a new field MediaStore.Images.Media.IS_PENDING (which indicates that the item is still being saved) and insert it into the MediaStore.
IS_PENDING value 1 indicates that the image is still being saved and IS_PENDING value 0 indicates that the image is stored and can now be shared with external applications.

Now we will insert the image into the media store with the corresponding content values

val imageUri = application.contentResolver.insert(

collection,

newImageValues

)



imageUri?.let {

outputStream = application.contentResolver.openOutputStream(imageUri)

bitmap.compress(format, 100, outputStream)

}
newImageValues.clear()newImageValues.put(MediaStore.Images.Media.IS_PENDING,0)application.contentResolver.update(imageUri, newImageValues, null,null)

} catch (e: Exception) {

log.d("Exception occured, error message = %s", e.printStackTrace())

}

}
}
}

2. Delete an Image

In order to DELETE an existing image from Media Store, we need to wrap the code inside a try block and catch RecoverableSecurityException.
This exception is thrown everytime you try to Update, Delete, OpenFileDescriptor or write on an image which is not created by your application. One important thing to remember is whenever you uninstall and reinstall your application, the Android OS treats it as a separate application and denies the access to the files which were created in the last install. Thus, we need to ask the user explicitly for permission to MODIFY or DELETE those files.
We do so by making use of RecoverableSecurityException and asking the user for permission to MODIFY or DELETE those files.
In Android 10, bulk operations are not possible and we will need to handle each request separately, whereas starting from Android 11, bulk operations are available.

suspend fun deleteImageFile(mediaListToBeDeleted: List<Media>) {

val resultList: MutableList<IntentSender?> = ArrayList()
withContext(Dispatchers.IO) {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
val uriList : MutableList<Uri> = ArrayList()
for(media : Media in mediaListToBeDeleted){
uriList.add(media.uri)
}
var result: IntentSender? = null


val
pi = MediaStore.createDeleteRequest(application.contentResolver, uriList);

result = pi.getIntentSender()
resultList.add(result)

__permissionNeededForDelete.postValue(resultList)
} else { //passing as a list to support multi delete
for (mediaItem: Media in mediaListToBeDeleted) {
var result: IntentSender? = null
try
{
mediaItem?.let {
application.contentResolver.delete(
mediaItem.uri, "${MediaStore.Images.Media._ID} = ?",
arrayOf(mediaItem.id.toString()))

}
} catch (securityException: SecurityException) { //if the app doesn't have permission
if (Build.SDK_INT >= Build.VERSION_CODES.Q) {
val recoverableSecurityException =
securityException as? RecoverableSecurityException
?: throw securityException
result =
recoverableSecurityException.userAction.actionIntent.intentSender

} else {
throw securityException
}

}
resultList.add(result)

}

Here, if the Application doesn’t have the permission to delete the image file, then security exception will be thrown.
You can make use of the LiveData object to get the updated value of IntentSender in your activity or fragment and execute the next steps accordingly. (Only for Android 10)

try {



_permissionNeededForDelete.postValue(result)

} catch (e: java.lang.Exception) {

e.printStackTrace()

}
}

In the Activity/Fragment, you can make use of the LiveData object and check if the image was successfully deleted or not. If the application had permission to delete the image and no exception occurred, then the value updated by the LiveData would be null, else it will contain the intent sender, which will be needed to ask the permission from the user in order to delete the image.
Use the following code as reference in your Activity/Fragment.

if (deleteImageIntentSender != null) {

try {

startIntentSenderForResult(

deleteImageIntentSender,

IMAGE_DELETE_PERMISSION_REQUEST,

null,

0,

0,

0,

null

)

} catch (e: java.lang.Exception) {

//Exception occurred

e.printStackTrace

}

} else {

//Image deleted successfully

}

In the onActivityResult(), check if the user provided the permission to delete the image or not. If yes, then continue with image deletion. If not, then ask the user to grant the permission.

NOTE: In Android 10, once the permission is granted you need to manually delete the image. Whereas in Android 11, the system automatically handles that and deletes the image as soon as the permission is granted.

@Override

public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {

super.onActivityResult(requestCode, resultCode, data);
if (requestCode == IMAGE_DELETE_PERMISSION_REQUEST) {

if (resultCode == Activity.RESULT_OK) {

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { //for android 11 and above

//Image deleted successfully

} else {

//Permission granted, call the method to delete image

}

} else {

//Ask user to grant permission to delete

}

}
}

3. Update an Image in Media Store

We can also update an existing image with new values inside the Media Store. The process is similar to DELETE. Here, we would be passing the URI that needs to be updated , and would be calling the update query method to update the existing URI in the Media Store.
As pointed earlier, catching the RecoverableSecurityException is a must, as we would need to ask for permission from the user in order to update a file which was not created by our application.

suspend fun updateImage(uriToBeUpdated: Uri) {

withContext(Dispatchers.IO) {



val updated
Date = System.currentTimeMillis()

var result: IntentSender? = null

if
(Build.Version.SDK_INT >= Build.VERSION_CODES.Q){



try {



try {



val newImageValues = ContentValues().apply {

put(MediaStore.MediaColumns.DATE_MODIFIED, updatedDate)

put(MediaStore.MediaColumns.DATE_ADDED, updatedDate)

put(MediaStore.Image.Media.IS_PENDING, 0)

}

application
.contentResolver

.update(uriToBeUpdated, newImageValues, null, null) //updating the value

}

}catch (securityException: SecurityException){

if (Build.Version.SDK_INT >= Build.VERSION_CODES.Q){

val recoverableSecurityException =

securityException as? RecoverableSecurityException

?: throw securityException

result = recoverableSecurityException.userAction.actionIntent.intentSender

} else {

throw securityException

}



}

} catch (e: java.lang.Exception) {

log.d("Exception occured, error message %s", e.message)

}

If the app has permission to update the image file, then the value of result will be null, else it will contain the RecoverableSecurityException. You can get its value using the LiveData and perform the next steps in your activity/fragment.

try{_permissionNeededToUpdateImage.postValue(result)}catch (e: java.lang.Exception) {

log.d("Exception occured, error message %s", e.message)

}

Now in your activity/fragment you can observe the LiveData and handle the result.

if (intentSender!= null) {
try {

startIntentSenderForResult(intentSender, REQUEST_UPDATE_IMAGE_PERMISSION, null, 0, 0, 0, null);
}catch (Exception e){
e.printStackTrace();
}
}
else {
Toast.makeText(this, “Image updated successfully”, LENGTH_SHORT).show()}

In the onActivityResult(), check for the request code, In Android 11 and above, it will automatically update the file but in Android 10 and lower, you will need to again call the method to update and only then the file will be updated.

Additional Content

You can also update the URI of a video/audio file. Here is a snippet to help you with that.

suspend fun updateAudio(file : File , uriToBeUpdated: Uri) {

withContext(Dispatchers.IO) {
try {

val pfd: ParcelFileDescriptor
uriToBeUpdated?.let {

pfd = application.contentResolver.openFileDescriptor(uriToBeUpdated, "rwt")!!

val out = FileOutputStream(pfd.fileDescriptor)

val fis = FileInputStream(file) //getting the temp file here

val buf = ByteArray(8192)

var len: Int

while (fis.read(buf).also { len = it } > 0) {

out.write(buf, 0, len);

}

out.close()

fis.close()

pfd.close()
val newAudioValues = ContentValues().apply {
put(MediaStore.Audio.Media.IS_PENDING, 0)
}
application
.contentResolver
.update(uriToBeUpdated, newAudioValues, null, null) //updating the value
}catch ( e : java.lang.exception{e.printstacktrace()}}}

Here just pass the URI that needs to be updated in the uriToBeUpdated field, and the file with which it needs to be updated.

We use the ParcelFileDescriptor to update the MediaStore Audio file with the new URI.

Type of Media Store collections

The Media store API saves the data in the form of a database. For each collection, there is a separate table to store the data. The media store API is commonly classified into three collections. They are image, video and audio. From Android 10, they have added one more collection, which is called Download Collections.

For images — MediaStore.Images table

For Videos — MediaStore.Video table

For Audio — MediaStore.Audio table

For downloads — MediaStore.Downloads table

Advantages of using Scoped Storage

Enhancing Security: Scoped storage provide developers the advantage to not request permissions for the files they have created. Also, it provides their files from other applications.

Reducing leftover app data: Whenever a user uninstalls the app, it was seen that some data was still there. With scoped storage the data added always resides in the app directory.

Limiting the abuse of READ_EXTERNAL_STORAGE permission: This used to give the developers access to all the files. But with Scoped Storage, developers can only have access to the files stored in the app directory.

For apps that require access to all the folders like a FileManager app, you’d need to fill a form on the PlayConsole and file a request. If approved, your app will be granted the access to all the folders.
If your application requires such access, you can declare it on Google Play.

Conclusion

Scoped Storage brings a major change in the way android application works with Files. There are a few limitations to it, but it majorly increases the security and the memory management in android devices. Scoped Storage is only mandatory starting from Android 11. But even if you target lower android versions, it is a good idea to make your application compatible with it.

Hope the concept explained in this post was helpful to you.

Thankyou

References

https://developer.android.com/about/versions/11/privacy/storage

Shray Bhatia
Software Engineer (Android)
Hubble Connected

--

--