Cómo crear un proveedor de contenido multimedia en la nube para Android

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 que proporciona el proveedor de contenido multimedia en la nube cuando una app usa ACTION_PICK_IMAGES o ACTION_GET_CONTENT para solicitarle archivos multimedia al usuario. Un proveedor de contenido multimedia en la nube también puede proporcionar información sobre álbumes, que se puede 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.

Elegibilidad

Android está ejecutando un programa piloto para permitir que las apps nominadas por OEM se conviertan en proveedores de contenido multimedia en la nube. Por el momento, solo las apps nominadas por OEM son aptas para participar en este programa y convertirse en proveedor de contenido multimedia en la nube para Android. Cada OEM puede nominar hasta 3 apps. Una vez que se aprueben, se podrá acceder a estas apps como proveedores de contenido multimedia en la nube en cualquier dispositivo Android con GMS en el que estén instaladas.

Android mantiene una lista del servidor de todos los proveedores de servicios en la nube aptos. Cada OEM puede elegir un proveedor de servicios en la nube predeterminado mediante 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 piloto del proveedor de medios en la nube de OEM, completa el formulario de consultas.

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úen como la fuente principal del usuario para crear copias de seguridad de fotos y videos de la nube y recuperarlos. Si tu app tiene una biblioteca de contenido útil, pero no se suele usar como una solución de almacenamiento de fotos, deberías considerar crear un proveedor de documentos en su lugar.

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 su app seleccionada de proveedor de contenido multimedia en la nube en cualquier momento desde la configuración del selector de fotos.

De forma predeterminada, el selector de fotos de Android intentará elegir automáticamente un proveedor de servicios en la nube.

  • Si solo hay un proveedor de servicios en la nube apto en el dispositivo, esa app se seleccionará automáticamente como 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 predeterminado del 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 OEM elegido de forma predeterminada, 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.

Diagrama de secuencias que muestra el flujo del selector de fotos a un proveedor de contenido multimedia en la nube
Figura 1: Diagrama de secuencias de eventos durante una sesión de selección de fotos
  1. El sistema inicializa el proveedor de servicios en la nube preferido del usuario y sincroniza de forma periódica los metadatos de los medios en el backend del selector de fotos de Android.
  2. Cuando una app para Android inicia el selector de fotos, antes de mostrarle al usuario una cuadrícula combinada de elementos locales o en 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 cumple la fecha límite, la cuadrícula del selector de fotos ahora muestra todas las fotos a las que se puede acceder y combina las almacenadas localmente en tu dispositivo con las sincronizadas desde la nube.
  3. 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.
  4. Cuando el usuario completa la sesión y los resultados incluyen un elemento multimedia en la nube, el selector de fotos solicita descriptores de archivos para el contenido, genera un URI y otorga acceso al archivo a la aplicación que realiza la llamada.
  5. La app ahora puede abrir el URI y tiene acceso de solo lectura al contenido multimedia. Los metadatos sensibles se ocultan de forma predeterminada. 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 consideras tu implementación:

Cómo evitar archivos duplicados

Como el selector de fotos de Android no tiene forma de inspeccionar el estado del contenido multimedia en la nube, el CloudMediaProvider debe proporcionar el elemento MEDIA_STORE_URI en la fila de cursor de cualquier archivo que exista en la nube y en el dispositivo local, o el usuario verá archivos duplicados en el selector de fotos.

Cómo optimizar los tamaños de las imágenes para la vista previa de anuncios

Es muy importante que el archivo que muestra onOpenPreview no sea la imagen en resolución máxima y cumpla con la Size que se solicita. Una imagen demasiado grande generará tiempos de carga en la IU, y una imagen demasiado pequeña podría estar 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 manera incorrecta en la cuadrícula de vista previa.

Evita el acceso no autorizado

Busca el MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION antes de mostrar datos al emisor desde el ContentProvider. Esto evitará que 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 de CloudMediaProvider, el selector de fotos de Android incorpora una clase CloudMediaProviderContract. En esta clase, se describe la interoperabilidad entre el selector de fotos y el proveedor de contenido multimedia en la nube, que abarca aspectos como MediaCollectionInfo para las operaciones de sincronización, columnas previstas de Cursor y Bundle adicionales.

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 sus elementos de medios en la nube almacenados en caché y determinar la sincronización necesaria con el proveedor de contenido multimedia en la nube. Debido al potencial de que el sistema operativo realice llamadas frecuentes, onGetMediaCollectionInfo() se considera fundamental para el rendimiento. Es fundamental evitar operaciones de larga duración o efectos secundarios que podrían 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 muestra incluye las siguientes constantes:

onQueryMedia

El método onQueryMedia() se usa para propagar 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 las puede llamar 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á de forma indefinida una respuesta para mostrar los resultados y podría agotar el tiempo de espera de estas solicitudes para fines de interfaz de usuario. El cursor que se muestra intentará procesarse en la base de datos del selector de fotos para sesiones futuras.

Este método muestra una Cursor que representa todos los elementos multimedia de la colección multimedia, que se filtran de forma opcional por los elementos adicionales proporcionados y se ordenan en orden cronológico inverso de MediaColumns#DATE_TAKEN_MILLIS (primero los elementos más recientes).

El paquete CloudMediaProviderContract que se muestra incluye las siguientes constantes:

El proveedor de contenido multimedia en la nube debe establecer CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte del Bundle que se muestra. No establecerlo es un error y se invalida el Cursor que se muestra. Si el proveedor de contenido multimedia en la nube controló algún filtro en los servicios adicionales proporcionados, debe agregar la clave al ContentResolver#EXTRA_HONORED_ARGS como parte del Cursor#setExtras que se muestra.

onQueryDeletedMedia

El método onQueryDeletedMedia() se usa para garantizar que los elementos borrados de la cuenta en la nube se quiten correctamente de la interfaz de usuario del selector de fotos. Debido a su posible sensibilidad a la latencia, estas llamadas pueden iniciarse 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, pueden ocurrir tiempos de espera. Cualquier objeto Cursor que se muestre intentará procesarse en la base de datos del selector de fotos para sesiones futuras.

Este método muestra una Cursor que representa todos los elementos multimedia borrados en la colección completa de contenido multimedia dentro de la versión actual del proveedor como lo muestra onGetMediaCollectionInfo(). De manera opcional, estos elementos se pueden filtrar por servicios adicionales. El proveedor de contenido multimedia en la nube debe establecer CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte del Cursor#setExtras que se muestra. Si la configuración no es un error, se invalida Cursor. Si el proveedor controló algún filtro en los servicios adicionales proporcionados, debe agregar la clave a ContentResolver#EXTRA_HONORED_ARGS.

onQueryAlbums

El método onQueryAlbums() se usa para recuperar una lista de álbumes en la nube que están disponibles en el proveedor de servicios en la nube y sus metadatos asociados. Para obtener más detalles, consulta CloudMediaProviderContract.AlbumColumns.

Este método muestra una Cursor que representa todos los elementos del álbum en la colección de contenido multimedia, que se pueden filtrar de forma opcional por los elementos adicionales proporcionados y se ordenan 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 configurar CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte del Cursor que se muestra. No establecerlo es un error y se invalida el Cursor que se muestra. Si el proveedor controló algún filtro en los servicios adicionales proporcionados, debe agregar la clave a ContentResolver#EXTRA_HONORED_ARGS como parte del objeto Cursor que se muestra.

onOpenMedia

El método onOpenMedia() debe mostrar el contenido multimedia 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 mostrar 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 más baja que el elemento que muestra 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 mostrar un CloudMediaSurfaceController que se usa para renderizar la vista previa de los elementos multimedia, o bien null si no se admite la renderización de vista previa.

CloudMediaSurfaceController administra la renderización de la vista previa de los elementos multimedia en instancias determinadas de Surface. Los métodos de esta clase están destinados a ser asíncronos y no se deben bloquear mediante la realización de operaciones pesadas. Una sola instancia de CloudMediaSurfaceController es responsable de renderizar varios elementos multimedia asociados con varias plataformas.

CloudMediaSurfaceController admite la siguiente lista de devoluciones de llamada de ciclo de vida: