Working with Scoped Storage

On September 3rd, Android 10 was officially released and available for Pixel devices, and with it comes a controversial change: Scoped Storage.
In a nutshell, Scoped Storage prevents apps from having unrestricted access to the filesystem on the device. Previously, an app could access any file on the device using the standard File APIs, and it only needed the user to grant the storage permission.
Is Scoped Storage required for all apps on Android 10?
No, it only applies when your targetSdkVersion is set to 29, so legacy apps continue to work. Also, even if you are targeting API 29 you can still use the legacy storage by setting android:requestLegacyExternalStorage=”true”
on the application tag inside of AndroidManifest.xml.
However, you should implement Scoped Storage in your app as soon as possible becuase on Android 11 Google says it will be required for all apps regardless of targetSdkVersion.
How to access simple files?
In order to access files on the device, you can use the Storage Access Framework (SAF). By using ACTION_OPEN_DOCUMENT
a dialog will be shown to the user where the needed documents can be selected. There’s also ACTION_OPEN_DOCUMENT_TREE
to ask the user to select a directory and ACTION_CREATE_DOCUMENT
in order to save files.
Implementing Scoped Storage on a photo application
Now we’ll be using Date To Photo, which is an app I’m building and is currently in beta, although very close to release, as an example, since recently I updated it to be compatible with Scoped Storage.
Date To Photo is an app which allows the user to add a watermark to their photos with the date when the photo was taken. When the user opens the app, a grid with the photos that hasn’t been yet watermarked appears, the user can select one/multiple photos or add the watermark to all photos. The app has a setting for overwriting the original photo or storing it as a new one, and the user can choose which folders they want to process (for example, they can choose to only display the Camera images). In addition to that, the app supports this process to be run when the device is charging, thus requiring no intervention from the user.
Displaying the photo gallery
The first step, displaying the photos, is very simple. The only thing we have to keep in mind is to not use Files to reference the photos (the MediaStore.Images.Media.DATA
column is now deprecated), and instead we have to use Uris (which was also a recommended practice before).
We can get the id of the photo by using MediaStore.Images.Media._ID
and build the Uri using ContentUris.withAppendedId
For the app we also need to know the folder (also known as bucket) where the photo was located (Screenshots, Camera, …). We can request this by using MediaStore.Images.Media.BUCKET_DISPLAY_NAME
(we can also use MediaStore.Images.Media.BUCKET_ID
in order to get a unique identifier, which can be used for grouping). If we need the original name of the file we can use MediaStore.Images.Media.DISPLAY_NAME
.
Getting the photo to process it
In order to get the photo as a Bitmap to process it, you can get an InputStream from the Uri and pass it to BitmapFactory.decodeResource or use the new ImageDecoder
API on API 28+
Saving a photo to the gallery
Due to Scoped Storage, we can’t write the image directly to the desired folder and then update the MediaStore
. Instead, Android Q introduces a new field MediaStore.Images.Media.RELATIVE_PATH
in which we can specify the path of the image (for example, "Pictures/Screenshots/"
).
As well as 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.
One thing you might have noticed above is that we’re not using MediaStore.Images.Media.EXTERNAL_CONTENT_URI
to save the image. Instead, Android Q allows you to easily save media files to any external storage device attached (like an external sdcard). Here we’re saving the image to the main storage of the device by specifying MediaStore.VOLUME_EXTERNAL_PRIMARY
, but we can get a list of all the storage devices using MediaStore.getExternalVolumeNames
.
After that, we can use the returned image Uri to save the image. For that, we can use context.contentResolver.openOutputStream
and save it to that stream, or we can also open a FileDescriptor
on write mode using context.contentResolver.openFileDescriptor
. Finally, once the image is saved we set the MediaStore.Images.Media.IS_PENDING
attribute to 0 to indicate we’re done inserting the image.
Overwriting the original photo
In order to overwrite a photo you have to write to the Uri or get a file descriptor, just like when we saved the image.
One important thing to keep in mind though is that if you try to call update
, delete
or open a file descriptor on write mode on an image added by another application, the system will throw a RecoverableSecurityException
. You can catch this exception and show a dialog for the user to grant access.
This approach is fine if we only need to operate over one item (like deleting a single photo), but it’s not suitable if we need to operate over multiple items at once or process photos on the background.
What we can do for this situations is to request access to the desired folder to the user using the Storage Access Framework using ACTION_OPEN_DOCUMENT_TREE
, specifying the flags FLAG_GRANT_WRITE_URI_PERMISSION
and FLAG_GRANT_PERSISTABLE_URI_PERMISSION
. We can do this the first time the user opens the app or the first time the user selects that folder to be processed.
After the user has granted access, we have to take the persistable permission
Then, when we want to overwrite the photo an important thing to note is that we can’t write to the MediaStore Uri. Instead, we have to first convert that Uri into a SAF Uri in order to be able to write to it.
Conclusion
Scoped Storage introduces important changes to the way Android apps work with files. The important thing to keep in mind is that we can’t use the File APIs to directly access files anymore. Instead, we can rely on the Storage Access Framework for choosing files or folders and the MediaStore for media files.
I hope the approachs shown in this post have been helpful to you, and remember that although Scoped Storage isn’t mandatory until next year, it’s a good idea to have your app ready as soon as possible.