Tworzenie dostawcy multimediów w chmurze na Androida

Dostawca multimediów w chmurze udostępnia dodatkowe treści multimedialne w chmurze do selektora zdjęć na Androidzie. Użytkownicy mogą wybierać zdjęcia i filmy dostarczone przez dostawcę multimediów w chmurze, gdy aplikacja korzysta z ACTION_PICK_IMAGES lub ACTION_GET_CONTENT, aby prosić użytkownika o udostępnienie plików multimedialnych. Dostawca multimediów w chmurze może też przekazać informacje o albumach, które można przeglądać w selektorze zdjęć na Androidzie.

Zanim zaczniesz

Zanim rozpoczniesz tworzenie dostawcy multimediów w chmurze, weź pod uwagę te informacje.

Wymagania

Android prowadzi program pilotażowy, w ramach którego aplikacje nominowane przez OEM mogą stać się dostawcami multimediów w chmurze. Obecnie w tym programie mogą brać udział tylko aplikacje nominowane przez producentów OEM, aby stać się dostawcą multimediów w chmurze na Androida. Każdy produkt OEM może nominować maksymalnie 3 aplikacje. Po zatwierdzeniu aplikacje te stają się dostępne jako dostawcy multimediów w chmurze na każdym urządzeniu z systemem GMS z Androidem, na którym są zainstalowane.

Android prowadzi po stronie serwera listę wszystkich kwalifikujących się dostawców chmury. Każdy OEM może wybrać domyślnego dostawcę usług chmurowych za pomocą konfigurowalnej nakładki. Wyznaczone 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 OEM dla dostawców multimediów w chmurze, wypełnij formularz zapytania.

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

Dostawcy multimediów w chmurze to aplikacje lub usługi działające jako podstawowe źródło tworzenia kopii zapasowych zdjęć i filmów z chmury oraz ich pobierania. Jeśli Twoja aplikacja zawiera bibliotekę przydatnych treści, ale zwykle nie jest ona używana jako rozwiązanie do przechowywania zdjęć, rozważ utworzenie dostawcy dokumentów.

Jeden aktywny dostawca chmury na profil

W przypadku każdego profilu Androida może istnieć maksymalnie 1 aktywny dostawca multimediów w chmurze. Użytkownicy mogą w każdej chwili usunąć lub zmienić wybraną aplikację dostawcy multimediów w ustawieniach selektora zdjęć.

Domyślnie selektor zdjęć na Androidzie automatycznie spróbuje wybrać dostawcę usług w chmurze.

  • Jeśli na urządzeniu dostępnych jest tylko 1 odpowiedni dostawca chmury, aplikacja zostanie automatycznie wybrana jako obecny dostawca.
  • Jeśli na urządzeniu dostępnych jest więcej niż 1 odpowiedni dostawca usług w chmurze, a jeden z nich jest zgodny z domyślnym wybranym dostawcą OEM, zostanie wybrana aplikacja wybrana przez OEM.

  • Jeśli na urządzeniu jest więcej niż 1 kwalifikujący się dostawca usług w chmurze, ale żaden z nich nie jest zgodny z wybranym domyślnym dostawcą OEM, żadna aplikacja nie zostanie wybrana.

Utwórz swojego dostawcę multimediów w chmurze

Na diagramie poniżej widać sekwencję zdarzeń zarówno przed, jak i w trakcie sesji wyboru zdjęcia między aplikacją na Androida, selektorem zdjęć na Androidzie, MediaProvider na urządzeniu lokalnym i CloudMediaProvider na urządzeniu lokalnym.

Schemat sekwencji pokazujący przepływ danych z selektora zdjęć do dostawcy multimediów w chmurze
Rysunek 1. Schemat sekwencji zdarzeń podczas sesji wyboru 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ęć, to zanim wyświetli użytkownikowi scaloną siatkę elementów lokalnych lub w chmurze, selektor zdjęć przeprowadzi przyrostową synchronizację z dostawcą chmury, aby zapewnić jak najbardziej aktualne wyniki. Gdy otrzymasz odpowiedź lub nadejdzie termin, w siatce selektora zdjęć pojawią się wszystkie dostępne zdjęcia, które będą połączone ze zdjęciami zapisanymi lokalnie na urządzeniu i zsynchronizowanymi z chmurą.
  3. Gdy użytkownik przewija stronę, selektor zdjęć pobiera od dostawcy multimediów w chmurze miniatury, które są wyświetlane w interfejsie.
  4. Gdy użytkownik zakończy sesję, gdy wyniki będą zawierać element multimedialny w chmurze, selektor zdjęć poprosi o deskryptory plików tych treści, wygeneruje identyfikator URI i przyzna dostęp do pliku aplikacji wywołującej.
  5. Aplikacja może teraz otwierać identyfikator URI i ma dostęp tylko do odczytu do treści multimedialnych. Domyślnie poufne metadane są usuwane. Selektor zdjęć wykorzystuje system 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 należy pamiętać, rozważając wdrożenie:

Unikanie duplikatów plików

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

Zoptymalizuj rozmiary obrazu na potrzeby wyświetlania podglądu

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

Obsługa prawidłowej orientacji

Jeśli miniatury zwrócone w funkcji onOpenPreview nie zawierają danych EXIF, powinny zostać zwrócone w prawidłowej orientacji, aby uniknąć nieprawidłowego obracania miniatur na siatce podglądu.

Zapobieganie nieautoryzowanemu dostępowi

Zanim zwrócisz dane elementu wywołującego z ContentProvider, sprawdź, czy występuje MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION. Dzięki temu niepowołane aplikacje nie będą miały dostępu do danych w chmurze.

Klasa CloudMediaProvider

Pochodzi z klasy android.content.ContentProvider, więc klasa CloudMediaProvider zawiera metody podobne do tych w tym 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 omawia interoperacyjność między selektorem zdjęć a dostawcą multimediów w chmurze, w tym takie aspekty jak MediaCollectionInfo operacje synchronizacji, oczekiwane Cursor kolumny i Bundle dodatki.

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 poprawności elementów multimedialnych przechowywanych w pamięci podręcznej w chmurze i określania niezbędnej synchronizacji z dostawcą multimediów w chmurze. Ze względu na możliwość częstych wywołań systemu operacyjnego onGetMediaCollectionInfo() uważa się za bardzo ważne dla wydajności. Należy unikać długotrwałych operacji lub efektów ubocznych, które mogłyby obniżyć wydajność. System operacyjny zapisuje w pamięci podręcznej wcześniejsze 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);

Zwracany pakiet MediaCollectionInfo zawiera te stałe:

onQueryMedia

Metoda onQueryMedia() jest używana do wypełniania głównej siatki zdjęć w selektorze zdjęć w różnych widokach. Te wywołania mogą być wrażliwe na czas oczekiwania i można je wywoływać w ramach aktywnej synchronizacji w tle lub podczas sesji selektora zdjęć, gdy wymagany jest pełny lub przyrostowy stan synchronizacji. Interfejs selektora zdjęć nie czeka w nieskończoność na odpowiedź z wynikami. Może też przekroczyć limit czasu na realizację tych żądań. Zwrócony kursor nadal będzie próbować zostać przetworzony w bazie danych selektora zdjęć na potrzeby przyszłych sesji.

Ta metoda zwraca element Cursor reprezentujący wszystkie elementy multimedialne w kolekcji multimediów opcjonalnie przefiltrowany według tych materiałów i posortowany w odwrotnej kolejności chronologicznej wartości MediaColumns#DATE_TAKEN_MILLIS (najnowsze elementy na początku).

Zwracany pakiet CloudMediaProviderContract zawiera te stałe:

Dostawca multimediów w chmurze musi ustawić CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID jako część zwróconego Bundle. Jeśli nie skonfigurujesz tej wartości, jest to błąd i unieważnia zwrócone wartości Cursor. Jeśli dostawca multimediów w chmurze obsługuje filtry w dostarczonych dodatkiach, musi dodać klucz do ContentResolver#EXTRA_HONORED_ARGS w ramach zwróconego Cursor#setExtras.

onQueryUsunięteMedia

Dzięki metodzie onQueryDeletedMedia() usunięte elementy z konta w chmurze są prawidłowo usuwane z interfejsu selektora zdjęć. Ze względu na wrażliwość na opóźnienia te połączenia mogą być inicjowane w ramach:

  • Aktywna synchronizacja w tle
  • Sesje selektora zdjęć (gdy wymagana jest pełna lub przyrostowa synchronizacja)

Interfejs selektora zdjęć traktuje priorytetowo elastyczne działanie użytkownika i nie czeka na odpowiedź w nieskończoność. Aby zapewnić płynność interakcji, mogą wystąpić przekroczenia limitów czasu. Wszelkie zwrócone dane Cursor nadal będą próbować przetworzyć je w bazie danych selektora zdjęć na potrzeby przyszłych sesji.

Ta metoda zwraca wartość Cursor reprezentującą wszystkie usunięte elementy multimedialne w całej kolekcji multimediów w bieżącej wersji dostawcy, która jest zwracana przez metodę 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óconej wartości. Cursor#setExtras Jeśli nie skonfigurujesz tego ustawienia, jest to błąd i unieważnia Cursor. Jeśli dostawca obsługiwał jakiekolwiek filtry w dostarczonych dodatków, musi dodać klucz do funkcji 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, wraz z powiązanymi z nimi metadanymi. Więcej informacji znajdziesz w sekcji CloudMediaProviderContract.AlbumColumns.

Ta metoda zwraca element Cursor reprezentujący wszystkie elementy z albumu w kolekcji multimediów opcjonalnie przefiltrowany według tych materiałów i posortowany w odwrotnej kolejności chronologicznej według: AlbumColumns#DATE_TAKEN_MILLIS (najnowsze elementy na początku). Dostawca multimediów w chmurze musi ustawić CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID jako część zwróconego Cursor. Jeśli nie skonfigurujesz tej wartości, jest to błąd i unieważnia zwrócone wartości Cursor. Jeśli dostawca obsługiwał jakiekolwiek filtry w dostarczonych dodatków, musi dodać klucz do ContentResolver#EXTRA_HONORED_ARGS w ramach zwracanego atrybutu Cursor.

onOpenMedia

Metoda onOpenMedia() powinna zwracać multimedia w pełnym rozmiarze określone przez podany parametr mediaId. Jeśli ta metoda blokuje pobieranie treści na urządzenie, co jakiś czas sprawdzaj podaną wartość CancellationSignal, aby przerwać porzucone żądania.

onOpenPreview

Metoda onOpenPreview() powinna zwrócić miniaturę podanego parametru size dla elementu podanego mediaId. Miniatura powinna być w oryginalnym formacie CloudMediaProviderContract.MediaColumns#MIME_TYPE, a rozdzielczość powinna być znacznie mniejsza niż w przypadku elementu zwróconego przez onOpenMedia. Jeśli ta metoda jest zablokowana podczas pobierania treści na urządzenie, okresowo sprawdzaj podaną wartość CancellationSignal, aby przerwać porzucone żądania.

Kontroler onCreateCloudMediaSurfaceController

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

Element CloudMediaSurfaceController zarządza renderowaniem podglądu elementów multimedialnych w danych instancjach Surface. Metody tej klasy są asynchroniczne i nie powinny być blokowane przez wykonywanie żadnych intensywnych operacji. Pojedyncza instancja CloudMediaSurfaceController odpowiada za renderowanie wielu elementów multimedialnych powiązanych z wieloma platformami.

CloudMediaSurfaceController obsługuje tę listę wywołań zwrotnych cyklu życia: