Tworzenie dostawcy multimediów w chmurze na Androida

Dostawca multimediów w chmurze udostępnia dodatkowe treści multimedialne w chmurze w selektorze zdjęć na Androidzie . Gdy aplikacja używa ACTION_PICK_IMAGES lub ACTION_GET_CONTENT, aby poprosić użytkownika o pliki multimedialne, może on wybrać zdjęcia lub filmy dostarczone przez dostawcę multimediów w chmurze. Dostawca multimediów w chmurze może też podawać informacje o albumach, które można przeglądać w selektorze zdjęć na Androidzie.

Zanim zaczniesz

Zanim zaczniesz tworzyć dostawcę multimediów w chmurze, weź pod uwagę te kwestie.

Dostępność

Android prowadzi program pilotażowy, który umożliwia aplikacjom wskazanym przez producentów OEM stanie się dostawcami multimediów w chmurze. Obecnie w tym programie mogą uczestniczyć tylko aplikacje wskazane przez producentów OEM, które chcą zostać dostawcami multimediów w chmurze na Androidzie. Każdy producent OEM może wskazać maksymalnie 3 aplikacje. Po zatwierdzeniu te aplikacje stają się dostępne jako dostawcy multimediów w chmurze na każdym urządzeniu z Androidem i usługami GMS, na którym są zainstalowane.

Android utrzymuje listę wszystkich kwalifikujących się dostawców chmury po stronie serwera. Każdy producent OEM może wybrać domyślnego dostawcę chmury za pomocą konfigurowalnej nakładki. Wskazane aplikacje muszą spełniać wszystkie wymagania techniczne i przejść wszystkie testy jakości. Aby dowiedzieć się więcej o procesie i wymaganiach programu pilotażowego dla dostawców multimediów w chmurze OEM, wypełnij formularz zapytania.

Zdecyduj, czy musisz utworzyć dostawcę multimediów w chmurze

Dostawcy multimediów w chmurze to aplikacje lub usługi, które mają być głównym źródłem tworzenia kopii zapasowych zdjęć i filmów oraz ich pobierania z chmury. Jeśli Twoja aplikacja ma bibliotekę przydatnych treści, ale nie jest zwykle używana jako rozwiązanie do przechowywania zdjęć, rozważ utworzenie dostawcy dokumentów zamiast tego.

Jeden aktywny dostawca chmury na profil

W danym momencie każdy profil Androida może mieć tylko 1 aktywnego dostawcę multimediów w chmurze. Użytkownicy mogą w dowolnym momencie usunąć lub zmienić wybraną aplikację dostawcy multimediów w chmurze w ustawieniach selektora zdjęć.

Domyślnie selektor zdjęć na Androidzie będzie próbował automatycznie wybrać dostawcę chmury.

  • Jeśli na urządzeniu jest tylko 1 kwalifikujący się dostawca chmury, ta aplikacja zostanie automatycznie wybrana jako bieżący dostawca.
  • Jeśli na urządzeniu jest więcej niż 1 kwalifikujący się dostawca chmury, a jeden z nich odpowiada domyślnemu dostawcy wybranemu przez producenta OEM, zostanie wybrana aplikacja wybrana przez producenta OEM.

  • Jeśli na urządzeniu jest więcej niż 1 kwalifikujący się dostawca chmury, a żaden z nich nie odpowiada domyślnemu dostawcy wybranemu przez producenta OEM, nie zostanie wybrana żadna aplikacja.

Tworzenie dostawcy multimediów w chmurze

Poniższy diagram ilustruje sekwencję zdarzeń przed sesją wybierania zdjęć i podczas niej między aplikacją na Androida, selektorem zdjęć na Androida, na urządzeniu lokalnym MediaProvider a CloudMediaProvider.

Diagram sekwencji przedstawiający przepływ danych od selektora zdjęć do dostawcy multimediów w chmurze
Rysunek 1. Diagram sekwencji zdarzeń podczas sesji wybierania zdjęć.
  1. System inicjuje preferowanego dostawcę chmury użytkownika i okresowo synchronizuje metadane multimediów z backendem selektora zdjęć na Androidzie.
  2. Gdy aplikacja na Androida uruchamia selektor zdjęć, przed wyświetleniem użytkownikowi połączonej siatki elementów lokalnych lub w chmurze selektor zdjęć przeprowadza synchronizację przyrostową z dostawcą chmury, aby zapewnić jak najbardziej aktualne wyniki. Po otrzymaniu odpowiedzi lub po upływie terminu siatka selektora zdjęć wyświetla wszystkie dostępne zdjęcia, łącząc te przechowywane lokalnie na urządzeniu z tymi zsynchronizowanymi z chmury.
  3. Gdy użytkownik przewija, selektor zdjęć pobiera miniatury multimediów od dostawcy multimediów w chmurze, aby wyświetlić je w interfejsie.
  4. Gdy użytkownik zakończy sesję, a wyniki obejmują element multimedialny w chmurze, selektor zdjęć wysyła prośbę o deskryptory plików treści, generuje identyfikator URI i przyznaje dostęp do pliku aplikacji wywołującej.
  5. Aplikacja może teraz otworzyć identyfikator URI i ma dostęp do treści multimedialnych tylko do odczytu. Domyślnie poufne metadane są redagowane. Selektor zdjęć używa systemu plików FUSE do koordynowania wymiany danych między aplikacją na Androida a dostawcą multimediów w chmurze.

Typowe problemy

Oto kilka ważnych kwestii, o których musisz pamiętać, rozważając implementację:

Unikanie duplikatów plików

Selektor zdjęć na Androidzie nie ma możliwości sprawdzenia stanu multimediów w chmurze, dlatego CloudMediaProvider musi podać MEDIA_STORE_URI w wierszu kursora dowolnego pliku, który znajduje się zarówno w chmurze, jak i na urządzeniu lokalnym. W przeciwnym razie użytkownik zobaczy w selektorze zdjęć duplikaty plików.

Optymalizacja rozmiarów obrazów pod kątem wyświetlania podglądu

Bardzo ważne jest, aby plik zwracany przez onOpenPreview nie był obrazem w pełnej rozdzielczości i był zgodny z żądanym Size. Zbyt duży obraz spowoduje wydłużenie czasu wczytywania w interfejsie, a zbyt mały może być rozpikselowany lub rozmazany w zależności od rozmiaru ekranu urządzenia.

Obsługa prawidłowej orientacji

Jeśli miniatury zwracane w onOpenPreview nie zawierają danych EXIF, powinny być zwracane w prawidłowej orientacji, aby uniknąć nieprawidłowego obracania miniatur w siatce podglądu.

Zapobiegaj nieautoryzowanemu dostępowi

Zanim zwrócisz dane do wywołującego z ContentProvider, sprawdź, czy masz MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION. Zapobiegnie to nieautoryzowanym aplikacjom dostępowi do danych w chmurze.

Klasa CloudMediaProvider

Klasa CloudMediaProvider pochodzi od android.content.ContentProvider i zawiera metody takie jak te pokazane w poniższym przykładzie:

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

Klasa CloudMediaProviderContract

Oprócz głównej klasy implementacji CloudMediaProvider selektor zdjęć na Androidzie zawiera klasę CloudMediaProviderContract. Ta klasa określa współdziałanie selektora zdjęć i dostawcy multimediów w chmurze, obejmując takie aspekty jak MediaCollectionInfo na potrzeby operacji synchronizacji, oczekiwane kolumny Cursor i dodatki 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

System operacyjny używa metody onGetMediaCollectionInfo() do oceny ważności buforowanych elementów multimedialnych w chmurze i określania niezbędnej synchronizacji z dostawcą multimediów w chmurze. Ze względu na możliwość częstych wywołań przez system operacyjny metoda onGetMediaCollectionInfo() jest uważana za krytyczną dla wydajności. Należy unikać długotrwałych operacji lub efektów ubocznych, które mogłyby negatywnie wpłynąć na wydajność. System operacyjny buforuje poprzednie odpowiedzi z tej metody i porównuje je z kolejnymi odpowiedziami, aby określić odpowiednie działania.

Kotlin

abstract fun onGetMediaCollectionInfo(extras: Bundle): Bundle

Java

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

Zwrócony MediaCollectionInfo pakiet zawiera te stałe:

onQueryMedia

Metoda onQueryMedia() służy do wypełniania głównej siatki zdjęć w selektorze zdjęć w różnych widokach. Te wywołania mogą być wrażliwe na opóźnienia i mogą być wywoływane w ramach proaktywnej synchronizacji w tle lub podczas sesji selektora zdjęć, gdy wymagany jest pełny lub przyrostowy stan synchronizacji. Interfejs selektora zdjęć nie będzie czekać w nieskończoność na odpowiedź, aby wyświetlić wyniki, i może przekroczyć limit czasu tych żądań na potrzeby interfejsu. Zwrócony kursor nadal będzie próbował zostać przetworzony w bazie danych selektora zdjęć na potrzeby przyszłych sesji.

Ta metoda zwraca Cursor reprezentujący wszystkie elementy multimedialne w kolekcji multimediów, opcjonalnie filtrowane przez podane dodatki i posortowane w odwrotnej kolejności chronologicznej według MediaColumns#DATE_TAKEN_MILLIS (najnowsze elementy jako pierwsze).

Zwrócony CloudMediaProviderContract pakiet zawiera następujące stałe:

Dostawca multimediów w chmurze musi ustawić CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID jako część zwróconego Bundle. Nieustawienie tej wartości jest błędem i unieważnia zwrócony Cursor. Jeśli dostawca multimediów w chmurze obsługiwał filtry w podanych dodatkach, musi dodać klucz do ContentResolver#EXTRA_HONORED_ARGS jako część zwróconego Cursor#setExtras.

onQueryDeletedMedia

Metoda onQueryDeletedMedia() służy do zapewnienia, że usunięte elementy na koncie w chmurze zostaną prawidłowo usunięte z interfejsu selektora zdjęć. Ze względu na potencjalną wrażliwość na opóźnienia te wywołania mogą być inicjowane w ramach:

  • proaktywnej synchronizacji w tle,
  • sesji selektora zdjęć (gdy wymagany jest pełny lub przyrostowy stan synchronizacji).

Interfejs selektora zdjęć priorytetowo traktuje responsywność i nie będzie czekać w nieskończoność na odpowiedź. Aby zapewnić płynne interakcje, mogą wystąpić przekroczenia limitu czasu. Każdy zwrócony Cursor nadal będzie próbował zostać przetworzony w bazie danych selektora zdjęć na potrzeby przyszłych sesji.

Ta metoda zwraca Cursor reprezentujący wszystkie usunięte elementy multimedialne w całej kolekcji multimediów w bieżącej wersji dostawcy, zgodnie z informacjami zwróconymi przez onGetMediaCollectionInfo(). Te elementy można opcjonalnie filtrować według dodatków. Dostawca multimediów w chmurze musi ustawić CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID jako część zwróconego Cursor#setExtras Nieustawienie tej wartości jest błędem i unieważnia Cursor. Jeśli dostawca obsługiwał filtry w podanych dodatkach, musi dodać klucz do ContentResolver#EXTRA_HONORED_ARGS.

onQueryAlbums

Metoda onQueryAlbums() służy do pobierania listy albumów w chmurze, które są dostępne u dostawcy chmury, oraz powiązanych z nimi metadanych. Więcej informacji znajdziesz w artykule CloudMediaProviderContract.AlbumColumns.

Ta metoda zwraca Cursor reprezentujący wszystkie elementy albumu w kolekcji multimediów , opcjonalnie filtrowane przez podane dodatki i posortowane w odwrotnej kolejności chronologicznej według AlbumColumns#DATE_TAKEN_MILLIS (najnowsze elementy jako pierwsze). Dostawca multimediów w chmurze musi ustawić CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID jako część zwróconego Cursor. Nieustawienie tej wartości jest błędem i unieważnia zwrócony Cursor. Jeśli dostawca obsługiwał filtry w podanych dodatkach, musi dodać klucz do ContentResolver#EXTRA_HONORED_ARGS jako część zwróconego Cursor.

onOpenMedia

Metoda onOpenMedia() powinna zwracać multimedia w pełnym rozmiarze zidentyfikowane przez podany mediaId. Jeśli ta metoda blokuje pobieranie treści na urządzenie, należy okresowo sprawdzać podany CancellationSignal, aby przerwać porzucone żądania.

onOpenPreview

Metoda onOpenPreview() powinna zwracać miniaturę o podanym size dla elementu o podanym mediaId. Miniatura powinna być w oryginalnym formacie CloudMediaProviderContract.MediaColumns#MIME_TYPE i powinna mieć znacznie niższą rozdzielczość niż element zwracany przez onOpenMedia. Jeśli ta metoda jest zablokowana podczas pobierania treści na urządzenie, należy okresowo sprawdzać podany CancellationSignal, aby przerwać porzucone żądania.

onCreateCloudMediaSurfaceController

Metoda onCreateCloudMediaSurfaceController() powinna zwracać CloudMediaSurfaceController używany do renderowania podglądu elementów multimedialnych lub null, jeśli renderowanie podglądu nie jest obsługiwane.

Klasa CloudMediaSurfaceController zarządza renderowaniem podglądu elementów multimedialnych w podanych instancjach Surface. Metody tej klasy mają być asynchroniczne i nie powinny blokować się przez wykonywanie żadnych złożonych operacji. Jedna instancja CloudMediaSurfaceController jest odpowiedzialna za renderowanie wielu elementów multimedialnych powiązanych z wieloma powierzchniami.

Klasa CloudMediaSurfaceController obsługuje te wywołania zwrotne cyklu życia: