Choosing the right storage experience

By Pro Web Design

2023-08-10 23:00:00

The upcoming stable release of Android 14 is fast approaching. Now is a great time to test your app with this new release’s changes if you haven’t done so already. With Platform Stability, you can even submit apps targeting SDK 34 to the Google Play Store.

Android 14 introduces a new feature called Selected Photos Access, allowing users to grant apps access to specific images and videos in their library, rather than granting access to all media of a given type. This is a great way for users to feel more comfortable sharing media with apps, and it’s also a great way for developers to build apps that respect user privacy.

To ease the migration for apps that currently use storage permissions, apps will run in a compatibility mode. In this mode, if a user chooses “Select photos and videos” the permission will appear to be granted, but the app will only be able to access the selected photos. The permission will be revoked when your app process is killed or in the background for a certain time (similar to one time permissions). When the permission is once again requested by your app, users can select a different set of pictures or videos if they wish. Instead of letting the system manage this re-selection, it’s recommended for apps to handle this process to have a better user experience.

Even when your app correctly manages media re-selection, we believe that for the vast majority of apps, the permissionless photo picker that we introduced last year will be the best media selection solution for both user experience and privacy. Most apps allow users to choose media to do tasks such as attaching to an email, changing a profile picture, sharing with friends, and the Android photo picker’s familiar UI gives users a consistent, high-quality experience that helps users grant access in confidence, allowing you to focus on the differentiating features of your app. If you absolutely need a more tightly integrated solution, integrating with MediaStore can be considered as an alternative to the photo picker.

Image of My Profile page on a mobile device

To use the photo picker in your app, you only need to register an activity result:

val pickMedia = registerForActivityResult(PickVisualMedia()) uri ->

if (uri != null)
Log.d("PhotoPicker", "Selected URI: $uri")
else
Log.d("PhotoPicker", "No media selected")

The photo picker allows customization of media type selection between photos, videos, or a specific mime type when launched:


pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageAndVideo))

pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))

pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.VideoOnly))

pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.SingleMimeType("image/gif")))

You can set a maximum limit when allowing multiple selections:

val pickMultipleMedia = registerForActivityResult(PickMultipleVisualMedia(5)) uris ->

if (uris.isNotEmpty())
Log.d("PhotoPicker", "Number of items selected: $uris.size")
else
Log.d("PhotoPicker", "No media selected")

Lastly, you can enable the photo picker support on older devices from Android KitKat onwards (API 19+) using Google Play services, by adding this entry to your AndroidManifest.xml file:


<service android:name="com.google.android.gms.metadata.ModuleDependencies"
android:enabled="false"
android:exported="false"
tools:ignore="MissingClass">

<intent-filter>
<action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
intent-filter>

<meta-data android:name="photopicker_activity:0:required" android:value="" />
service>

In less than 20 lines of code you have a well-integrated photo/video picker within your app that doesn’t require any permissions!

Creating your own gallery picker

Creating your own gallery picker requires extensive development and maintenance, and the app needs to request storage permissions to get explicit user consent, which users can deny, or, as of Android 14, limit access to selected media.

First, request the correct storage permissions in the Android manifest depending on the OS version:


<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />

Then, the app needs to request the correct runtime permissions, also depending on the OS version:

val requestPermissions = registerForActivityResult(RequestMultiplePermissions()) results ->

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
requestPermissions.launch(arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_VISUAL_USER_SELECTED))
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
requestPermissions.launch(arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VIDEO))
else
requestPermissions.launch(arrayOf(READ_EXTERNAL_STORAGE))

With the Selected Photos Access feature in Android 14, your app should adopt the new READ_MEDIA_VISUAL_USER_SELECTED permission to control media re-selection, and update your app’s UX to let users grant your app access to a different set of images and videos.

When opening the selection dialog, photos and/or videos will be shown depending on the permissions requested: if you’re requesting the READ_MEDIA_VIDEO permission without the READ_MEDIA_IMAGES permission, only videos would appear in the UI for users to select files.


requestPermissions.launch(arrayOf(READ_MEDIA_VIDEO, READ_MEDIA_VISUAL_USER_SELECTED))

You can check if your app has full, partial or denied access to the device’s photo library and update your UX accordingly. It’s even more important now to request these permissions when the app needs storage access, instead of at startup. Keep in mind that the permission grant can be changed between the onStart and onResume lifecycle callbacks, as the user can change the access in the settings without closing your app.

if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
(
ContextCompat.checkSelfPermission(context, READ_MEDIA_IMAGES) == PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(context, READ_MEDIA_VIDEO) == PERMISSION_GRANTED
)
)

else if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
ContextCompat.checkSelfPermission(context, READ_MEDIA_VISUAL_USER_SELECTED) == PERMISSION_GRANTED
)

else if (ContextCompat.checkSelfPermission(context, READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)

else

Once you verified you have access to the right storage permissions, you can interact with MediaStore to query the device library (whether the granted access is partial or full):

data class Media(
val uri: Uri,
val name: String,
val size: Long,
val mimeType: String,
val dateTaken: Long
)

suspend fun getImages(contentResolver: ContentResolver): List = withContext(Dispatchers.IO)
val projection = arrayOf(
Images.Media._ID,
Images.Media.DISPLAY_NAME,
Images.Media.SIZE,
Images.Media.MIME_TYPE,
)

val collectionUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)

Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
else
Images.Media.EXTERNAL_CONTENT_URI

val images = mutableListOf()

contentResolver.query(
collectionUri,
projection,
null,
null,
"$Images.Media.DATE_ADDED DESC"
)?.use cursor ->
val idColumn = cursor.getColumnIndexOrThrow(Images.Media._ID)
val displayNameColumn = cursor.getColumnIndexOrThrow(Images.Media.DISPLAY_NAME)
val sizeColumn = cursor.getColumnIndexOrThrow(Images.Media.SIZE)
val mimeTypeColumn = cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE)

while (cursor.moveToNext())
val uri = ContentUris.withAppendedId(collectionUri, cursor.getLong(idColumn))
val name = cursor.getString(displayNameColumn)
val size = cursor.getLong(sizeColumn)
val mimeType = cursor.getString(mimeTypeColumn)
val dateTaken = cursor.getLong(4)

val image = Media(uri, name, size, mimeType, dateTaken)
images.add(image)

return@withContext images

The code snippet above is simplified to illustrate how to interact with MediaStore. In a proper production app, you should consider using pagination with something like the Paging library to ensure good performance.

You may not need permissions

As of Android 10 (API 29), you no longer need storage permissions to add files to shared storage. This means that you can add images to the gallery, record videos and save them to shared storage, or download PDF invoices without having to request storage permissions. If your app only adds files to shared storage and does not query images or videos, you should stop requesting storage permissions and set a maxSdkVersion of API 28 in your AndroidManifest.xml:


<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />

ACTION_GET_CONTENT behavior change

In our last storage blog post, we announced that we’ll be rolling out a behavior change whenever ACTION_GET_CONTENT intent is launched with an image and/or video mime type. If you haven’t tested yet this change, you can enable it manually on your device:

adb shell device_config put storage_native_boot take_over_get_content true

That covers how to offer visual media selection in your app with the privacy-preserving changes we’ve made across multiple Android releases.If you have any feedback or suggestions, submit tickets to our issue tracker.

#Choosing #storage #experience

Recent Posts