Cloud-Medienanbieter für Android erstellen

Ein Cloud-Medienanbieter stellt der Android-Bildauswahl zusätzliche Cloud-Medieninhalte zur Verfügung. Nutzer können Fotos oder Videos auswählen, die vom Cloud-Medienanbieter bereitgestellt werden, wenn eine App Mediendateien vom Nutzer über ACTION_PICK_IMAGES oder ACTION_GET_CONTENT anfordert. Ein Cloud-Medienanbieter kann auch Informationen zu Alben zur Verfügung stellen, die du in der Android-Bildauswahl durchsuchen kannst.

Vorbereitung

Berücksichtigen Sie die folgenden Punkte, bevor Sie mit der Erstellung Ihres Cloud-Medienanbieters beginnen.

Berechtigung

Android führt ein Pilotprogramm durch, damit von OEMs nominierte Apps zu Cloud-Medienanbietern werden können. Nur von OEMs nominierte Apps sind zur Teilnahme an diesem Programm berechtigt, um ein Cloud-Medienanbieter für Android zu werden. Jeder OEM kann bis zu drei Apps nominieren. Nach der Genehmigung sind diese Apps als Cloud-Medienanbieter über jedes Android-GMD-Gerät zugänglich, auf dem sie installiert sind.

Android führt eine serverseitige Liste aller berechtigten Cloud-Anbieter. Jeder OEM kann mithilfe eines konfigurierbaren Overlays einen Standard-Cloud-Anbieter auswählen. Nominierte Apps müssen alle technischen Anforderungen erfüllen und alle Qualitätstests bestehen. Wenn Sie mehr über den Ablauf und die Anforderungen des Pilotprogramms für OEM-Cloud-Medienanbieter erfahren möchten, füllen Sie das Anfrageformular aus.

Entscheiden, ob Sie einen Cloud-Medienanbieter erstellen müssen

Cloud-Medienanbieter sind Apps oder Dienste, die Nutzern als Hauptquelle zum Sichern und Abrufen von Fotos und Videos aus der Cloud dienen. Wenn Ihre Anwendung eine Bibliothek nützlicher Inhalte hat, diese aber normalerweise nicht als Fotospeicherlösung verwendet wird, sollten Sie stattdessen einen Dokumentanbieter erstellen.

Ein aktiver Cloud-Anbieter pro Profil

Für jedes Android-Profil kann jeweils nur ein aktiver Cloud-Medienanbieter vorhanden sein. Nutzer können die App des ausgewählten Cloud-Medienanbieters jederzeit in den Einstellungen für die Bildauswahl entfernen oder ändern.

Standardmäßig versucht die Android-Bildauswahl automatisch einen Cloud-Anbieter auszuwählen.

  • Wenn sich auf dem Gerät nur ein zulässiger Cloud-Anbieter befindet, wird diese App automatisch als aktueller Anbieter ausgewählt.
  • Wenn sich auf dem Gerät mehr als ein berechtigter Cloud-Anbieter befindet und einer davon mit dem vom OEM gewählten Standard übereinstimmt, wird die vom OEM ausgewählte App ausgewählt.

  • Wenn sich auf dem Gerät mehr als ein berechtigter Cloud-Anbieter befindet und keiner von ihnen dem vom OEM gewählten Standard entspricht, wird keine App ausgewählt.

Cloud-Medienanbieter erstellen

Das folgende Diagramm zeigt die Abfolge der Ereignisse vor und während einer Fotoauswahl zwischen der Android-App, der Android-Bildauswahl, dem MediaProvider des lokalen Geräts und einem CloudMediaProvider.

Sequenzdiagramm, das den Fluss von der Bildauswahl zu einem Cloud-Medienanbieter zeigt
Abbildung 1: Diagramm mit Ereignissequenz während einer Fotoauswahl
  1. Das System initialisiert den bevorzugten Cloud-Anbieter des Nutzers und synchronisiert regelmäßig Medienmetadaten mit dem Bildauswahl-Back-End von Android.
  2. Wenn eine Android-App die Bildauswahl startet, bevor dem Nutzer ein zusammengeführtes Raster für lokale oder cloudbasierte Elemente angezeigt wird, führt die Bildauswahl eine latenzempfindliche inkrementelle Synchronisierung mit dem Cloud-Anbieter durch, um sicherzustellen, dass die Ergebnisse so aktuell wie möglich sind. Nachdem du eine Antwort erhalten hast oder die Frist abgelaufen ist, werden in der Bildauswahl jetzt alle zugänglichen Fotos angezeigt. Die lokal auf deinem Gerät gespeicherten Fotos werden mit den über die Cloud synchronisierten Fotos kombiniert.
  3. Während der Nutzer scrollt, ruft die Bildauswahl Medien-Miniaturansichten vom Cloud-Medienanbieter ab, um sie auf der UI anzuzeigen.
  4. Wenn der Nutzer die Sitzung beendet und die Ergebnisse ein Cloud-Medienelement enthalten, fordert die Bildauswahl Dateideskriptoren für den Inhalt an, generiert einen URI und gewährt der aufrufenden Anwendung Zugriff auf die Datei.
  5. Die App kann jetzt den URI öffnen und hat Lesezugriff auf die Medieninhalte. Standardmäßig werden sensible Metadaten entfernt. Die Bildauswahl nutzt das Dateisystem FUSE, um den Datenaustausch zwischen der Android-App und dem Cloud-Medienanbieter zu koordinieren.

Häufige Fragen und Probleme

Hier sind einige wichtige Überlegungen, die Sie bei der Berücksichtigung Ihrer Implementierung berücksichtigen sollten:

Doppelte Dateien vermeiden

Da die Android-Bildauswahl keine Möglichkeit hat, den Cloud-Medienstatus zu prüfen, muss CloudMediaProvider das MEDIA_STORE_URI in der Cursorzeile jeder Datei bereitstellen, die sowohl in der Cloud als auch auf dem lokalen Gerät vorhanden ist. Andernfalls sieht der Nutzer doppelte Dateien in der Bildauswahl.

Bildgrößen für die Vorschauanzeige optimieren

Es ist sehr wichtig, dass die von onOpenPreview zurückgegebene Datei nicht das Bild in voller Auflösung hat und dem angeforderten Size entspricht. Ein zu großes Bild führt zu Ladezeiten in der UI und ein zu klein gestaltetes Bild kann je nach Bildschirmgröße des Geräts verpixelt oder unscharf erscheinen.

Richtige Ausrichtung handhaben

Wenn in onOpenPreview zurückgegebene Miniaturansichten keine EXIF-Daten enthalten, sollten sie mit der richtigen Ausrichtung zurückgegeben werden, um zu vermeiden, dass Miniaturansichten im Vorschauraster falsch gedreht werden.

Unbefugten Zugriff verhindern

Prüfen Sie die MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION, bevor Sie Daten aus dem ContentProvider an den Aufrufer zurückgeben. Dadurch wird verhindert, dass nicht autorisierte Anwendungen auf Cloud-Daten zugreifen.

Klasse „CloudMediaProvider“

Die von android.content.ContentProvider abgeleitete Klasse CloudMediaProvider enthält Methoden wie die im folgenden Beispiel:

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

Klasse „CloudMediaProviderContract“

Zusätzlich zur primären CloudMediaProvider-Implementierungsklasse enthält die Android-Bildauswahl eine CloudMediaProviderContract-Klasse. Diese Klasse beschreibt die Interoperabilität zwischen der Bildauswahl und dem Cloud-Medienanbieter und umfasst Aspekte wie MediaCollectionInfo für Synchronisierungsvorgänge, erwartete Cursor-Spalten und Bundle-Extras.

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

Das Betriebssystem verwendet die Methode onGetMediaCollectionInfo(), um die Gültigkeit der im Cache gespeicherten Cloud-Medienelemente zu bewerten und die erforderliche Synchronisierung mit dem Cloud-Medienanbieter zu ermitteln. Aufgrund der Möglichkeit häufiger Aufrufe durch das Betriebssystem gilt onGetMediaCollectionInfo() als leistungskritisch. Lang andauernde Vorgänge oder Nebeneffekte, die sich negativ auf die Leistung auswirken könnten, sollten unbedingt vermieden werden. Das Betriebssystem speichert frühere Antworten dieser Methode im Cache und vergleicht sie mit nachfolgenden Antworten, um die entsprechenden Aktionen zu bestimmen.

Kotlin

abstract fun onGetMediaCollectionInfo(extras: Bundle): Bundle

Java

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

Das zurückgegebene MediaCollectionInfo-Bundle enthält die folgenden Konstanten:

onQueryMedia

Die Methode onQueryMedia() wird verwendet, um das Hauptraster in der Bildauswahl in verschiedenen Ansichten zu füllen. Diese Aufrufe können latenzempfindlich sein und können im Rahmen einer proaktiven Hintergrundsynchronisierung oder während Bildauswahlsitzungen aufgerufen werden, wenn ein vollständiger oder inkrementeller Synchronisierungsstatus erforderlich ist. Die Benutzeroberfläche der Bildauswahl wartet nicht unbegrenzt auf eine Antwort, um Ergebnisse anzuzeigen, und kann aus Gründen der Benutzeroberfläche eine Zeitüberschreitung für diese Anfragen verursachen. Der zurückgegebene Cursor wird weiterhin für zukünftige Sitzungen in der Datenbank der Bildauswahl verarbeitet.

Diese Methode gibt eine Cursor zurück, die alle Medienelemente in der Mediensammlung darstellt, die optional nach den bereitgestellten Extras gefiltert und in umgekehrter chronologischer Reihenfolge von MediaColumns#DATE_TAKEN_MILLIS (neueste Elemente zuerst) sortiert sind.

Das zurückgegebene CloudMediaProviderContract-Bundle enthält die folgenden Konstanten:

Der Cloud-Medienanbieter muss CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID als Teil der zurückgegebenen Bundle festlegen. Wenn Sie dies nicht festlegen, liegt ein Fehler vor und die zurückgegebene Cursor wird ungültig. Wenn der Cloud-Medienanbieter Filter in den bereitgestellten Extras verarbeitet hat, muss er den Schlüssel zu ContentResolver#EXTRA_HONORED_ARGS als Teil des zurückgegebenen Cursor#setExtras hinzufügen.

onQueryDeletedMedia

Mit der Methode onQueryDeletedMedia() wird sichergestellt, dass gelöschte Elemente im Cloudkonto korrekt aus der Benutzeroberfläche der Bildauswahl entfernt werden. Aufgrund ihrer potenziellen Latenzempfindlichkeit können diese Aufrufe als Teil von Folgendem initiiert werden:

  • Proaktive Synchronisierung im Hintergrund
  • Bildauswahl-Sitzungen (wenn eine vollständige oder inkrementelle Synchronisierung erforderlich ist)

In der Benutzeroberfläche der Bildauswahl ist eine reaktionsschnelle Nutzererfahrung priorisiert und sie wartet nicht unbegrenzt auf eine Antwort. Um eine reibungslose Interaktion zu gewährleisten, kann es zu Zeitüberschreitungen kommen. Alle zurückgegebenen Cursor werden weiterhin für zukünftige Sitzungen in die Datenbank der Bildauswahl verarbeitet.

Diese Methode gibt ein Cursor zurück, das alle gelöschten Medienelemente in der gesamten Mediensammlung innerhalb der aktuellen Anbieterversion darstellt, wie von onGetMediaCollectionInfo() zurückgegeben. Diese Elemente können optional nach Extras gefiltert werden. Der Cloud-Medienanbieter muss CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID als Teil des zurückgegebenen Cursor#setExtras festlegen. Wenn dies nicht festgelegt wird, ist dies ein Fehler und Cursor wird ungültig. Wenn der Anbieter Filter in den bereitgestellten Extras verarbeitet hat, muss der Schlüssel zu ContentResolver#EXTRA_HONORED_ARGS hinzugefügt werden.

onQueryAlbums

Mit der Methode onQueryAlbums() wird eine Liste der Cloud-Alben, die beim Cloud-Anbieter verfügbar sind, und der zugehörigen Metadaten abgerufen. Weitere Informationen finden Sie unter CloudMediaProviderContract.AlbumColumns.

Diese Methode gibt ein Cursor zurück, das alle Albumelemente in der Mediensammlung darstellt, optional gefiltert nach den bereitgestellten Extras und in umgekehrter chronologischer Reihenfolge nach AlbumColumns#DATE_TAKEN_MILLIS sortiert. Die neuesten Elemente stehen zuerst. Der Cloud-Medienanbieter muss CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID als Teil des zurückgegebenen Cursor festlegen. Wenn Sie dies nicht festlegen, liegt ein Fehler vor und die zurückgegebene Cursor wird ungültig. Wenn der Anbieter Filter in den bereitgestellten Extras verarbeitet hat, muss er den Schlüssel als Teil der zurückgegebenen Cursor zu ContentResolver#EXTRA_HONORED_ARGS hinzufügen.

aufOpenMedia

Die Methode onOpenMedia() sollte das Medium in voller Größe zurückgeben, das durch die angegebene mediaId identifiziert wird. Wenn diese Methode beim Herunterladen von Inhalten auf das Gerät blockiert wird, sollten Sie den bereitgestellten CancellationSignal regelmäßig prüfen, um abgebrochene Anfragen abzubrechen.

onOpenPreview

Die Methode onOpenPreview() sollte eine Miniaturansicht des angegebenen size für das Element mit der angegebenen mediaId zurückgeben. Die Miniaturansicht sollte im ursprünglichen CloudMediaProviderContract.MediaColumns#MIME_TYPE vorliegen und hat wahrscheinlich eine viel geringere Auflösung als das von onOpenMedia zurückgegebene Element. Wenn diese Methode beim Herunterladen von Inhalten auf das Gerät blockiert wird, prüfen Sie regelmäßig die angegebene CancellationSignal, um abgebrochene Anfragen abzubrechen.

onCreateCloudMediaSurfaceController

Über die Methode onCreateCloudMediaSurfaceController() sollte ein CloudMediaSurfaceController zurückgegeben werden, das für das Rendering der Vorschau von Medienelementen verwendet wird, oder null, wenn das Vorschau-Rendering nicht unterstützt wird.

CloudMediaSurfaceController verwaltet das Rendern der Vorschau von Medienelementen für bestimmte Instanzen von Surface. Die Methoden dieser Klasse sind asynchron und sollten nicht durch aufwendige Vorgänge blockiert werden. Eine einzelne CloudMediaSurfaceController-Instanz ist für das Rendern mehrerer Medienelemente verantwortlich, die mit mehreren Oberflächen verknüpft sind.

CloudMediaSurfaceController unterstützt die folgende Liste von Lebenszyklus-Callbacks: