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
.
- Sistem menginisialisasi penyedia cloud pilihan pengguna dan menyinkronkan metadata media secara berkala ke backend pemilih foto Android.
- 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.
- Saat pengguna men-scroll, pemilih foto akan mengambil thumbnail media dari penyedia media cloud untuk ditampilkan di UI.
- 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.
- 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:
EXTRA_ALBUM_ID
EXTRA_LOOPING_PLAYBACK_ENABLED
EXTRA_MEDIA_COLLECTION_ID
EXTRA_PAGE_SIZE
EXTRA_PAGE_TOKEN
EXTRA_PREVIEW_THUMBNAIL
EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED
EXTRA_SYNC_GENERATION
MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION
PROVIDER_INTERFACE
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:
onConfigChange
onDestroy
onMediaPause
onMediaPlay
onMediaSeekTo
onPlayerCreate
onPlayerRelease
onSurfaceChanged
onSurfaceCreated
onSurfaceDestroyed