A cloud media provider provides additional cloud media content to the Android
photo picker. Users are able to select photos or videos supplied by the
cloud media provider when an app uses ACTION_PICK_IMAGES
or
ACTION_GET_CONTENT
to request media files from the user. A cloud media
provider can also give information about albums, which can be browsed in the
Android photo picker.
Before you begin
Take the following items into consideration before you begin building your cloud media provider.
Eligibility
Android is running a pilot program to allow OEM-nominated apps to become cloud media providers. Only apps nominated by OEMs are eligible to participate in this program to become a cloud media provider for Android at this time. Each OEM can nominate up to 3 apps. Once approved, these apps become accessible as cloud media providers on any GMS Android-powered device on which they are installed.
Android maintains a server-side list of all eligible cloud providers. Each OEM can choose a default cloud provider using a configurable overlay. Nominated apps must meet all technical requirements and pass all quality tests. To learn more about the OEM cloud media provider pilot program's process and requirements, complete the inquiry form.
Decide if you need to create a cloud media provider
Cloud media providers are intended to be apps or services that act as a users' primary source for backing up and retrieving photos and videos from the cloud. If your app has a library of useful content, but it is not typically used as a photo storage solution, you should consider creating a document provider instead.
One active cloud provider per profile
There can be at most one active cloud media provider at a time for each Android profile. Users might remove or change their selected cloud media provider app at any time from photo picker settings.
By default, the Android photo picker will attempt to choose a cloud provider automatically.
- If there is only one eligible cloud provider on the device, that app will be selected as the current provider automatically.
If there are more than one eligible cloud providers on the device and one of them matches the OEM chosen default, then the OEM-chosen app will be selected.
If there are more than one eligible cloud providers on the device, and none of them match the OEM chosen default, no app will be selected.
Build your cloud media provider
The following diagram illustrates the sequence of events both before and during
a photo selection session between the Android app, the Android photo picker, the
local device's MediaProvider
, and a CloudMediaProvider
.
- The system initializes the user's preferred cloud provider and periodically syncs media metadata into the Android photo picker backend.
- When an Android app launches the photo picker, before showing a merged local or cloud item grid to the user, the photo picker performs a latency-sensitive incremental sync with the cloud provider to ensure results are as up-to-date as possible. After receiving a response, or when the deadline is reached, the photo picker grid now displays all accessible photos, combining those stored locally on your device with those synced from the cloud.
- While the user scrolls, the photo picker fetches media thumbnails from the cloud media provider to display in the UI.
- When the user completes the session and the results include a cloud media item, the photo picker requests file descriptors for the content, generates a URI, and grants access to the file to the calling application.
- The app is now able to open the URI and has read-only access to the media contents. By default, sensitive metadata is redacted. The photo picker leverages the FUSE file system to coordinate data exchange between the Android app and the cloud media provider.
Common Issues
Here are some important considerations to keep in mind when considering your implementation:
Avoid duplicate files
Since the Android photo picker has no way of inspecting the cloud media state,
the CloudMediaProvider
needs to provide the MEDIA_STORE_URI
in the cursor
row of any file that exists both in the cloud and on the local device, or the
user will see duplicate files in the photo picker.
Optimize image sizes for preview display
It's very important that the file returned from onOpenPreview
is not the full
resolution image, and adheres to the Size
being requested. Too large an image
will incur loading times in the UI, and too small an image might be pixelated or
blurry based on the screen size of the device.
Handle correct orientation
If thumbnails returned in onOpenPreview
do not contain their EXIF data, they
should be returned in the correct orientation to avoid thumbnails being rotated
incorrectly in the preview grid.
Prevent unauthorized access
Check for the MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION
before returning data to
the caller from the ContentProvider. This will prevent unauthorized apps from
accessing cloud data.
The CloudMediaProvider class
Derived from android.content.ContentProvider
, the CloudMediaProvider
class includes methods like the ones shown in the following example:
Kotlin
abstract class CloudMediaProvider : ContentProvider() {
@NonNull
abstract override fun onGetMediaCollectionInfo(@NonNull bundle: Bundle): Bundle
@NonNull
override fun onQueryAlbums(@NonNull bundle: Bundle): Cursor = TODO("Implement onQueryAlbums")
@NonNull
abstract override fun onQueryDeletedMedia(@NonNull bundle: Bundle): Cursor
@NonNull
abstract override fun onQueryMedia(@NonNull bundle: Bundle): Cursor
@NonNull
abstract override fun onOpenMedia(
@NonNull string: String,
@Nullable bundle: Bundle?,
@Nullable cancellationSignal: CancellationSignal?
): ParcelFileDescriptor
@NonNull
abstract override fun onOpenPreview(
@NonNull string: String,
@NonNull point: Point,
@Nullable bundle: Bundle?,
@Nullable cancellationSignal: CancellationSignal?
): AssetFileDescriptor
@Nullable
override fun onCreateCloudMediaSurfaceController(
@NonNull bundle: Bundle,
@NonNull callback: CloudMediaSurfaceStateChangedCallback
): CloudMediaSurfaceController? = null
}
Java
public abstract class CloudMediaProvider extends android.content.ContentProvider {
@NonNull
public abstract android.os.Bundle onGetMediaCollectionInfo(@NonNull android.os.Bundle);
@NonNull
public android.database.Cursor onQueryAlbums(@NonNull android.os.Bundle);
@NonNull
public abstract android.database.Cursor onQueryDeletedMedia(@NonNull android.os.Bundle);
@NonNull
public abstract android.database.Cursor onQueryMedia(@NonNull android.os.Bundle);
@NonNull
public abstract android.os.ParcelFileDescriptor onOpenMedia(@NonNull String, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
@NonNull
public abstract android.content.res.AssetFileDescriptor onOpenPreview(@NonNull String, @NonNull android.graphics.Point, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
@Nullable
public android.provider.CloudMediaProvider.CloudMediaSurfaceController onCreateCloudMediaSurfaceController(@NonNull android.os.Bundle, @NonNull android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback);
}
The CloudMediaProviderContract class
In addition to the primary CloudMediaProvider
implementation class, the
Android photo picker incorporates a CloudMediaProviderContract
class.
This class outlines the interoperability between the photo picker and the cloud
media provider, encompassing aspects such as MediaCollectionInfo
for
synchronization operations, anticipated Cursor
columns, and Bundle
extras.
Kotlin
object CloudMediaProviderContract {
const val EXTRA_ALBUM_ID = "android.provider.extra.ALBUM_ID"
const val EXTRA_LOOPING_PLAYBACK_ENABLED = "android.provider.extra.LOOPING_PLAYBACK_ENABLED"
const val EXTRA_MEDIA_COLLECTION_ID = "android.provider.extra.MEDIA_COLLECTION_ID"
const val EXTRA_PAGE_SIZE = "android.provider.extra.PAGE_SIZE"
const val EXTRA_PAGE_TOKEN = "android.provider.extra.PAGE_TOKEN"
const val EXTRA_PREVIEW_THUMBNAIL = "android.provider.extra.PREVIEW_THUMBNAIL"
const val EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED = "android.provider.extra.SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED"
const val EXTRA_SYNC_GENERATION = "android.provider.extra.SYNC_GENERATION"
const val MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION = "com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS"
const val PROVIDER_INTERFACE = "android.content.action.CLOUD_MEDIA_PROVIDER"
object MediaColumns {
const val DATE_TAKEN_MILLIS = "date_taken_millis"
const val DURATION_MILLIS = "duration_millis"
const val HEIGHT = "height"
const val ID = "id"
const val IS_FAVORITE = "is_favorite"
const val MEDIA_STORE_URI = "media_store_uri"
const val MIME_TYPE = "mime_type"
const val ORIENTATION = "orientation"
const val SIZE_BYTES = "size_bytes"
const val STANDARD_MIME_TYPE_EXTENSION = "standard_mime_type_extension"
const val STANDARD_MIME_TYPE_EXTENSION_ANIMATED_WEBP = 3 // 0x3
const val STANDARD_MIME_TYPE_EXTENSION_GIF = 1 // 0x1
const val STANDARD_MIME_TYPE_EXTENSION_MOTION_PHOTO = 2 // 0x2
const val STANDARD_MIME_TYPE_EXTENSION_NONE = 0 // 0x0
const val SYNC_GENERATION = "sync_generation"
const val WIDTH = "width"
}
object AlbumColumns {
const val DATE_TAKEN_MILLIS = "date_taken_millis"
const val DISPLAY_NAME = "display_name"
const val ID = "id"
const val MEDIA_COUNT = "album_media_count"
const val MEDIA_COVER_ID = "album_media_cover_id"
}
object MediaCollectionInfo {
const val ACCOUNT_CONFIGURATION_INTENT = "account_configuration_intent"
const val ACCOUNT_NAME = "account_name"
const val LAST_MEDIA_SYNC_GENERATION = "last_media_sync_generation"
const val MEDIA_COLLECTION_ID = "media_collection_id"
}
}
Java
public final class CloudMediaProviderContract {
public static final String EXTRA_ALBUM_ID = "android.provider.extra.ALBUM_ID";
public static final String EXTRA_LOOPING_PLAYBACK_ENABLED = "android.provider.extra.LOOPING_PLAYBACK_ENABLED";
public static final String EXTRA_MEDIA_COLLECTION_ID = "android.provider.extra.MEDIA_COLLECTION_ID";
public static final String EXTRA_PAGE_SIZE = "android.provider.extra.PAGE_SIZE";
public static final String EXTRA_PAGE_TOKEN = "android.provider.extra.PAGE_TOKEN";
public static final String EXTRA_PREVIEW_THUMBNAIL = "android.provider.extra.PREVIEW_THUMBNAIL";
public static final String EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED = "android.provider.extra.SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED";
public static final String EXTRA_SYNC_GENERATION = "android.provider.extra.SYNC_GENERATION";
public static final String MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION = "com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS";
public static final String PROVIDER_INTERFACE = "android.content.action.CLOUD_MEDIA_PROVIDER";
}
// Columns available for every media item
public static final class CloudMediaProviderContract.MediaColumns {
public static final String DATE_TAKEN_MILLIS = "date_taken_millis";
public static final String DURATION_MILLIS = "duration_millis";
public static final String HEIGHT = "height";
public static final String ID = "id";
public static final String IS_FAVORITE = "is_favorite";
public static final String MEDIA_STORE_URI = "media_store_uri";
public static final String MIME_TYPE = "mime_type";
public static final String ORIENTATION = "orientation";
public static final String SIZE_BYTES = "size_bytes";
public static final String STANDARD_MIME_TYPE_EXTENSION = "standard_mime_type_extension";
public static final int STANDARD_MIME_TYPE_EXTENSION_ANIMATED_WEBP = 3; // 0x3
public static final int STANDARD_MIME_TYPE_EXTENSION_GIF = 1; // 0x1
public static final int STANDARD_MIME_TYPE_EXTENSION_MOTION_PHOTO = 2; // 0x2
public static final int STANDARD_MIME_TYPE_EXTENSION_NONE = 0; // 0x0
public static final String SYNC_GENERATION = "sync_generation";
public static final String WIDTH = "width";
}
// Columns available for every album item
public static final class CloudMediaProviderContract.AlbumColumns {
public static final String DATE_TAKEN_MILLIS = "date_taken_millis";
public static final String DISPLAY_NAME = "display_name";
public static final String ID = "id";
public static final String MEDIA_COUNT = "album_media_count";
public static final String MEDIA_COVER_ID = "album_media_cover_id";
}
// Media Collection metadata that is cached by the OS to compare sync states.
public static final class CloudMediaProviderContract.MediaCollectionInfo {
public static final String ACCOUNT_CONFIGURATION_INTENT = "account_configuration_intent";
public static final String ACCOUNT_NAME = "account_name";
public static final String LAST_MEDIA_SYNC_GENERATION = "last_media_sync_generation";
public static final String MEDIA_COLLECTION_ID = "media_collection_id";
}
onGetMediaCollectionInfo
The onGetMediaCollectionInfo()
method is used by the operating system to
assess the validity of its cached cloud media items and determine necessary
synchronization with the cloud media provider. Due to the potential for frequent
calls by the operating system, onGetMediaCollectionInfo()
is considered
performance-critical; it is crucial to avoid long-running operations or side
effects that could negatively impact performance. The operating system caches
previous responses from this method and compares them with subsequent responses
to determine the appropriate actions.
Kotlin
abstract fun onGetMediaCollectionInfo(extras: Bundle): Bundle
Java
@NonNull
public abstract Bundle onGetMediaCollectionInfo(@NonNull Bundle extras);
The returned MediaCollectionInfo
bundle includes the following constants:
onQueryMedia
The onQueryMedia()
method is used to populate the main photo grid in
photo picker in a variety of views. These calls might be latency sensitive, and
can be called as part of a background proactive sync, or during photo picker
sessions when a full or incremental sync state is required. The photo picker
user interface won't wait indefinitely for a response to display results, and
might time out these requests for user interface purposes. The returned cursor
will still attempt to be processed into the photo picker's database for future
sessions.
This method returns a Cursor
representing all media items in the media
collection optionally filtered by the provided extras and sorted in reverse
chronological order of MediaColumns#DATE_TAKEN_MILLIS
(most recent items
first).
The returned CloudMediaProviderContract
bundle includes the following
constants:
EXTRA_ALBUM_ID
EXTRA_LOOPING_PLAYBACK_ENABLED
EXTRA_MEDIA_COLLECTION_ID
EXTRA_PAGE_SIZE
EXTRA_PAGE_TOKEN
EXTRA_PREVIEW_THUMBNAIL
EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED
EXTRA_SYNC_GENERATION
MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION
PROVIDER_INTERFACE
The cloud media provider must set
CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID
as part of the returned
Bundle
. Not setting this is an error and invalidates the returned Cursor
. If
the cloud media provider handled any filters in the provided extras, it must add
the key to the ContentResolver#EXTRA_HONORED_ARGS
as part of the returned
Cursor#setExtras
.
onQueryDeletedMedia
The onQueryDeletedMedia()
method is used to ensure deleted items in the
cloud account are correctly removed from the photo picker user interface. Due to
their potential latency sensitivity, these calls might be initiated as part of:
- Background proactive synchronization
- Photo picker sessions (when a full or incremental sync state is required)
The photo picker's user interface prioritizes a responsive user experience and
will not wait indefinitely for a response. To maintain smooth interactions,
timeouts might occur. Any returned Cursor
will still attempt to be processed
into the photo picker's database for future sessions.
This method returns a Cursor
representing all deleted media items in the
entire media collection within the current provider version as returned by
onGetMediaCollectionInfo()
. These items can be optionally filtered by extras.
The cloud media provider must set the
CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID
as part of the returned
Cursor#setExtras
Not setting this is an error and invalidates the Cursor
. If
the provider handled any filters in the provided extras, it must add the key to
the ContentResolver#EXTRA_HONORED_ARGS
.
onQueryAlbums
The onQueryAlbums()
method is used to fetch a list of Cloud albums that
are available in the cloud provider, and their associated metadata. See
CloudMediaProviderContract.AlbumColumns
for additional details.
This method returns a Cursor
representing all album items in the media
collection optionally filtered by the provided extras and sorted in reverse
chronological order of AlbumColumns#DATE_TAKEN_MILLIS
, most recent items
first. The cloud media provider must set the
CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID
as part of the returned
Cursor
. Not setting this is an error and invalidates the returned Cursor
. If
the provider handled any filters in the provided extras, it must add the key to
the ContentResolver#EXTRA_HONORED_ARGS
as part of the returned Cursor
.
onOpenMedia
The onOpenMedia()
method should return the full size media identified by
the provided mediaId
. If this method blocks while downloading content to the
device, you should periodically check the provided CancellationSignal
to abort
abandoned requests.
onOpenPreview
The onOpenPreview()
method should return a thumbnail of the provided
size
for the item of the provided mediaId. The thumbnail should be in the
original CloudMediaProviderContract.MediaColumns#MIME_TYPE
and is expected to
be much lower resolution than the item returned by onOpenMedia
. If this method
is blocked while downloading content to the device, you should periodically
check the provided CancellationSignal
to abort abandoned requests.
onCreateCloudMediaSurfaceController
The onCreateCloudMediaSurfaceController()
method should return a
CloudMediaSurfaceController
used for rendering the preview of media items, or
null
if preview rendering is not supported.
The CloudMediaSurfaceController
manages rendering the preview of media items
on given instances of Surface
. The methods of this class are meant to be
asynchronous, and should not block by performing any heavy operation. A single
CloudMediaSurfaceController
instance is responsible for rendering multiple
media items associated with multiple surfaces.
The CloudMediaSurfaceController
has support for the following list of
lifecycle callbacks:
onConfigChange
onDestroy
onMediaPause
onMediaPlay
onMediaSeekTo
onPlayerCreate
onPlayerRelease
onSurfaceChanged
onSurfaceCreated
onSurfaceDestroyed