Creare un fornitore di contenuti multimediali cloud per Android

Un fornitore di contenuti multimediali sul cloud fornisce contenuti multimediali cloud aggiuntivi al selettore di foto Android. Gli utenti possono selezionare le foto o i video forniti dal provider di contenuti multimediali sul cloud quando un'app utilizza ACTION_PICK_IMAGES o ACTION_GET_CONTENT per richiedere file multimediali all'utente. Un provider di contenuti multimediali cloud può anche fornire informazioni sugli album, che possono essere sfogliate nel selettore di foto Android.

Prima di iniziare

Prendi in considerazione i seguenti elementi prima di iniziare a creare il tuo cloud media provider.

Idoneità

Android sta eseguendo un programma pilota per consentire alle app nominate OEM di diventare fornitori di media cloud. Al momento solo le app nominate dagli OEM sono idonee a partecipare a questo programma per diventare un cloud media provider per Android. Ogni OEM può nominare fino a tre app. Una volta approvate, queste app diventano accessibili come provider di media cloud su qualsiasi dispositivo GMS con tecnologia Android su cui sono installate.

Android gestisce un elenco lato server di tutti i cloud provider idonei. Ogni OEM può scegliere un provider cloud predefinito utilizzando un overlay configurabile. Le app classificate devono soddisfare tutti i requisiti tecnici e superare tutti i test di qualità. Per scoprire di più sul processo e sui requisiti del programma pilota per i fornitori di contenuti multimediali cloud OEM, compila il modulo di richiesta.

Decidi se è necessario creare un fornitore di contenuti multimediali cloud

I fornitori di contenuti multimediali cloud sono app o servizi che fungono da fonte principale per gli utenti per il backup e il recupero di foto e video dal cloud. Se la tua app ha una raccolta di contenuti utili, che però in genere non viene utilizzata come soluzione di archiviazione delle foto, ti consigliamo di creare un fornitore di documenti.

Un cloud provider attivo per profilo

Può esserci al massimo un fornitore di contenuti multimediali cloud attivo alla volta per ogni profilo Android. Gli utenti possono rimuovere o modificare l'app del provider di contenuti multimediali selezionata in qualsiasi momento dalle impostazioni del selettore di foto.

Per impostazione predefinita, il selettore di foto Android tenterà di scegliere automaticamente un provider cloud.

  • Se sul dispositivo è presente un solo cloud provider idoneo, quell'app verrà selezionata automaticamente come provider attuale.
  • Se sul dispositivo sono presenti più cloud provider idonei e uno di questi corrisponde all'impostazione predefinita scelta dall'OEM, verrà selezionata l'app scelta dall'OEM.

  • Se sul dispositivo sono presenti più cloud provider idonei e nessuno corrisponde all'OEM scelto per impostazione predefinita, non verrà selezionata alcuna app.

Crea il tuo fornitore di contenuti multimediali cloud

Il seguente diagramma illustra la sequenza di eventi prima e durante una sessione di selezione delle foto tra l'app per Android, il selettore di foto Android, il MediaProvider del dispositivo locale e un CloudMediaProvider.

Diagramma di sequenza che mostra il flusso dal selettore di foto a un fornitore di contenuti multimediali sul cloud
Figura 1: diagramma della sequenza di eventi durante una sessione di selezione delle foto.
  1. Il sistema inizializza il provider cloud preferito dell'utente e sincronizza periodicamente i metadati multimediali nel backend del selettore di foto Android.
  2. Quando un'app per Android avvia il selettore di foto, prima di mostrare all'utente una griglia di elementi locali o cloud unita, il selettore di foto esegue una sincronizzazione incrementale sensibile alla latenza con il provider cloud per garantire che i risultati siano il più aggiornati possibile. Dopo aver ricevuto una risposta o al raggiungimento della scadenza, la griglia del selettore di foto ora mostra tutte le foto accessibili, combinando quelle memorizzate localmente sul tuo dispositivo con quelle sincronizzate dal cloud.
  3. Mentre l'utente scorre, il selettore di foto recupera le miniature dei contenuti multimediali dal fornitore di contenuti multimediali sul cloud per visualizzarle nell'interfaccia utente.
  4. Quando l'utente completa la sessione e i risultati includono un elemento multimediale cloud, il selettore di foto richiede i descrittori dei file per i contenuti, genera un URI e concede l'accesso al file all'applicazione chiamante.
  5. L'app ora è in grado di aprire l'URI e ha accesso di sola lettura ai contenuti multimediali. Per impostazione predefinita, i metadati sensibili sono oscurati. Il selettore di foto sfrutta il file system FUSE per coordinare lo scambio di dati tra l'app per Android e il fornitore di contenuti multimediali sul cloud.

Problemi comuni

Ecco alcune considerazioni importanti da tenere a mente al momento di valutare la tua implementazione:

Evita file duplicati

Poiché il selettore di foto Android non ha modo di esaminare lo stato dei contenuti multimediali del cloud, l'oggetto CloudMediaProvider deve indicare il valore MEDIA_STORE_URI nella riga del cursore di qualsiasi file esistente sia nel cloud sia sul dispositivo locale, altrimenti l'utente vedrà i file duplicati nel selettore di foto.

Ottimizza le dimensioni delle immagini per la visualizzazione di anteprima

È molto importante che il file restituito da onOpenPreview non sia l'immagine alla massima risoluzione e che sia conforme al Size richiesto. Un'immagine troppo grande comporta tempi di caricamento nell'interfaccia utente e un'immagine troppo piccola potrebbe risultare pixelata o sfocata a seconda delle dimensioni dello schermo del dispositivo.

Gestisci l'orientamento corretto

Se le miniature restituite in onOpenPreview non contengono i relativi dati EXIF, devono essere restituite nell'orientamento corretto per evitare che vengano ruotate in modo errato nella griglia di anteprima.

Impedire l'accesso non autorizzato

Controlla il campo MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION prima di restituire dati al chiamante dalla classe ContentProvider. In questo modo le app non autorizzate non potranno accedere ai dati cloud.

La classe CloudMediaProvider

Derivata da android.content.ContentProvider, la classe CloudMediaProvider include metodi come quelli mostrati nell'esempio seguente:

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

Oltre alla classe di implementazione CloudMediaProvider principale, il selettore di foto Android incorpora una classe CloudMediaProviderContract. Questa classe illustra l'interoperabilità tra il selettore di foto e il provider di media cloud, e riguarda aspetti come MediaCollectionInfo per le operazioni di sincronizzazione, le colonne Cursor previste e Bundle elementi extra.

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";
}

suGetMediaCollectionInfo

Il metodo onGetMediaCollectionInfo() viene utilizzato dal sistema operativo per valutare la validità degli elementi multimediali cloud memorizzati nella cache e determinare la necessaria sincronizzazione con il provider di contenuti multimediali sul cloud. A causa del potenziale per chiamate frequenti da parte del sistema operativo, onGetMediaCollectionInfo() è considerato un fattore critico per le prestazioni ed è fondamentale evitare operazioni a lunga esecuzione o effetti collaterali che potrebbero influire negativamente sulle prestazioni. Il sistema operativo memorizza nella cache le risposte precedenti di questo metodo e le confronta con le risposte successive per determinare le azioni appropriate.

Kotlin

abstract fun onGetMediaCollectionInfo(extras: Bundle): Bundle

Java

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

Il bundle MediaCollectionInfo restituito include le seguenti costanti:

onQueryMedia

Il metodo onQueryMedia() viene utilizzato per completare la griglia principale delle foto nel selettore di foto in diverse visualizzazioni. Queste chiamate potrebbero essere sensibili alla latenza e possono essere chiamate nell'ambito di una sincronizzazione proattiva in background o durante sessioni di selettore di foto quando è richiesto uno stato di sincronizzazione completo o incrementale. L'interfaccia utente del selettore di foto non aspetta a tempo indeterminato una risposta per visualizzare i risultati e potrebbe disattivare queste richieste ai fini dell'interfaccia utente. Il cursore restituito tenterà comunque di essere elaborato nel database del selettore di foto per le sessioni future.

Questo metodo restituisce un elemento Cursor che rappresenta tutti gli elementi multimediali nella raccolta multimediale, facoltativamente filtrato in base agli extra forniti e ordinati in ordine cronologico inverso di MediaColumns#DATE_TAKEN_MILLIS (gli elementi più recenti per primi).

Il bundle CloudMediaProviderContract restituito include le seguenti costanti:

Il fornitore di contenuti multimediali cloud deve impostare CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID come parte dell'elemento Bundle restituito. La mancata impostazione di questo parametro rappresenta un errore e rende non valido l'oggetto Cursor restituito. Se il fornitore di contenuti multimediali cloud ha gestito eventuali filtri negli extra forniti, deve aggiungere la chiave a ContentResolver#EXTRA_HONORED_ARGS come parte dell'elemento Cursor#setExtras restituito.

onQuerydeletedMedia

Il metodo onQueryDeletedMedia() viene utilizzato per garantire che gli elementi eliminati nell'account cloud vengano rimossi correttamente dall'interfaccia utente del selettore di foto. Data la loro potenziale sensibilità alla latenza, queste chiamate potrebbero essere avviate nell'ambito di:

  • Sincronizzazione proattiva in background
  • Sessioni del selettore di foto (quando è necessario uno stato di sincronizzazione completo o incrementale)

L'interfaccia utente del selettore di foto dà la priorità a un'esperienza utente reattiva e non attende a tempo indeterminato una risposta. Per mantenere interazioni fluide, potrebbero verificarsi timeout. Qualsiasi Cursor restituito tenterà comunque di essere elaborato nel database del selettore di foto per le sessioni future.

Questo metodo restituisce un Cursor che rappresenta tutti gli elementi multimediali eliminati nell'intera raccolta multimediale all'interno della versione corrente del provider, come restituito da onGetMediaCollectionInfo(). Questi articoli possono essere facoltativamente filtrati in base a extra. Il fornitore di contenuti multimediali cloud deve impostare CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID come parte del valore Cursor#setExtras Non impostato restituito. Questo è un errore e rende Cursor non valido. Se il provider ha gestito eventuali filtri negli extra forniti, deve aggiungere la chiave a ContentResolver#EXTRA_HONORED_ARGS.

onQueryAlbum

Il metodo onQueryAlbums() viene utilizzato per recuperare un elenco di album Cloud disponibili nel cloud provider e i relativi metadati associati. Per ulteriori dettagli, visita la pagina CloudMediaProviderContract.AlbumColumns.

Questo metodo restituisce un elemento Cursor che rappresenta tutti gli elementi dell'album nella raccolta multimediale, facoltativamente filtrati in base agli extra forniti e ordinati in ordine cronologico inverso di AlbumColumns#DATE_TAKEN_MILLIS, gli elementi più recenti per primi. Il fornitore di contenuti multimediali cloud deve impostare CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID come parte del Cursor restituito. La mancata impostazione di questo parametro rappresenta un errore e rende non valido l'oggetto Cursor restituito. Se il provider ha gestito eventuali filtri negli extra forniti, deve aggiungere la chiave a ContentResolver#EXTRA_HONORED_ARGS come parte del valore Cursor restituito.

suOpenMedia

Il metodo onOpenMedia() deve restituire i contenuti multimediali a grandezza originale identificati dal valore mediaId fornito. Se questo metodo blocca il download dei contenuti sul dispositivo, devi controllare periodicamente il CancellationSignal fornito per interrompere le richieste abbandonate.

suOpenPreview

Il metodo onOpenPreview() deve restituire una miniatura del valore size fornito per l'elemento del mediaId fornito. La miniatura deve essere nel formato CloudMediaProviderContract.MediaColumns#MIME_TYPE originale e dovrebbe avere una risoluzione molto inferiore rispetto all'elemento restituito da onOpenMedia. Se questo metodo è bloccato durante il download dei contenuti sul dispositivo, devi controllare periodicamente il CancellationSignal fornito per interrompere le richieste abbandonate.

onCreateCloudMediaSurfaceController

Il metodo onCreateCloudMediaSurfaceController() deve restituire un valore CloudMediaSurfaceController utilizzato per il rendering dell'anteprima degli elementi multimediali oppure null se il rendering dell'anteprima non è supportato.

L'elemento CloudMediaSurfaceController gestisce il rendering dell'anteprima degli elementi multimediali su determinate istanze di Surface. I metodi di questa classe sono asincroni e non devono bloccare l'esecuzione di operazioni impegnative. Una singola istanza CloudMediaSurfaceController è responsabile del rendering di più elementi multimediali associati a più piattaforme.

CloudMediaSurfaceController supporta il seguente elenco di callback del ciclo di vita: