Un proveedor de contenido multimedia en la nube proporciona contenido multimedia adicional en la nube al selector de fotos de Android. Los usuarios pueden seleccionar fotos o videos proporcionados por el proveedor de contenido multimedia en la nube cuando una app usa ACTION_PICK_IMAGES o ACTION_GET_CONTENT para solicitar archivos multimedia al usuario. Un proveedor de contenido multimedia en la nube también puede brindar información sobre los álbumes, que se pueden explorar en el selector de fotos de Android.
Antes de comenzar
Ten en cuenta los siguientes elementos antes de comenzar a compilar tu proveedor de contenido multimedia en la nube.
Requisitos
Android está ejecutando un programa piloto para permitir que las apps nominadas por los OEM se conviertan en proveedores de contenido multimedia en la nube. Por el momento, solo las apps nominadas por los OEM pueden participar en este programa para convertirse en proveedores de contenido multimedia en la nube para Android. Cada OEM puede nominar hasta 3 apps. Una vez aprobadas, estas apps se vuelven accesibles como proveedores de contenido multimedia en la nube en cualquier dispositivo con GMS y Android en el que estén instaladas.
Android mantiene una lista del servidor de todos los proveedores de la nube aptos. Cada OEM puede elegir un proveedor de servicios en la nube predeterminado con una superposición configurable. Las apps nominadas deben cumplir con todos los requisitos técnicos y pasar todas las pruebas de calidad. Para obtener más información sobre el proceso y los requisitos del programa de prueba piloto de proveedores de contenido multimedia en la nube para OEM, completa el formulario de consulta.
Decide si necesitas crear un proveedor de contenido multimedia en la nube
Los proveedores de contenido multimedia en la nube están diseñados para ser apps o servicios que actúan como la fuente principal de los usuarios para crear copias de seguridad y recuperar fotos y videos de la nube. Si tu app tiene una biblioteca de contenido útil, pero no se usa como solución de almacenamiento de fotos, considera crear un proveedor de documentos.
Un proveedor de servicios en la nube activo por perfil
Puede haber, como máximo, un proveedor de contenido multimedia en la nube activo a la vez para cada perfil de Android. Los usuarios pueden quitar o cambiar la app del proveedor de contenido multimedia en la nube que seleccionaron en cualquier momento desde la configuración del selector de fotos.
De forma predeterminada, el selector de fotos de Android intentará elegir un proveedor de servicios en la nube automáticamente.
- Si solo hay un proveedor de servicios en la nube apto en el dispositivo, esa app se seleccionará automáticamente como el proveedor actual.
Si hay más de un proveedor de servicios en la nube apto en el dispositivo y uno de ellos coincide con el valor predeterminado elegido por el OEM, se seleccionará la app elegida por el OEM.
Si hay más de un proveedor de servicios en la nube apto en el dispositivo y ninguno de ellos coincide con el valor predeterminado elegido por el OEM, no se seleccionará ninguna app.
Crea tu proveedor de contenido multimedia en la nube
En el siguiente diagrama, se ilustra la secuencia de eventos antes y durante una sesión de selección de fotos entre la app para Android, el selector de fotos de Android, el MediaProvider del dispositivo local y un CloudMediaProvider.
- El sistema inicializa el proveedor de servicios en la nube preferido del usuario y sincroniza periódicamente los metadatos de los medios en el backend del selector de fotos de Android.
- Cuando una app para Android inicia el selector de fotos, antes de mostrarle al usuario una cuadrícula combinada de elementos locales o de la nube, el selector de fotos realiza una sincronización incremental sensible a la latencia con el proveedor de servicios en la nube para garantizar que los resultados estén lo más actualizados posible. Después de recibir una respuesta o cuando se alcanza la fecha límite, la cuadrícula del selector de fotos ahora muestra todas las fotos accesibles, combinando las que se almacenan localmente en tu dispositivo con las que se sincronizan desde la nube.
- Mientras el usuario se desplaza, el selector de fotos recupera miniaturas de contenido multimedia del proveedor de contenido multimedia en la nube para mostrarlas en la IU.
- Cuando el usuario completa la sesión y los resultados incluyen un elemento multimedia de la nube, el selector de fotos solicita descriptores de archivo para el contenido, genera un URI y otorga acceso al archivo a la aplicación que realiza la llamada.
- Ahora la app puede abrir el URI y tiene acceso de solo lectura al contenido multimedia. De forma predeterminada, los metadatos sensibles se ocultan. El selector de fotos aprovecha el sistema de archivos FUSE para coordinar el intercambio de datos entre la app para Android y el proveedor de contenido multimedia en la nube.
Problemas comunes
Estas son algunas consideraciones importantes que debes tener en cuenta cuando pienses en tu implementación:
Cómo evitar archivos duplicados
Dado que el selector de fotos de Android no tiene forma de inspeccionar el estado del contenido multimedia en la nube, el CloudMediaProvider debe proporcionar el MEDIA_STORE_URI en la fila del cursor de cualquier archivo que exista tanto en la nube como en el dispositivo local. De lo contrario, el usuario verá archivos duplicados en el selector de fotos.
Optimiza el tamaño de las imágenes para la visualización de vistas previas
Es muy importante que el archivo que se devuelve de onOpenPreview no sea la imagen en resolución completa y que cumpla con el Size solicitado. Una imagen demasiado grande generará tiempos de carga en la IU, y una imagen demasiado pequeña podría verse pixelada o borrosa según el tamaño de la pantalla del dispositivo.
Cómo controlar la orientación correcta
Si las miniaturas que se muestran en onOpenPreview no contienen sus datos EXIF, se deben mostrar en la orientación correcta para evitar que se roten de forma incorrecta en la cuadrícula de vista previa.
Evitar el acceso no autorizado
Verifica MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION antes de devolver datos al llamador desde ContentProvider. Esto evitará que las apps no autorizadas accedan a los datos de la nube.
La clase CloudMediaProvider
Derivada de android.content.ContentProvider, la clase CloudMediaProvider incluye métodos como los que se muestran en el siguiente ejemplo:
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);
}
La clase CloudMediaProviderContract
Además de la clase de implementación principal CloudMediaProvider, el selector de fotos de Android incorpora una clase CloudMediaProviderContract.
Esta clase describe la interoperabilidad entre el selector de fotos y el proveedor de contenido multimedia en la nube, y abarca aspectos como MediaCollectionInfo para las operaciones de sincronización, las columnas Cursor previstas y los elementos adicionales Bundle.
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
El sistema operativo usa el método onGetMediaCollectionInfo() para evaluar la validez de los elementos multimedia en la nube almacenados en caché y determinar la sincronización necesaria con el proveedor de contenido multimedia en la nube. Debido a la posibilidad de llamadas frecuentes por parte del sistema operativo, onGetMediaCollectionInfo() se considera fundamental para el rendimiento. Es crucial evitar operaciones de larga duración o efectos secundarios que puedan afectar negativamente el rendimiento. El sistema operativo almacena en caché las respuestas anteriores de este método y las compara con las respuestas posteriores para determinar las acciones adecuadas.
Kotlin
abstract fun onGetMediaCollectionInfo(extras: Bundle): Bundle
Java
@NonNull
public abstract Bundle onGetMediaCollectionInfo(@NonNull Bundle extras);
El paquete MediaCollectionInfo que se devuelve incluye las siguientes constantes:
onQueryMedia
El método onQueryMedia() se usa para completar la cuadrícula de fotos principal en el selector de fotos en una variedad de vistas. Estas llamadas pueden ser sensibles a la latencia y se pueden realizar como parte de una sincronización proactiva en segundo plano o durante las sesiones del selector de fotos cuando se requiere un estado de sincronización completo o incremental. La interfaz de usuario del selector de fotos no esperará indefinidamente una respuesta para mostrar los resultados y es posible que se agote el tiempo de espera de estas solicitudes para fines de la interfaz de usuario. El cursor devuelto seguirá intentando procesarse en la base de datos del selector de fotos para sesiones futuras.
Este método devuelve un Cursor que representa todos los elementos multimedia de la colección de medios, filtrados de forma opcional por los elementos adicionales proporcionados y ordenados en orden cronológico inverso de MediaColumns#DATE_TAKEN_MILLIS (los elementos más recientes primero).
El paquete CloudMediaProviderContract que se devuelve incluye las siguientes constantes:
EXTRA_ALBUM_IDEXTRA_LOOPING_PLAYBACK_ENABLEDEXTRA_MEDIA_COLLECTION_IDEXTRA_PAGE_SIZEEXTRA_PAGE_TOKENEXTRA_PREVIEW_THUMBNAILEXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLEDEXTRA_SYNC_GENERATIONMANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSIONPROVIDER_INTERFACE
El proveedor de contenido multimedia en la nube debe establecer CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte del Bundle devuelto. Si no se configura este parámetro, se producirá un error y se invalidará el objeto Cursor que se devolvió. Si el proveedor de contenido multimedia en la nube controló algún filtro en los elementos adicionales proporcionados, debe agregar la clave a ContentResolver#EXTRA_HONORED_ARGS como parte del Cursor#setExtras que se devolvió.
onQueryDeletedMedia
El método onQueryDeletedMedia() se usa para garantizar que los elementos borrados en la cuenta de la nube se quiten correctamente de la interfaz de usuario del selector de fotos. Debido a su posible sensibilidad a la latencia, estas llamadas se pueden iniciar como parte de lo siguiente:
- Sincronización proactiva en segundo plano
- Sesiones del selector de fotos (cuando se requiere un estado de sincronización completo o incremental)
La interfaz de usuario del selector de fotos prioriza una experiencia del usuario responsiva y no esperará indefinidamente una respuesta. Para mantener interacciones fluidas, es posible que se produzcan tiempos de espera. Cualquier Cursor que se devuelva se intentará procesar en la base de datos del selector de fotos para sesiones futuras.
Este método devuelve un objeto Cursor que representa todos los elementos multimedia borrados en toda la colección de medios dentro de la versión actual del proveedor, tal como lo devuelve onGetMediaCollectionInfo(). Estos elementos se pueden filtrar de forma opcional por extras.
El proveedor de contenido multimedia en la nube debe establecer CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte del Cursor#setExtras que se devuelve. No establecerlo es un error y anula el Cursor. Si el proveedor controló algún filtro en los elementos adicionales proporcionados, debe agregar la clave a ContentResolver#EXTRA_HONORED_ARGS.
onQueryAlbums
El método onQueryAlbums() se usa para recuperar una lista de los álbumes de Cloud que están disponibles en el proveedor de servicios en la nube y sus metadatos asociados. Consulta CloudMediaProviderContract.AlbumColumns para obtener más detalles.
Este método devuelve un objeto Cursor que representa todos los elementos del álbum en la colección de medios, filtrados de forma opcional por los elementos adicionales proporcionados y ordenados en orden cronológico inverso de AlbumColumns#DATE_TAKEN_MILLIS, con los elementos más recientes primero. El proveedor de contenido multimedia en la nube debe establecer CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte del Cursor devuelto. Si no se configura este parámetro, se producirá un error y se invalidará el objeto Cursor que se devolvió. Si el proveedor controló algún filtro en los elementos adicionales proporcionados, debe agregar la clave a ContentResolver#EXTRA_HONORED_ARGS como parte del Cursor devuelto.
onOpenMedia
El método onOpenMedia() debe devolver el medio de tamaño completo identificado por el mediaId proporcionado. Si este método se bloquea mientras se descarga contenido en el dispositivo, debes verificar periódicamente el CancellationSignal proporcionado para anular las solicitudes abandonadas.
onOpenPreview
El método onOpenPreview() debe devolver una miniatura del size proporcionado para el elemento del mediaId proporcionado. La miniatura debe estar en el CloudMediaProviderContract.MediaColumns#MIME_TYPE original y se espera que tenga una resolución mucho menor que el elemento que devuelve onOpenMedia. Si este método se bloquea mientras se descarga contenido en el dispositivo, debes verificar periódicamente el CancellationSignal proporcionado para anular las solicitudes abandonadas.
onCreateCloudMediaSurfaceController
El método onCreateCloudMediaSurfaceController() debe devolver un objeto CloudMediaSurfaceController que se use para renderizar la vista previa de los elementos multimedia o null si no se admite la renderización de la vista previa.
El CloudMediaSurfaceController administra la renderización de la vista previa de los elementos multimedia en instancias determinadas de Surface. Los métodos de esta clase deben ser asíncronos y no deben bloquearse realizando ninguna operación pesada. Una sola instancia de CloudMediaSurfaceController es responsable de renderizar varios elementos multimedia asociados con varias superficies.
CloudMediaSurfaceController admite la siguiente lista de devoluciones de llamada del ciclo de vida:
onConfigChangeonDestroyonMediaPauseonMediaPlayonMediaSeekToonPlayerCreateonPlayerReleaseonSurfaceChangedonSurfaceCreatedonSurfaceDestroyed