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 mit ACTION_PICK_IMAGES oder ACTION_GET_CONTENT Mediendateien vom Nutzer anfordert. Ein Cloud-Medien anbieter kann auch Informationen zu Alben bereitstellen, die in der Android-Bildauswahl durchsucht werden können.

Hinweis

Beachten Sie die folgenden Punkte, bevor Sie mit der Entwicklung Ihres Cloud-Medienanbieters beginnen.

Voraussetzungen

Android führt ein Pilotprogramm durch, mit dem von OEMs nominierte Apps zu Cloud-Medienanbietern werden können. Derzeit können nur von OEMs nominierte Apps an diesem Programm teilnehmen, um Cloud-Medienanbieter für Android zu werden. Jeder OEM kann bis zu drei Apps nominieren. Nach der Genehmigung sind diese Apps auf allen Android-Geräten mit GMS, auf denen sie installiert sind, als Cloud-Medienanbieter verfügbar.

Android führt eine serverseitige Liste aller infrage kommenden Cloud-Anbieter. Jeder OEM kann über ein konfigurierbares Overlay 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 als primäre Quelle für Nutzer zum Sichern und Abrufen von Fotos und Videos aus der Cloud dienen. Wenn Ihre App eine Bibliothek mit nützlichen Inhalten enthält, 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 ausgewählte Cloud-Medienanbieter-App jederzeit in den Einstellungen der Bildauswahl entfernen oder ändern.

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

  • Wenn auf dem Gerät nur ein infrage kommender Cloud-Anbieter vorhanden ist, wird diese App automatisch als aktueller Anbieter ausgewählt.
  • Wenn auf dem Gerät mehrere infrage kommende Cloud-Anbieter vorhanden sind und einer davon dem vom OEM ausgewählten Standard entspricht, wird die vom OEM ausgewählte App ausgewählt.

  • Wenn auf dem Gerät mehrere infrage kommende Cloud-Anbieter vorhanden sind und keiner davon dem vom OEM ausgewählten Standard entspricht, wird keine App ausgewählt.

Cloud-Medienanbieter erstellen

Das folgende Diagramm veranschaulicht die Ereignissequenz vor und während einer Fotositzung zwischen der Android-App, der Android-Bildauswahl, dem MediaProvider des lokalen Geräts und einem CloudMediaProvider.

Sequenzdiagramm, das den Ablauf von der Bildauswahl zu einem Cloud-Media-Anbieter zeigt
Abbildung 1: Sequenzdiagramm der Ereignisse während einer Fotoauswahlsitzung.
  1. Das System initialisiert den bevorzugten Cloud-Anbieter des Nutzers und synchronisiert regelmäßig Metadaten von Medien mit dem Backend der Android-Bildauswahl.
  2. Wenn eine Android-App die Bildauswahl startet, führt die Bildauswahl vor dem Anzeigen eines zusammengeführten Rasters mit lokalen oder Cloud-Elementen für den Nutzer eine latenzempfindliche inkrementelle Synchronisierung mit dem Cloud-Anbieter durch, um sicherzustellen, dass die Ergebnisse so aktuell wie möglich sind. Nachdem eine Antwort eingegangen ist oder die Frist abgelaufen ist, werden im Raster der Bildauswahl alle zugänglichen Fotos angezeigt. Dabei werden die lokal auf dem Gerät gespeicherten Fotos mit den aus der Cloud synchronisierten Fotos kombiniert.
  3. Während der Nutzer scrollt, ruft die Bildauswahl Miniaturansichten von Medien vom Cloud-Medienanbieter ab, um sie in der Benutzeroberfläche 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 schreibgeschützten Zugriff auf die Medieninhalte. Standardmäßig werden vertrauliche Metadaten entfernt. Die Bildauswahl nutzt das FUSE-Dateisystem, um den Datenaustausch zwischen der Android-App und dem Cloud-Medienanbieter zu koordinieren.

Häufige Fragen und Probleme

Hier sind einige wichtige Punkte, die Sie bei der Implementierung beachten sollten:

Doppelte Dateien vermeiden

Da die Android-Bildauswahl den Status von Cloud-Medien nicht prüfen kann, muss der CloudMediaProvider den MEDIA_STORE_URI in der Cursorzeile jeder Datei angeben, die sowohl in der Cloud als auch auf dem lokalen Gerät vorhanden ist. Andernfalls werden dem Nutzer in der Bildauswahl doppelte Dateien angezeigt.

Bildgrößen für die Vorschau optimieren

Es ist sehr wichtig, dass die von onOpenPreview zurückgegebene Datei nicht das Bild in voller Auflösung ist und die angeforderte Size eingehalten wird. Ein zu großes Bild führt zu Ladezeiten auf der Benutzeroberfläche und ein zu kleines Bild kann je nach Bildschirmgröße des Geräts verpixelt oder unscharf sein.

Korrekte Ausrichtung verarbeiten

Wenn die in onOpenPreview zurückgegebenen Miniaturansichten keine EXIF-Daten enthalten, sollten sie in der richtigen Ausrichtung zurückgegeben werden, damit sie im Vorschauraster nicht falsch gedreht werden.

Unbefugten Zugriff verhindern

Prüfen Sie vor der Rückgabe von Daten an den Aufrufer vom ContentProvider aus, ob die MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION vorhanden ist. So wird verhindert, dass nicht autorisierte Apps auf Cloud-Daten zugreifen.

Die Klasse „CloudMediaProvider“

Die Klasse CloudMediaProvider wird von android.content.ContentProvider abgeleitet und 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);
}

Die Klasse „CloudMediaProviderContract“

Neben der primären CloudMediaProvider Implementierungsklasse enthält die Android-Bildauswahl auch eine CloudMediaProviderContract Klasse. Diese Klasse beschreibt die Interoperabilität zwischen der Bildauswahl und dem Cloud-Medienanbieter, einschließlich Aspekten 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

Die onGetMediaCollectionInfo() Methode wird vom Betriebssystem verwendet, um die Gültigkeit der im Cache gespeicherten Cloud-Medienelemente zu bewerten und die erforderliche Synchronisierung mit dem Cloud-Medienanbieter zu bestimmen. Da diese Methode möglicherweise häufig vom Betriebssystem aufgerufen wird, ist onGetMediaCollectionInfo() leistungskritisch. Es ist wichtig, langwierige Vorgänge oder Nebeneffekte zu vermeiden, die sich negativ auf die Leistung auswirken könnten. Das Betriebssystem speichert frühere Antworten von dieser Methode im Cache und vergleicht sie mit nachfolgenden Antworten, um die entsprechenden Maßnahmen 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 onQueryMedia() Methode wird verwendet, um das Hauptfotoraster in der Bildauswahl in verschiedenen Ansichten zu füllen. Diese Aufrufe können latenzempfindlich sein und im Rahmen einer proaktiven Hintergrundsynchronisierung oder während Sitzungen der Bildauswahl erfolgen, wenn ein vollständiger oder inkrementeller Synchronisierungsstatus erforderlich ist. Die Benutzeroberfläche der Bildauswahl wartet nicht unbegrenzt auf eine Antwort, um Ergebnisse anzuzeigen, und es kann zu Zeitüberschreitungen bei diesen Anfragen kommen. Der zurückgegebene Cursor wird weiterhin versucht, für zukünftige Sitzungen in die Datenbank der Bildauswahl verarbeitet zu werden.

Diese Methode gibt einen Cursor zurück, der alle Medienelemente in der Mediensammlung darstellt. Optional können die Elemente nach den angegebenen Extras gefiltert und in umgekehrt chronologischer Reihenfolge von MediaColumns#DATE_TAKEN_MILLIS sortiert werden (die neuesten Elemente zuerst).

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

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

onQueryDeletedMedia

Die onQueryDeletedMedia() Methode wird verwendet, um sicherzustellen, dass gelöschte Elemente im Cloud-Konto korrekt aus der Benutzeroberfläche der Bildauswahl entfernt werden. Aufgrund ihrer potenziellen Latenzempfindlichkeit können diese Aufrufe im Rahmen von Folgendem initiiert werden:

  • Proaktive Hintergrundsynchronisierung
  • Sitzungen der Bildauswahl (wenn ein vollständiger oder inkrementeller Synchronisierungsstatus erforderlich ist)

Die Benutzeroberfläche der Bildauswahl priorisiert eine reaktionsschnelle Nutzererfahrung und wartet nicht unbegrenzt auf eine Antwort. Um reibungslose Interaktionen zu gewährleisten, kann es zu Zeitüberschreitungen kommen. Jeder zurückgegebene Cursor wird weiterhin versucht, für zukünftige Sitzungen in die Datenbank der Bildauswahl verarbeitet zu werden.

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

onQueryAlbums

Die Methode onQueryAlbums() wird verwendet, um eine Liste der in der Cloud verfügbaren Cloud-Alben und die zugehörigen Metadaten abzurufen. Weitere Informationen finden Sie unter CloudMediaProviderContract.AlbumColumns.

Diese Methode gibt einen Cursor zurück , der alle Album-Elemente in der Mediensammlung darstellt. Optional können die Elemente nach den angegebenen Extras gefiltert und in umgekehrt chronologischer Reihenfolge von AlbumColumns#DATE_TAKEN_MILLIS sortiert werden (die neuesten Elemente zuerst). Der Cloud-Medienanbieter muss CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID als Teil des zurückgegebenen Cursor festlegen. Wenn dies nicht festgelegt wird, ist das ein Fehler und der zurückgegebene Cursor ist ungültig. Wenn der Anbieter Filter in den angegebenen Extras verarbeitet hat, muss er den Schlüssel zu ContentResolver#EXTRA_HONORED_ARGS als Teil des zurückgegebenen Cursor hinzufügen.

onOpenMedia

Die onOpenMedia() Methode sollte die vollständige Mediendatei zurückgeben, die durch die angegebene mediaId identifiziert wird. Wenn diese Methode blockiert wird, während Inhalte auf das Gerät heruntergeladen werden, sollten Sie regelmäßig das angegebene CancellationSignal prüfen, um abgebrochene Anfragen abzubrechen.

onOpenPreview

Die onOpenPreview() Methode sollte eine Miniaturansicht der angegebenen size für das Element der angegebenen mediaId zurückgeben. Die Miniaturansicht sollte im ursprünglichen CloudMediaProviderContract.MediaColumns#MIME_TYPE vorliegen und eine viel geringere Auflösung als das von onOpenMedia zurückgegebene Element haben. Wenn diese Methode blockiert wird, während Inhalte auf das Gerät heruntergeladen werden, sollten Sie regelmäßig das angegebene CancellationSignal prüfen, um abgebrochene Anfragen abzubrechen.

onCreateCloudMediaSurfaceController

Die onCreateCloudMediaSurfaceController() Methode sollte einen CloudMediaSurfaceController zurückgeben, der zum Rendern der Vorschau von Medienelementen verwendet wird, oder null wenn das Rendern der Vorschau nicht unterstützt wird.

Der CloudMediaSurfaceController verwaltet das Rendern der Vorschau von Medienelementen auf bestimmten Instanzen von Surface. Die Methoden dieser Klasse sind asynchron und sollten nicht durch die Ausführung aufwendiger Vorgänge blockiert werden. Eine einzelne CloudMediaSurfaceController-Instanz ist für das Rendern mehrerer Medienelemente verantwortlich, die mehreren Oberflächen zugeordnet sind.

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