Membuat penyedia media cloud untuk Android

Penyedia media cloud menyediakan konten media cloud tambahan untuk perangkat pemilih foto. Pengguna dapat memilih foto atau video yang disediakan oleh penyedia media cloud saat aplikasi menggunakan ACTION_PICK_IMAGES atau ACTION_GET_CONTENT untuk meminta file media dari pengguna. Media cloud penyedia juga dapat memberikan informasi tentang album, yang dapat dijelajahi di Pemilih foto Android.

Sebelum memulai

Pertimbangkan item berikut sebelum Anda mulai membangun solusi cloud penyedia media tersebut.

Kelayakan

Android menjalankan program uji coba untuk memungkinkan aplikasi yang dinominasikan OEM menjadi cloud penyedia media. Hanya aplikasi yang dinominasikan oleh OEM yang memenuhi syarat untuk berpartisipasi program ini untuk menjadi penyedia media cloud untuk Android pada saat ini. Masing-masing OEM dapat menominasikan hingga 3 aplikasi. Setelah disetujui, aplikasi ini dapat diakses sebagai penyedia media cloud di perangkat mana pun yang didukung Android GMS tempat mereka berada terinstal.

Android mengelola daftar sisi server dari semua penyedia cloud yang memenuhi syarat. Setiap OEM dapat memilih penyedia cloud default menggunakan overlay yang dapat dikonfigurasi. Dinominasikan aplikasi harus memenuhi semua persyaratan teknis dan lulus semua uji kualitas. Untuk mempelajari lebih lanjut tentang proses program uji coba penyedia media cloud OEM persyaratan, lengkapi formulir pertanyaan.

Menentukan apakah Anda perlu membuat penyedia media cloud

Penyedia media cloud dimaksudkan sebagai aplikasi atau layanan yang bertindak sebagai sumber utama untuk mencadangkan dan mengambil foto serta video dari {i>cloud<i}. Jika aplikasi Anda memiliki pustaka konten yang berguna, tetapi biasanya tidak digunakan sebagai solusi penyimpanan foto, sebaiknya pertimbangkan untuk membuat penyedia dokumen sebagai gantinya.

Satu penyedia cloud aktif per profil

Hanya boleh ada maksimal satu penyedia media cloud yang aktif pada satu waktu untuk setiap perangkat profil Anda. Pengguna mungkin menghapus atau mengubah penyedia media cloud yang mereka pilih kapan saja dari setelan pemilih foto.

Secara default, pemilih foto Android akan mencoba memilih penyedia cloud secara otomatis.

  • Jika hanya ada satu penyedia {i>cloud <i} yang memenuhi syarat di perangkat, aplikasi tersebut akan dipilih sebagai penyedia saat ini secara otomatis.
  • Jika ada lebih dari satu penyedia {i>cloud <i} yang memenuhi syarat di perangkat dan salah satu cocok dengan default pilihan OEM, lalu aplikasi yang dipilih OEM akan dipilih.

  • Jika ada lebih dari satu penyedia {i>cloud <i} yang memenuhi syarat di perangkat, dan tidak ada cocok dengan OEM yang dipilih default, tidak ada aplikasi yang akan dipilih.

Membangun penyedia media cloud Anda

Diagram berikut mengilustrasikan urutan peristiwa baik sebelum dan selama sesi pemilihan foto antara aplikasi Android, pemilih foto Android, MediaProvider perangkat lokal, dan CloudMediaProvider.

Diagram urutan yang menunjukkan alur dari pemilih foto ke penyedia media cloud
Gambar 1: Diagram urutan peristiwa selama sesi pemilihan foto.
  1. Sistem akan melakukan inisialisasi penyedia cloud pilihan pengguna dan secara berkala menyinkronkan metadata media ke backend pemilih foto Android.
  2. Saat aplikasi Android meluncurkan pemilih foto, sebelum menampilkan foto lokal yang digabungkan atau petak item cloud kepada pengguna, pemilih foto menjalankan fungsi yang sensitif sinkronisasi inkremental dengan penyedia cloud untuk memastikan hasilnya aktual mungkin. Setelah menerima respons, atau ketika batas waktu tercapai, petak pemilih foto kini menampilkan semua foto yang dapat diakses, yang menggabungkan foto yang disimpan secara lokal di perangkat Anda dengan data yang disinkronkan dari {i>cloud<i}.
  3. Selagi pengguna men-scroll, pemilih foto mengambil thumbnail media dari penyedia media cloud untuk ditampilkan di UI.
  4. Saat pengguna menyelesaikan sesi dan hasilnya mencakup media cloud item, pemilih foto akan meminta deskriptor file untuk konten, menghasilkan URI, dan memberikan akses ke file untuk aplikasi panggilan.
  5. Aplikasi kini dapat membuka URI dan memiliki akses hanya baca ke media konten. Secara default, metadata sensitif disamarkan. Pemilih foto memanfaatkan sistem file FUSE untuk mengkoordinasikan pertukaran data antara aplikasi Android dan penyedia media cloud.

Masalah Umum

Berikut ini beberapa pertimbangan penting yang harus diperhatikan saat mempertimbangkan penerapan:

Menghindari file duplikat

Karena pemilih foto Android tidak dapat memeriksa status media cloud, CloudMediaProvider harus menyediakan MEDIA_STORE_URI di kursor baris file apa pun yang ada di {i>cloud<i} dan di perangkat lokal, atau di akan melihat file duplikat di pemilih foto.

Mengoptimalkan ukuran gambar untuk tampilan pratinjau

Sangat penting bahwa file yang ditampilkan dari onOpenPreview tidak penuh resolusi gambar, dan mematuhi Size yang diminta. Gambar terlalu besar akan menimbulkan waktu pemuatan di UI, dan terlalu kecil gambar mungkin akan mengalami pikselasi atau buram berdasarkan ukuran layar perangkat.

Menangani orientasi yang benar

Jika thumbnail yang ditampilkan di onOpenPreview tidak berisi data EXIF, thumbnail tersebut akan harus dikembalikan dalam orientasi yang benar agar thumbnail tidak diputar di petak pratinjau.

Mencegah akses yang tidak sah

Periksa MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION sebelum mengembalikan data ke pemanggil dari ContentProvider. Tindakan ini akan mencegah aplikasi yang tidak diizinkan yang mengakses data cloud.

Class CloudMediaProvider

Berasal dari android.content.ContentProvider, CloudMediaProvider menyertakan metode seperti yang ditunjukkan dalam contoh berikut:

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

Class CloudMediaProviderContract

Selain class implementasi CloudMediaProvider utama, class Pemilih foto Android menggabungkan class CloudMediaProviderContract. Kelas ini menjelaskan interoperabilitas antara pemilih foto dan cloud penyedia media, yang mencakup aspek seperti MediaCollectionInfo untuk operasi sinkronisasi, kolom Cursor yang diharapkan, dan tambahan 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

Metode onGetMediaCollectionInfo() digunakan oleh sistem operasi untuk menilai validitas item media cloud yang di-cache dan menentukan sinkronisasi dengan penyedia media {i>cloud<i}. Karena potensi terjadinya oleh sistem operasi, onGetMediaCollectionInfo() dianggap performa yang penting; sangat penting untuk menghindari operasi yang berjalan lama atau yang dapat berdampak negatif terhadap performa. Sistem operasi menyimpan {i>cache<i} respons sebelumnya dari metode ini dan membandingkannya dengan respons berikutnya untuk menentukan tindakan yang tepat.

Kotlin

abstract fun onGetMediaCollectionInfo(extras: Bundle): Bundle

Java

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

Paket MediaCollectionInfo yang ditampilkan menyertakan konstanta berikut:

onQueryMedia

Metode onQueryMedia() digunakan untuk mengisi petak foto utama di pemilih foto dalam berbagai tampilan. Panggilan ini mungkin sensitif terhadap latensi, dan dapat dipanggil sebagai bagian dari sinkronisasi proaktif latar belakang, atau selama pemilih foto sesi saat status sinkronisasi penuh atau inkremental diperlukan. Pemilih foto antarmuka pengguna tidak akan menunggu waktu yang ditentukan untuk menampilkan hasil, dan mungkin kehabisan waktu permintaan ini untuk tujuan antarmuka pengguna. Kursor yang dikembalikan masih akan mencoba diproses ke dalam database pemilih foto untuk masa mendatang sesi.

Metode ini menampilkan Cursor yang mewakili semua item media pada media koleksi secara opsional difilter berdasarkan tambahan yang disediakan dan diurutkan secara terbalik urutan kronologis MediaColumns#DATE_TAKEN_MILLIS (item terbaru terlebih dahulu).

Paket CloudMediaProviderContract yang ditampilkan mencakup hal berikut konstanta:

Penyedia media cloud harus menetapkan CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID sebagai bagian dari item baris yang ditampilkan Bundle. Tidak menetapkan nilai ini adalah error dan membuat Cursor yang ditampilkan menjadi tidak valid. Jika penyedia media {i>cloud <i}menangani semua filter dalam tambahan yang disediakan, penyedia itu harus kunci ke ContentResolver#EXTRA_HONORED_ARGS sebagai bagian dari nilai yang ditampilkan Cursor#setExtras.

onQueryDeletedMedia

Metode onQueryDeletedMedia() digunakan untuk memastikan item yang dihapus di akun cloud dihapus dengan benar dari antarmuka pengguna pemilih foto. Karena sensitivitas latensi potensialnya, panggilan ini dapat dimulai sebagai bagian dari:

  • Sinkronisasi proaktif latar belakang
  • Sesi pemilih foto (jika status sinkronisasi penuh atau inkremental diperlukan)

Antarmuka pengguna pemilih foto memprioritaskan pengalaman pengguna yang responsif dan tidak akan menunggu respons untuk waktu yang tak terbatas. Untuk mempertahankan interaksi yang lancar, mungkin terjadi waktu tunggu. Setiap Cursor yang dikembalikan akan tetap berupaya diproses ke dalam {i>database <i}pemilih foto untuk sesi-sesi selanjutnya.

Metode ini menampilkan Cursor yang mewakili semua item media yang dihapus dalam seluruh koleksi media dalam versi penyedia saat ini seperti yang ditampilkan oleh onGetMediaCollectionInfo(). Item ini dapat difilter secara opsional menurut tambahan. Penyedia media cloud harus menetapkan CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID sebagai bagian dari item baris yang ditampilkan Cursor#setExtras Tidak menetapkan ini adalah error dan membuat Cursor menjadi tidak valid. Jika penyedia menangani setiap filter dalam tambahan yang disediakan, penyedia itu harus menambahkan kunci ContentResolver#EXTRA_HONORED_ARGS.

onQueryAlbums

Metode onQueryAlbums() digunakan untuk mengambil daftar album Cloud yang tersedia di penyedia {i>cloud<i}, dan {i>metadata<i} yang terkait. Lihat CloudMediaProviderContract.AlbumColumns untuk detail tambahan.

Metode ini menampilkan Cursor yang mewakili semua item album di media koleksi secara opsional difilter berdasarkan tambahan yang disediakan dan diurutkan secara terbalik urutan kronologis AlbumColumns#DATE_TAKEN_MILLIS , item terbaru terlebih dahulu. Penyedia media cloud harus menetapkan CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID sebagai bagian dari item baris yang ditampilkan Cursor. Tidak menetapkan nilai ini adalah error dan membuat Cursor yang ditampilkan menjadi tidak valid. Jika penyedia menangani setiap filter dalam tambahan yang disediakan, penyedia itu harus menambahkan kunci ContentResolver#EXTRA_HONORED_ARGS sebagai bagian dari Cursor yang ditampilkan.

onOpenMedia

Metode onOpenMedia() harus menampilkan media ukuran penuh yang diidentifikasi oleh mediaId yang disediakan. Jika metode ini melakukan pemblokiran saat mendownload konten ke perangkat Anda, secara berkala Anda harus memeriksa CancellationSignal yang disediakan untuk membatalkan permintaan yang diabaikan.

onOpenPreview

Metode onOpenPreview() harus menampilkan thumbnail yang disediakan size untuk item mediaId yang disediakan. Thumbnail harus berada di CloudMediaProviderContract.MediaColumns#MIME_TYPE asli dan diharapkan memiliki resolusi yang jauh lebih rendah daripada item yang ditampilkan oleh onOpenMedia. Jika metode ini diblokir saat mengunduh konten ke perangkat, Anda harus secara berkala periksa CancellationSignal yang disediakan untuk membatalkan permintaan yang diabaikan.

onCreateCloudMediaSurfaceController

Metode onCreateCloudMediaSurfaceController() harus menampilkan CloudMediaSurfaceController digunakan untuk merender pratinjau item media, atau null jika rendering pratinjau tidak didukung.

CloudMediaSurfaceController mengelola rendering pratinjau item media pada instance Surface tertentu. Metode class ini dimaksudkan untuk asinkron, dan tidak boleh memblokir dengan melakukan operasi berat apa pun. Satu Instance CloudMediaSurfaceController bertanggung jawab untuk merender beberapa item media yang terkait dengan beberapa platform.

CloudMediaSurfaceController memiliki dukungan untuk daftar callback siklus proses: