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 de la nube al Android selector de fotos. 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 contenido multimedia en la nube proveedor también puede brindar información sobre los álbumes, que se pueden explorar en la Selector de fotos de Android

Antes de comenzar

Ten en cuenta los siguientes elementos antes de comenzar a crear proveedor de contenido multimedia.

Requisitos

Android está ejecutando un programa piloto para permitir que las apps nominadas para OEM se conviertan en la nube proveedores de medios. Solo las apps nominadas por OEM son aptas para participar en como proveedor de contenido multimedia en la nube para Android. Cada El OEM puede nominar hasta 3 apps. Una vez aprobadas, 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 esté instalado.

Android mantiene una lista del servidor de todos los proveedores de servicios en la nube aptos. Cada OEM puedes elegir un proveedor de servicios en la nube predeterminado con una superposición configurable. Nominada Las apps deben cumplir con todos los requisitos técnicos y aprobar todas las pruebas de calidad. Para aprender más información sobre el proceso del programa piloto para proveedores de medios en la nube de OEM y 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 son apps o servicios que actúan como fuente principal para crear copias de seguridad y recuperar fotos y videos de la nube. Si tu app tiene una biblioteca de contenido útil, pero no suele usarse como almacenamiento de fotos, deberías 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 activo de contenido multimedia en la nube a la vez para cada instancia de Android perfil. Los usuarios pueden quitar o cambiar su proveedor de contenido multimedia en la nube seleccionado 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, se actualizará la app 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 coincidan con la configuración predeterminada del OEM y, luego, 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 coinciden con la configuración predeterminada elegida por el OEM, no se seleccionará ninguna app.

Compila 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 aplicación para Android, el selector de fotos de Android, MediaProvider del dispositivo local y 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 secuencia 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, de manera periódica, Sincroniza los metadatos multimedia en el backend del selector de fotos de Android.
  2. Cuando una app para Android inicia el selector de fotos, antes de mostrar un local combinado o cuadrícula de elementos en la nube al usuario, el selector de fotos realiza una evaluación sincronización incremental con el proveedor de servicios en la nube para garantizar que los resultados estén lo más actualizados de la forma más eficaz posible. Después de recibir una respuesta o cuando se cumple el plazo, el la cuadrícula del selector de fotos ahora muestra todas las fotos accesibles, combinando las almacenadas localmente en tu dispositivo con los que están sincronizados desde la nube.
  3. Mientras el usuario se desplaza, el selector de fotos obtiene miniaturas de medios de la proveedor de contenido multimedia en la nube para mostrar en la IU.
  4. Cuando el usuario completa la sesión y los resultados incluyen un recurso multimedia en 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.
  5. La app ahora puede abrir el URI y tiene acceso de solo lectura al contenido multimedia contenidos. De forma predeterminada, los metadatos sensibles están ocultos. 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

Aquí encontrarás algunas consideraciones importantes para tener en cuenta al considerar 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 de la nube, CloudMediaProvider debe proporcionar el MEDIA_STORE_URI en el cursor de cualquier archivo que exista en la nube y en el dispositivo local, o los usuarios verán los archivos duplicados en el selector de fotos.

Cómo optimizar el tamaño de las imágenes para la vista previa

Es muy importante que el archivo que muestra onOpenPreview no sea el archivo resolución y que cumpla con el Size que se solicita. Una imagen demasiado grande generarán tiempos de carga en la IU, y una imagen demasiado pequeña puede estar pixelada o desenfocado en función del 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, tendrán se deben devolver en la orientación correcta para evitar que las miniaturas se roten incorrectamente en la cuadrícula de vista previa.

Evita el acceso no autorizado

Verifica el valor de MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION antes de devolver los datos a el llamador del ContentProvider. Esto evitará que las apps no autorizadas el acceso a los datos en la nube.

La clase CloudMediaProvider

Derivado de android.content.ContentProvider, el 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 elemento El selector de fotos de Android incorpora una clase CloudMediaProviderContract. En esta clase, se describe la interoperabilidad entre el selector de fotos y la nube proveedor de medios, que incluye aspectos como MediaCollectionInfo para operaciones de sincronización, columnas Cursor anticipadas 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 la validez de los elementos de medios en la nube almacenados en caché y determinar y la sincronización con el proveedor de contenido multimedia en la nube. Debido a la posibilidad de que los usuarios llamadas por el sistema operativo, se considera onGetMediaCollectionInfo() es fundamental para el rendimiento; es crucial evitar operaciones de larga duración o procesos que podrían afectar el rendimiento de forma negativa. El sistema operativo almacena en caché 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 selector de fotos en una variedad de vistas. Estas llamadas pueden ser sensibles a la latencia y Se puede llamar como parte de una sincronización proactiva en segundo plano o durante el selector de fotos. sesiones cuando se requiere un estado de sincronización completo o incremental. El selector de fotos de usuario no esperará indefinidamente una respuesta para mostrar los resultados podría agotar el tiempo de espera de estas solicitudes para la interfaz de usuario. El cursor que se muestra tratarán de ser procesados en la base de datos del selector de fotos para el futuro sesiones.

Este método muestra un Cursor que representa todos los elementos multimedia del contenido. La colección se filtra de manera opcional por los extras proporcionados y se ordena de forma inversa. orden cronológico de MediaColumns#DATE_TAKEN_MILLIS (elementos más recientes primero).

El paquete CloudMediaProviderContract que se muestra incluye lo siguiente: constantes:

El proveedor de contenido multimedia en la nube debe establecer CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte de la Bundle Si no se establece, se considerará un error y se invalidará el Cursor que se muestra. Si el proveedor de contenido multimedia en la nube maneja los filtros en los extras proporcionados, debe agregar la clave a ContentResolver#EXTRA_HONORED_ARGS como parte del Cursor#setExtras

onQueryDeletedMedia

Se usa el método onQueryDeletedMedia() para garantizar que se borren los elementos del la cuenta de Cloud se quitaron correctamente de la interfaz de usuario del selector de fotos. Debido a la sensibilidad de latencia potencial, 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 completa o incremental)

La interfaz de usuario del selector de fotos prioriza una experiencia del usuario responsiva y no esperarán indefinidamente una respuesta. Para mantener interacciones fluidas, se pueden producir tiempos de espera. Se intentará procesar cualquier solicitud de Cursor que se devuelva. en la base de datos del selector de fotos para sesiones futuras.

Este método muestra un Cursor que representa todos los elementos multimedia borrados en el toda la colección de contenido multimedia dentro de la versión actual del proveedor como lo muestra onGetMediaCollectionInfo() Estos elementos se pueden filtrar de manera opcional por extras. El proveedor de contenido multimedia en la nube debe establecer la CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte de la Cursor#setExtras Si no configuras esto, se considerará un error y se invalidará Cursor. Si el proveedor manejó cualquier filtro en los extras proporcionados, debe agregar la clave al 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. Consulta CloudMediaProviderContract.AlbumColumns para obtener más detalles.

Este método devuelve un Cursor que representa todos los elementos del álbum en el contenido multimedia. La colección se filtra de manera opcional por los extras proporcionados y se ordena de forma inversa. orden cronológico de AlbumColumns#DATE_TAKEN_MILLIS , elementos más recientes antes de empezar. El proveedor de contenido multimedia en la nube debe establecer la CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte de la Cursor Si no se establece, se considerará un error y se invalidará el Cursor que se muestra. Si el proveedor manejó cualquier filtro en los extras proporcionados, debe agregar la clave al el ContentResolver#EXTRA_HONORED_ARGS como parte del 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 durante la descarga de contenido al , debes verificar periódicamente la CancellationSignal proporcionada para anular las solicitudes abandonadas.

onOpenPreview

El método onOpenPreview() debe mostrar una miniatura del elemento proporcionado. size para el elemento del mediaId proporcionado. La miniatura debe estar en CloudMediaProviderContract.MediaColumns#MIME_TYPE original y se espera que ser de resolución mucho más baja que el elemento que muestra onOpenMedia. Si este método se bloquea al descargar contenido en el dispositivo, deberías Verifica 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 elementos multimedia. null si no se admite la renderización de la vista previa

CloudMediaSurfaceController administra la renderización de la vista previa de elementos multimedia. en instancias determinadas de Surface. Los métodos de esta clase deben ser asíncronas y no debe bloquearse realizando ninguna operación pesada. Un solo La instancia CloudMediaSurfaceController es responsable de renderizar varias elementos multimedia asociados con varias plataformas.

CloudMediaSurfaceController es compatible con la siguiente lista de devoluciones de llamada de ciclo de vida: