Scoped Storage Support in Practo’s Android Apps
Scoped Storage was introduced in Android 10 but was not enforced till Android 11 given we use requestLegacyExternalStorage flag in AndroidManifest.xml and target API level 29.
Android 12 makes it mandatory to use Scoped Storage APIs. So this is the high time Android developers move their application to the new storage APIs.
In this blog, I am going to show how we implemented Scoped Storage in our Android apps at Practo. But before starting, it would be nice to discuss few things about scoped storage.
📁 What is Scoped Storage?
- It is a concept of limiting the storage access scope of applications to restrict access to the whole storage of a device.
- It helps in better identification of which files belong to which app. This can help removing the clutter once an app is uninstalled.
- One app’s data can not be used by other apps without proper permissions.
⭐ Key Points
- Unrestricted access to an application’s internal and external storage without any permission.
- Unrestricted access to built-in collections like audios, images and downloads. Reading and writing own files won’t require a permission.
- Reading files which our app don’t own requires
READ_EXTERNAL_STORAGE
permission. - Reading/Writing outside any collection requires system picker.
WRITE_EXTERNAL_STORAGE
is deprecated since API Level 29 and has the same effect asREAD_EXTERNAL_STORAGE
if used.- To access location metadata of a media file,
ACCESS_MEDIA_LOCATION
permission is required in Manifest file.
👨💻 Use Cases
Practo Consult allows users to chat and call the doctors. Chat allows to share multiple kinds of files:
- Images
- Medical test or consultation reports in a PDF file format.
From a developer perspective, we need to be able to browse local files, create empty file to download contents into it and open files. Let us go through each of these requirements one by one.
1️⃣ Browsing Files: We can use MediaStore APIs to browse files from built-in collection like Images and Downloads. Let us understand how it works for Images. This sample illustrates MediaStore usage very well.
Firstly we set the data which we want to retrieve.
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED
)
We can specify the order in which we want images to be displayed. The following will provide us latest images first.
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
Then we get a Cursor
using the above specifications.
val cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
sortOrder
)
Now we get the respective columns which consists of the data we are interested in.
val idColumn =
cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)val dateModifiedColumn =
cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)val displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
Now we can move the cursor one by one to get the image.
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn) val dateModified = Date(TimeUnit.SECONDS.toMillis(cursor.getLong(dateModifiedColumn)))
val displayName = cursor.getString(displayNameColumn) val contentUri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id
)
}
In this way, we get the Uri and other details about the current image. We can add the data in a list until cursor.moveToNext()
returns false and show in a RecyclerView, for example. This is a heavy task and should be done on a background thread. Kotlin Coroutines should make this task very easy.
It works similarly for browsing PDF files and we can use MediaStore.Downloads
to query the same. The uri to pass to query()
method and Content.withAppendedId()
should be:
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
2️⃣ Creating empty file: We first need to create a ContentValues
object.
val values = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
put(MediaStore.Downloads.MIME_TYPE, mimeType)
put(
MediaStore.Downloads.RELATIVE_PATH,
Environment.DIRECTORY_DOWNLOADS + relativePath
)
}
We can now create a file using ContentResolver
.
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
values.clear()
uri?.let { resolver.update(uri, values, null, null) }
Empty file has been created and we can use uri
to write the contents into it using FileOutputStream
and FileInputStream
.
3️⃣ Open Files: To open a file, first we need to check if it exists. If we already have file’s Uri
and know it exists, the task is trivial. Let’s discuss the other case.
To check whether a file exists, let’s make an assumption that we know the filename. Following the first few steps in point 1 above, we get the Cursor
to browse files. Now we just simply need to match the filename.
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val dateModified = Date(TimeUnit.SECONDS.toMillis(cursor.getLong(dateModifiedColumn)))
val displayName = cursor.getString(displayNameColumn)
if (displayName == fileName) {
return ContentUris.withAppendedId(
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
id
)
}
}
📚 References
- Storage samples by Google: Official samples from Google for various use cases.
- Data and file storage overview: Documentation for file storage access.