Membuat penyedia media cloud untuk Android

Penyedia media cloud menyediakan konten media cloud tambahan ke pemilih foto Android. 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. Penyedia media cloud juga dapat memberikan informasi tentang album, yang dapat dijelajahi di pemilih foto Android.

Sebelum memulai

Pertimbangkan item berikut sebelum Anda mulai membangun penyedia media cloud.

Kelayakan

Android menjalankan program uji coba untuk memungkinkan aplikasi yang dinominasikan OEM untuk menjadi penyedia media cloud. Hanya aplikasi yang dinominasikan oleh OEM yang memenuhi syarat untuk berpartisipasi dalam program ini guna menjadi penyedia media cloud untuk Android saat ini. Setiap OEM dapat menominasikan hingga 3 aplikasi. Setelah disetujui, aplikasi ini dapat diakses sebagai penyedia media cloud di perangkat yang didukung GMS Android yang menginstalnya.

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

Menentukan apakah Anda perlu membuat penyedia media cloud

Penyedia media cloud dimaksudkan sebagai aplikasi atau layanan yang berfungsi sebagai sumber utama pengguna untuk mencadangkan serta mengambil foto dan video dari cloud. Jika aplikasi Anda memiliki library 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 profil Android. Pengguna dapat menghapus atau mengubah aplikasi penyedia media cloud yang dipilih kapan saja dari setelan pemilih foto.

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

  • Jika hanya ada satu penyedia cloud yang memenuhi syarat di perangkat, aplikasi tersebut akan otomatis dipilih sebagai penyedia saat ini.
  • Jika ada lebih dari satu penyedia cloud yang memenuhi syarat di perangkat dan salah satunya cocok dengan default yang dipilih OEM, aplikasi yang dipilih OEM akan dipilih.

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

Membangun penyedia media cloud Anda

Diagram berikut mengilustrasikan urutan peristiwa 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 menginisialisasi penyedia cloud pilihan pengguna dan menyinkronkan metadata media secara berkala ke backend pemilih foto Android.
  2. Saat aplikasi Android meluncurkan pemilih foto, sebelum menampilkan petak item lokal atau cloud yang digabungkan kepada pengguna, pemilih foto akan melakukan sinkronisasi inkremental yang sensitif terhadap latensi dengan penyedia cloud untuk memastikan hasilnya selalu terbaru. 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 foto yang disinkronkan dari cloud.
  3. Saat pengguna men-scroll, pemilih foto akan mengambil thumbnail media dari penyedia media cloud untuk ditampilkan di UI.
  4. Saat pengguna menyelesaikan sesi dan hasilnya menyertakan item media cloud, pemilih foto meminta deskriptor file untuk konten, membuat URI, dan memberikan akses ke file ke aplikasi panggilan.
  5. Aplikasi kini dapat membuka URI dan memiliki akses hanya baca ke konten media. Secara default, metadata sensitif akan disamarkan. Pemilih foto memanfaatkan sistem file FUSE untuk mengoordinasikan pertukaran data antara aplikasi Android dan penyedia media cloud.

Masalah Umum

Berikut adalah beberapa pertimbangan penting yang perlu diingat saat mempertimbangkan implementasi Anda:

Menghindari file duplikat

Karena pemilih foto Android tidak memiliki cara untuk memeriksa status media cloud, CloudMediaProvider harus menyediakan MEDIA_STORE_URI di baris kursor file apa pun yang ada di cloud dan di perangkat lokal, atau pengguna akan melihat file duplikat di pemilih foto.

Mengoptimalkan ukuran gambar untuk tampilan pratinjau

Sangat penting bahwa file yang ditampilkan dari onOpenPreview bukan merupakan gambar dengan resolusi penuh, dan mematuhi Size yang diminta. Gambar yang terlalu besar akan menyebabkan waktu pemuatan di UI, dan gambar yang terlalu kecil dapat menjadi pecah atau buram berdasarkan ukuran layar perangkat.

Tangani orientasi yang benar

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

Mencegah akses yang tidak sah

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

Class CloudMediaProvider

Berasal dari android.content.ContentProvider, class 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, pemilih foto Android menggabungkan class CloudMediaProviderContract. Class ini menguraikan interoperabilitas antara pemilih foto dan penyedia media cloud, yang mencakup aspek seperti MediaCollectionInfo untuk operasi sinkronisasi, kolom Cursor yang diperkirakan, 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 yang diperlukan dengan penyedia media cloud. Karena potensi panggilan yang sering oleh sistem operasi, onGetMediaCollectionInfo() dianggap penting performanya; sangat penting untuk menghindari operasi yang berjalan lama atau efek samping yang dapat berdampak negatif pada performa. Sistem operasi meng-cache respons sebelumnya dari metode ini dan membandingkannya dengan respons berikutnya untuk menentukan tindakan yang sesuai.

Kotlin

abstract fun onGetMediaCollectionInfo(extras: Bundle): Bundle

Java

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

Paket MediaCollectionInfo yang ditampilkan mencakup 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 sesi pemilih foto saat status sinkronisasi penuh atau inkremental diperlukan. Antarmuka pengguna pemilih foto tidak akan menunggu respons tanpa batas waktu untuk menampilkan hasil, dan mungkin akan kehabisan waktu permintaan untuk tujuan antarmuka pengguna. Kursor yang ditampilkan masih akan mencoba diproses ke dalam database pemilih foto untuk sesi mendatang.

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

Paket CloudMediaProviderContract yang ditampilkan mencakup konstanta berikut:

Penyedia media cloud harus menetapkan CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID sebagai bagian dari Bundle yang ditampilkan. Tidak menetapkan ini merupakan error dan membuat Cursor yang ditampilkan menjadi tidak valid. Jika penyedia media cloud menangani filter apa pun dalam tambahan yang disediakan, penyedia tersebut harus menambahkan kunci ke ContentResolver#EXTRA_HONORED_ARGS sebagai bagian dari Cursor#setExtras yang ditampilkan.

onQueryDeletedMedia

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

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

Antarmuka pengguna pemilih foto memprioritaskan pengalaman pengguna yang responsif dan tidak akan menunggu respons tanpa batas waktu. Untuk menjaga interaksi yang lancar, waktu tunggu mungkin terjadi. Setiap Cursor yang ditampilkan masih akan mencoba diproses ke dalam database pemilih foto untuk sesi selanjutnya.

Metode ini menampilkan Cursor yang mewakili semua item media yang dihapus di 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 Cursor#setExtras yang ditampilkan. Tidak menetapkan ini sebagai error dan membatalkan Cursor. Jika penyedia menangani filter dalam tambahan yang disediakan, penyedia harus menambahkan kunci ke ContentResolver#EXTRA_HONORED_ARGS.

onQueryAlbums

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

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

onOpenMedia

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

onOpenPreview

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

{i>onCreateCloudMediaSurfaceController<i}

Metode onCreateCloudMediaSurfaceController() akan menampilkan CloudMediaSurfaceController yang 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 bersifat asinkron, dan tidak boleh memblokir dengan melakukan operasi berat. Satu instance CloudMediaSurfaceController bertanggung jawab untuk merender beberapa item media yang terkait dengan beberapa platform.

CloudMediaSurfaceController memiliki dukungan untuk daftar callback siklus proses berikut: