Mallow's Blog

Scoped Storage in Android

In each version, Android improves many features and upgrades security to make user data safe. Like that, Android restructures the storage system for security purposes from Android Q. In this version, the scoped storage is newly introduced to access the storage system of the device. Before the scoped storage, we need to access the storage through read and write permission. But users don’t have knowledge regarding which data are handled and modified by the developer. This issue is solved by the scoped storage. In order to make changes to the data, the developer needs to get permission from the user to handle the data. This makes it pretty good, so that the user is able to share the data as per their convenience.

Storage System 
The Android has two divisions in the storage system. They are private storage and shared storage.

What is private storage? 
The private storage is the part of the storage given by the Android, it is a specific directory of the app which can be accessed only by the owner of the app. To handle this part, the developer doesn’t have to get permission from the user. i.e. Android/data/your package name. 

What is shared storage?
The rest of the private storage is known as shared storage. The name itself indicates that it can be shared across the app in the device. Any application can get access to those data. 

Before Android Q, the developer needs to get user permission to read and write their data in the shared storage whereas in Android Q with scoped storage, the developer doesn’t have to get permission to write, read, and modify own data of our app. This sounds good. But the Android reconstructs the method of handling the storage also.

Android introduces the media store API to handle the data in the shared storage and there are no changes in handling private storage, this part can be accessed by the common method which we needed before.

Through the Scoped storage concept, Android improves the attributes i.e. the system knows which file is generated by which application and has better management over the files of the particular application.

With these, the system allows users to access their data within an application without any permissions. From Android Q, the location of data cannot be directly accessed by the developer, if they need access, they need the permission of “ACCESS_MEDIA_LOCATION”.

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

Permission required to access the shared data in the devices

In the media store API, to read the data in the device which is contributed by the app,  there is no need to get special permission from the user. 

To handle the data which is not contributed by the app, we need permission from the user to access data. We need to get read 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 those data. The non-media files are handled by the Storage Access Framework API.

These features of getting permission against the data are introduced from Android Q only. On Android Q, we are unable to get permission to do bulk operation, while modifying and deleting data. In this version, in order to do bulk operation, we need to get permission against each item through an alert dialog. But later, in Android R, they have introduced a permission to perform bulk modify and delete the data.

In Android R, they have added some additional features like favourite and trash. These features will not work below the Android version R.

 Favourite – In the name itself, we can understand that users can be allowed to select the data based on their importances and likes. By this feature, we can mark the data as favourite. The data can be favorited by all apps in the device and view in the favourite options.

Trash – It is not like a delete operation. On moving the data to trash, it will be added to the recycle bin. The file will stay there for 30 days and if no further action is taken, the system will automatically delete it after that time.

Accessing the data using Media Store API

Fetch Media Files

val projection = arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAYNAME, MediaStore.Images.Media.DATE_TAKEN)

val selection = "${MediaStore.Images.Media.DATE_TAKEN} >= ?"

val selectionArgs = arrayOf dateToTimestamp(day = 24, month = 7, year = 2019).toString())

val sortOrder = "${MediaStore.Images.Media.DATE_TAKEN} DESC”

getApplication().contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder)?.use
{ cursor -> imageList = addImagesFromCursor(cursor) }

Projection – An array that contains all the information needed. It’s similar to the select clause of the database

Selection – It’s similar to where clause in the database. It contains the condition based on which data should be retrieved.

SelectionArgs – An array containing the values corresponding to the selection placeholder.

SortOrder – The key used to sort the data based on column and order. Default order is ascending order. To switch to descending order, use DESC Keyword.

query() – A method of ContentResolver that takes in all above as parameters as well as an additional Uri parameter that maps to the required table in the provider. In case, the required Uri is EXTERNAL_CONTENT_URI, Uri is always mandatory. Since you are requesting images from outside the app, it cannot be a nullable parameter while the rest of the parameters can be nullable.

To save the data using Scoped storage Ex: save the image

val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, name)
put(MediaStore.Images.Media.MIME_TYPE, “image/jpeg")
put(MediaStore.Images.Media.RELATIVE_PATH, “Pictures/$bucketName/")
put(MediaStore.Images.Media.IS_PENDING, 1)
}
val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val imageUri = context.contentResolver.insert(collection, values)
context.contentResolver.openOutputStream(imageUri).use { out ->
bmp.compress(Bitmap.CompressFormat.JPEG, 90, out)
}
values.clear()
values.put(MediaStore.Images.Media.IS_PENDING, 0)
context.contentResolver.update(imageUri, values, null, null)

From Android Q, the new column field has been introduced in the media store to store the specified path of the file, that is MediaStore.Images.Media.RELATIVE_PATH.

On using the Media store API, we can’t write the image directly to the folder. When you insert an item that is marked as pending intent (value 1), by default it will be hidden from other apps in the device. This can be used when you use long-running Download. Eg: Video Downloading from URL.

Once the download is completed, set the pending intent to 0, to reveal it to other apps in the device 

value.put(MediaStore, Images.Media.IS_PENDING,0) resolver.update(item, null, null, null)

To Delete Media file using media store API

try {
getApplication().contentResolver.delete(
image.contentUri,"${MediaStore.Images.Media._ID} = ?",
arrayOf(image.id.toString()) )
}
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
}
}

In order to call contentResolver.delete(), it should be placed inside a try block because this method can throw a Security Exception at runtime. The method requires the Content Uri of the image you want to delete.  

In the ‘Where’ parameter, you should specify the column  based on which the file should be deleted. In the final parameter, you pass the value as an array to the placeholder in the ‘where’ clauses.

From Android Q, it isn’t possible to delete or modify items from the media store directly. The developer needs to get permission for these actions. The correct approach is to catch Recoverable Security Exception, which contains an intentSender that can prompt a user to grant permission and if the user accepts the permission then the file can be deleted.

Scoped storage Feature introduced After Android R:

createWriteRequest – getting bulk access to modify the data

createDeleteRequest – getting bulk access to delete the data

Bulk Delete Data 

fun deleteMediaBulk(context: Context, media: List): IntentSender {
val uris = media.map { it.uri }
return MediaStore.createDeleteRequest(context.contentResolver, uris).intentSender
}

This bulk operation is introduced to overcome the limitation of showing a dialogue for each data. Instead of calling the contentResolver.delete(), the developer can use Mediastore.createDeleteRequest which allows the user to grant access to delete all selected files with a single request.

Thus, the scoped storage introduced by Android assures security of data in the shared space. 

Gowtham G M,
Android Team,
Mallow Technologies.

Leave a Reply

%d bloggers like this: