Créer un fournisseur de services multimédias cloud pour Android

Un fournisseur de contenus multimédias cloud fournit du contenu multimédia cloud supplémentaire au sélecteur de photos Android. Les utilisateurs peuvent sélectionner des photos ou des vidéos fournies par le fournisseur de contenus multimédias cloud lorsqu'une application utilise ACTION_PICK_IMAGES ou ACTION_GET_CONTENT pour demander des fichiers multimédias à l'utilisateur. Un fournisseur de contenus multimédias cloud peut également fournir des informations sur les albums qu'il est possible de parcourir dans le sélecteur de photos Android.

Avant de commencer

Tenez compte des éléments suivants avant de commencer à créer votre fournisseur de services multimédias cloud.

Éligibilité

Android exécute un programme pilote pour permettre aux applications nommées par les OEM de devenir des fournisseurs de services multimédias cloud. Pour le moment, seules les applications nommées par des OEM peuvent participer à ce programme pour devenir fournisseur de médias cloud pour Android. Chaque OEM peut nominer jusqu'à trois applications. Une fois approuvées, ces applications deviennent accessibles en tant que fournisseurs de services multimédias cloud sur tous les appareils GMS Android sur lesquels elles sont installées.

Android tient à jour une liste de tous les fournisseurs cloud éligibles côté serveur. Chaque OEM peut choisir un fournisseur cloud par défaut à l'aide d'une superposition configurable. Les applications nommées doivent répondre à toutes les exigences techniques et réussir tous les tests de qualité. Pour en savoir plus sur le processus et les exigences du programme pilote des fournisseurs de médias cloud OEM, remplissez le formulaire de demande.

Créer un fournisseur de services multimédias cloud

Les fournisseurs multimédias cloud sont destinés à être des applications ou des services qui servent de source principale pour la sauvegarde et la récupération de photos et de vidéos depuis le cloud pour les utilisateurs. Si votre application dispose d'une bibliothèque de contenu utile, mais qu'elle n'est généralement pas utilisée comme solution de stockage de photos, envisagez plutôt de créer un fournisseur de documents.

Un seul fournisseur de services cloud actif par profil

Il ne peut y avoir qu'un seul fournisseur de services multimédias cloud actif à la fois pour chaque profil Android. Les utilisateurs peuvent à tout moment supprimer ou modifier l'application de fournisseur de services multimédias cloud qu'ils ont sélectionnée à partir des paramètres du sélecteur de photos.

Par défaut, le sélecteur de photos Android tente de choisir automatiquement un fournisseur cloud.

  • S'il n'y a qu'un seul fournisseur cloud éligible sur l'appareil, l'application sera automatiquement sélectionnée comme fournisseur actuel.
  • Si l'appareil dispose de plusieurs fournisseurs cloud éligibles et que l'un d'entre eux correspond au paramètre par défaut choisi par l'OEM, l'application choisie par l'OEM sera sélectionnée.

  • Si plusieurs fournisseurs cloud éligibles sont installés sur l'appareil et qu'aucun d'entre eux ne correspond au paramètre par défaut choisi par l'OEM, aucune application ne sera sélectionnée.

Créer votre fournisseur de services multimédias cloud

Le schéma suivant illustre la séquence d'événements avant et pendant une session de sélection de photos entre l'application Android, le sélecteur de photos Android, le MediaProvider de l'appareil local et un CloudMediaProvider.

Schéma séquentiel montrant le flux d'un sélecteur de photos vers un fournisseur de services multimédias cloud
Figure 1:Schéma de séquence des événements lors d'une session de sélection de photos
  1. Le système initialise le fournisseur cloud préféré de l'utilisateur et synchronise régulièrement les métadonnées multimédias dans le backend du sélecteur de photos Android.
  2. Lorsqu'une application Android lance le sélecteur de photos, avant d'afficher une grille d'éléments locaux ou cloud fusionnés à l'utilisateur, le sélecteur de photos effectue une synchronisation incrémentielle sensible à la latence avec le fournisseur de services cloud pour s'assurer que les résultats sont aussi à jour que possible. Après avoir reçu une réponse ou lorsque l'échéance est atteinte, la grille du sélecteur de photos affiche désormais toutes les photos accessibles, en combinant celles stockées localement sur votre appareil avec celles synchronisées depuis le cloud.
  3. Pendant que l'utilisateur fait défiler la page, le sélecteur de photos récupère les vignettes multimédias du fournisseur de contenus multimédias cloud pour les afficher dans l'interface utilisateur.
  4. Lorsque l'utilisateur termine la session et que les résultats incluent un élément multimédia cloud, le sélecteur de photos demande des descripteurs de fichier pour le contenu, génère un URI et accorde l'accès au fichier à l'application appelante.
  5. L'application peut maintenant ouvrir l'URI et dispose d'un accès en lecture seule au contenu multimédia. Par défaut, les métadonnées sensibles sont masquées. Le sélecteur de photos exploite le système de fichiers FUSE pour coordonner l'échange de données entre l'application Android et le fournisseur de médias cloud.

Problèmes courants

Voici quelques points importants à prendre en compte lors de la mise en œuvre:

Éviter les fichiers en double

Étant donné que le sélecteur de photos Android ne permet pas d'inspecter l'état du contenu multimédia dans le cloud, CloudMediaProvider doit fournir le MEDIA_STORE_URI dans la ligne du curseur de tout fichier existant à la fois dans le cloud et sur l'appareil local. Sinon, l'utilisateur verra des fichiers en double dans le sélecteur de photos.

Optimiser la taille des images pour l'affichage des aperçus

Il est très important que le fichier renvoyé par onOpenPreview ne corresponde pas à l'image en pleine résolution et qu'il respecte le Size demandé. Une image trop grande entraîne des temps de chargement dans l'interface utilisateur, et une image trop petite peut être pixélisée ou floue en fonction de la taille de l'écran de l'appareil.

Gérer l'orientation correcte

Si les vignettes renvoyées dans onOpenPreview ne contiennent pas leurs données EXIF, elles doivent être renvoyées dans le bon sens pour éviter toute rotation incorrecte dans la grille d'aperçu.

Empêcher les accès non autorisés

Recherchez MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION avant de renvoyer des données à l'appelant depuis ContentProvider. Cela empêchera les applications non autorisées d'accéder aux données cloud.

La classe CloudMediaProvider

Dérivée de android.content.ContentProvider, la classe CloudMediaProvider inclut des méthodes semblables à celles présentées dans l'exemple suivant:

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 classe CloudMediaProviderContract

En plus de la classe d'implémentation CloudMediaProvider principale, le sélecteur de photos Android intègre une classe CloudMediaProviderContract. Cette classe décrit l'interopérabilité entre le sélecteur de photos et le fournisseur de services multimédias cloud. Elle englobe des aspects tels que MediaCollectionInfo pour les opérations de synchronisation, les colonnes Cursor anticipées et les extras 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

La méthode onGetMediaCollectionInfo() permet au système d'exploitation d'évaluer la validité des éléments multimédias cloud mis en cache et de déterminer la synchronisation nécessaire avec le fournisseur de contenus multimédias cloud. En raison du potentiel d'appels fréquents de la part du système d'exploitation, onGetMediaCollectionInfo() est considéré comme essentiel pour les performances. Il est essentiel d'éviter les opérations de longue durée ou les effets secondaires susceptibles d'affecter les performances. Le système d'exploitation met en cache les réponses précédentes de cette méthode et les compare aux réponses suivantes pour déterminer les actions appropriées.

Kotlin

abstract fun onGetMediaCollectionInfo(extras: Bundle): Bundle

Java

@NonNull
public abstract Bundle onGetMediaCollectionInfo(@NonNull Bundle extras);

Le bundle MediaCollectionInfo renvoyé inclut les constantes suivantes:

onQueryMedia

La méthode onQueryMedia() permet de remplir la grille de photos principale du sélecteur de photos dans différentes vues. Ces appels peuvent être sensibles à la latence et peuvent être appelés dans le cadre d'une synchronisation proactive en arrière-plan ou lors de sessions de sélecteur de photos lorsqu'un état de synchronisation complet ou incrémentiel est requis. L'interface utilisateur du sélecteur de photos n'attend pas indéfiniment une réponse pour afficher les résultats et peut expirer le délai d'exécution de ces requêtes à des fins d'interface utilisateur. Le curseur renvoyé tente tout de même d'être traité dans la base de données du sélecteur de photos pour les sessions futures.

Cette méthode renvoie un Cursor représentant tous les éléments multimédias de la collection multimédia éventuellement filtrés selon les extras fournis et triés dans l'ordre chronologique inverse de MediaColumns#DATE_TAKEN_MILLIS (les éléments les plus récents en premier).

Le bundle CloudMediaProviderContract renvoyé inclut les constantes suivantes:

Le fournisseur de médias cloud doit définir CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID dans le Bundle renvoyé. Ne pas définir cette valeur est une erreur et invalide le Cursor renvoyé. Si le fournisseur de médias cloud a géré des filtres dans les extras fournis, il doit ajouter la clé au ContentResolver#EXTRA_HONORED_ARGS dans le Cursor#setExtras renvoyé.

onQuerySuppriméMedia

La méthode onQueryDeletedMedia() permet de s'assurer que les éléments supprimés dans le compte cloud sont correctement supprimés de l'interface utilisateur du sélecteur de photos. En raison de leur sensibilité potentielle à la latence, ces appels peuvent être initiés dans les cas suivants:

  • Synchronisation proactive en arrière-plan
  • Sessions du sélecteur de photos (lorsqu'un état de synchronisation complète ou incrémentielle est requis)

L'interface utilisateur du sélecteur de photos donne la priorité à une expérience utilisateur responsive et n'attend pas indéfiniment une réponse. Pour maintenir des interactions fluides, des délais d'inactivité peuvent survenir. Tout Cursor renvoyé tentera tout de même d'être traité dans la base de données du sélecteur de photos pour les sessions futures.

Cette méthode renvoie un Cursor représentant tous les éléments multimédias supprimés de l'ensemble de la collection multimédia dans la version actuelle du fournisseur, comme renvoyé par onGetMediaCollectionInfo(). Ces éléments peuvent être filtrés à l'aide d'éléments supplémentaires. Le fournisseur de médias cloud doit définir CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID dans l'élément renvoyé Cursor#setExtras. Ne pas définir cette valeur est une erreur et invalider la Cursor. Si le fournisseur a géré des filtres dans les extras fournis, il doit ajouter la clé à ContentResolver#EXTRA_HONORED_ARGS.

onQueryAlbums

La méthode onQueryAlbums() permet de récupérer la liste des albums Cloud disponibles dans le fournisseur cloud, ainsi que les métadonnées associées. Pour en savoir plus, consultez CloudMediaProviderContract.AlbumColumns.

Cette méthode renvoie un Cursor représentant tous les éléments de l'album de la collection multimédia éventuellement filtrés selon les extras fournis et triés dans l'ordre chronologique inverse de AlbumColumns#DATE_TAKEN_MILLIS , les éléments les plus récents en premier. Le fournisseur de médias cloud doit définir CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID dans le Cursor renvoyé. Ne pas définir cette valeur est une erreur et invalide le Cursor renvoyé. Si le fournisseur a géré des filtres dans les extras fournis, il doit ajouter la clé au ContentResolver#EXTRA_HONORED_ARGS dans le Cursor renvoyé.

onOpenMedia

La méthode onOpenMedia() doit renvoyer le support en taille réelle identifié par le mediaId fourni. Si cette méthode se bloque lors du téléchargement de contenu sur l'appareil, vous devez vérifier régulièrement le CancellationSignal fourni pour annuler les requêtes abandonnées.

onOpenPreview

La méthode onOpenPreview() doit renvoyer une vignette de l'élément size fourni pour l'élément associé au mediaId fourni. La vignette doit se trouver dans le CloudMediaProviderContract.MediaColumns#MIME_TYPE d'origine et sa résolution doit être nettement inférieure à celle de l'élément renvoyé par onOpenMedia. Si cette méthode est bloquée lors du téléchargement de contenu sur l'appareil, vous devez vérifier régulièrement le CancellationSignal fourni pour annuler les requêtes abandonnées.

onCreateCloudMediaSurfaceController

La méthode onCreateCloudMediaSurfaceController() doit renvoyer une CloudMediaSurfaceController utilisée pour afficher l'aperçu des éléments multimédias, ou null si l'affichage de l'aperçu n'est pas compatible.

CloudMediaSurfaceController gère l'affichage de l'aperçu des éléments multimédias sur des instances données de Surface. Les méthodes de cette classe sont censées être asynchrones et ne doivent pas être bloquées en effectuant une opération lourde. Une seule instance CloudMediaSurfaceController est responsable de l'affichage de plusieurs éléments multimédias associés à plusieurs surfaces.

CloudMediaSurfaceController est compatible avec la liste de rappels de cycle de vie suivante: