Android 10’s Scoped Storage: Image Picker (Gallery/Camera)
To give users more control over their files and to limit file clutter, apps targeting Android 10 (API level 29) and higher are given scoped access into an external storage device, or scoped storage, by default. Such apps can see only their app-specific directory accessed using Context.getExternalFilesDir()
and specific types of media. It's a best practice to use scoped storage unless your app needs access to a file that doesn't reside in either the app-specific directory or the MediaStore
.
Why Scoped Storage
First and foremost, it stops malicious apps that depend on the user for granting access to their sensitive data because they did not read what they saw in the dialog and just clicked on allow. It also allows a developer to have their own space on the storage of your device that is private without asking for any specific permissions and no other app can access any document it creates without user granting temporary permissions.
Another reason to do this is because when an application having storage permissions, creates files or folders they get scattered all over user’s storage directory making it cluttered, and if the user decides to uninstall the app the files / folders that were created by the app still exist in that same location, unless you manually delete those files / folders once you’re done using them.
Can I opt out of Scoped Storage?
Yes, you can opt out temporarily by using following methods:
- Target Android 9 (API level 28) or lower.
- If you target Android 10 or higher, set the value of
requestLegacyExternalStorage
totrue
in your app's manifest file
As per Google Play requirements, to provide users with the best Android experience possible, the Google Play Console will continue to require that apps target a recent API level. Apps will be required to use scoped storage in next year’s major platform release for all apps, independent of target SDK level. Therefore, you should ensure that your app works with scoped storage well in advance.
So, what’s the solution?
Talking about solution, it’s not exactly new, but it is now strictly enforced and no longer optional. With Android 4.4 (Api 19) introduces the Storage Access Framework (SAF). This gave apps a way to access files in other folders using an Android API instead of using standard programming file operations.
Following types of files within an external storage device without needing to request any storage-related user permissions:
- Files in the app-specific directory, accessed using
getExternalFilesDir()
. - Photos, videos, and audio clips that the app created from the media store.
I’m going to explain scoped storage changes using example of Image picker(Gallery/Camera) with compression. You can find source code here.
Creating file directory URI using FileProvider
To capture image we have to create external file directory using getExternalFilesDir(
Environment.DIRECTORY_DCIM) and create uri from it.
There are other Environment variables also available like
Environment.DIRECTORY_MUSIC
,Environment.DIRECTORY_PODCASTS
,Environment.DIRECTORY_RINGTONES
,Environment.DIRECTORY_ALARMS
,Environment.DIRECTORY_NOTIFICATIONS
,Environment.DIRECTORY_PICTURES
, orEnvironment.DIRECTORY_MOVIES
. This type value can benull
for the root of the files directory. Use as per your requirements.
But passing file://
URIs outside the package domain may leave the receiver with an inaccessible path. Therefore, attempts to pass a file://
URI trigger a FileUriExposedException
. The recommended way to share the content of a private file is using the FileProvider
.
Following steps for create FileProvider:
- Android Manifest
2. Specifying available files
3. Generating content URI for a file
Take a photo or Select Image from gallery
Here we’re using Intent.createChooser()
to take a photo using camera and select image from gallery.
Read Bitmap from URI
In onActivityResult() intentData
parameter contains URI if user selects image from gallery, otherwise data will be available in imageUri
which we were created earlier.
Note: You should not do this operation on UI thread. Do it in the background thread. For this I used Coroutines, its simple and easy to use.
To start image processing, we need to get bitmap from uri/path. Earlier we were using BitmapFactory.decodeFile(path), but it return null for external files. So, we have to use following method to get bitmap by using SAF method.
Use URI outside Activity Scope
Once the user select image from gallery, your app can return its content URI in onActivityResult(). You have access to the content of URI from the Activity that receives the URI. You can pass this access to other components of your app or even components of other apps by setting Intent flag as follows:
- Call the method
Context.grantUriPermission(package, Uri, mode_flags)
for thecontent://
Uri
, using the desired mode flags. This grants temporary access permission for the content URI to the specified package, according to the value of themode_flags
parameter, which you can set toIntent.FLAG_GRANT_READ_URI_PERMISSION
,Intent.FLAG_GRANT_WRITE_URI_PERMISSION
or both. The permission remains in effect until you revoke it by callingrevokeUriPermission()
or until the device reboots. - Put the content URI in an
Intent
by callingsetData()
. - Next, call the method
Intent.setFlags()
with eitherIntent.FLAG_GRANT_READ_URI_PERMISSION
orIntent.FLAG_GRANT_WRITE_URI_PERMISSION
or both. - Finally, send the
Intent
to another app. Most often, you do this by callingsetResult()
. - Permissions granted in an
Intent
remain in effect while the stack of the receivingActivity
is active. When the stack finishes, the permissions are automatically removed. Permissions granted to oneActivity
in a client app are automatically extended to other components of that app.
val intent = Intent(this, AnotherActivity::class.java)
.setData(uri)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
startActivity(intent)
Persist URI Permission
URI permission lasts until the user’s device restarts. To prevent this you can use contentResolver.takePersistableUriPermission()
as follows:
val takeFlags: Int = intent.flags and
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
contentResolver.takePersistableUriPermission(uri, takeFlags)
Manage Package visibility on Android 11
Add the following code in the manifest file:
<manifest >
...
<queries>
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE" />
</intent> <intent>
<action android:name="android.intent.action.PICK" />
<data android:mimeType="image/*" />
</intent></queries>
...
</manifest>
Following source code for a complete example of image picker with compression:
References:
https://developer.android.com/training/data-storage/files/external-scopedhttps://developer.android.com/guide/topics/providers/document-providerhttps://www.androidcentral.com/what-scoped-storage-android-q
If there are any questions or feedback regarding this post then please do reach out.
Happy Coding!