ספק מדיה בענן מספק תוכן מדיה נוסף בענן לכלי לבחירת תמונות ב-Android. המשתמשים יכולים לבחור תמונות או סרטונים שסופקו על ידי ספק המדיה בענן כשאפליקציה משתמשת ב-ACTION_PICK_IMAGES או ב-ACTION_GET_CONTENT כדי לבקש קובצי מדיה מהמשתמש. ספק מדיה בענן יכול גם לספק מידע על אלבומים, שאפשר לעיין בהם בכלי לבחירת תמונות ב-Android.
לפני שמתחילים
לפני שמתחילים לבנות את ספק המדיה בענן, כדאי לקחת בחשבון את הפריטים הבאים.
זכאות
אנחנו מריצים תוכנית פיילוט ב-Android כדי לאפשר לאפליקציות שנבחרו על ידי יצרני ציוד מקורי (OEM) להפוך לספקי מדיה בענן. בשלב הזה, רק אפליקציות שמועמדות על ידי OEM (יצרן ציוד מקורי) יכולות להשתתף בתוכנית הזו ולהפוך לספקי מדיה בענן ל-Android. כל יצרן ציוד מקורי יכול להציע עד 3 אפליקציות. אחרי האישור, אפשר לגשת לאפליקציות האלה כספקי מדיה בענן בכל מכשיר Android עם GMS שבו הן מותקנות.
Android שומר רשימה בצד השרת של כל ספקי הענן שעומדים בדרישות. כל יצרן ציוד מקורי יכול לבחור ספק שירותי ענן שיוגדר כברירת מחדל באמצעות שכבת-על שאפשר להגדיר. האפליקציות המועמדות צריכות לעמוד בכל הדרישות הטכניות ולעבור את כל בדיקות האיכות. כדי לקבל מידע נוסף על התהליך והדרישות של תוכנית הפיילוט לספקי מדיה בענן של OEM (יצרן ציוד מקורי), ממלאים את טופס הפנייה.
האם צריך ליצור ספק מדיה בענן
ספקי מדיה בענן הם אפליקציות או שירותים שמשמשים כמקור העיקרי של המשתמשים לגיבוי ולאחזור של תמונות וסרטונים מהענן. אם באפליקציה יש ספרייה של תוכן שימושי, אבל היא לא משמשת בדרך כלל כפתרון לאחסון תמונות, כדאי ליצור ספק מסמכים במקום זאת.
ספק שירותי ענן פעיל אחד לכל פרופיל
בכל רגע נתון יכול להיות לכל פרופיל Android רק פלאגין שמתממשק עם שירותים חיצוניים אחד פעיל של מדיה בענן. המשתמשים יכולים להסיר או לשנות את האפליקציה של ספק המדיה בענן שהם בחרו בכל שלב בהגדרות של כלי לבחירת תמונות.
כברירת מחדל, כלי לבחירת תמונות של Android ינסה לבחור ספק שירותי ענן באופן אוטומטי.
- אם יש במכשיר רק ספק שירותי ענן אחד שעומד בדרישות, האפליקציה שלו תיבחר אוטומטית כספק הנוכחי.
אם יש במכשיר יותר מספקי ענן שעומדים בדרישות, ואחד מהם תואם לברירת המחדל שנבחרה על ידי יצרן הציוד המקורי, האפליקציה שנבחרה על ידי יצרן הציוד המקורי תיבחר.
אם יש במכשיר יותר מספקי ענן שעומדים בדרישות, ואף אחד מהם לא תואם לברירת המחדל שנבחרה על ידי יצרן הציוד המקורי, לא תיבחר אף אפליקציה.
איך יוצרים ספק מדיה בענן
בתרשים הבא מוצגת רצף האירועים לפני ובמהלך סשן של בחירת תמונות בין אפליקציית Android, כלי בחירת התמונות של Android, MediaProvider במכשיר המקומי ו-CloudMediaProvider.
- המערכת מאתחלת את ספק שירותי הענן המועדף על המשתמש ומסנכרנת מעת לעת את המטא-נתונים של המדיה עם הבק-אנד של כלי לבחירת תמונות ב-Android.
- כשמפעילים את כלי בחירת התמונות באפליקציית Android, לפני שמוצגת למשתמש רשת של פריטים מקומיים או פריטים בענן, כלי בחירת התמונות מבצע סנכרון מצטבר עם ספק שירותי הענן, שרגיש לזמן האחזור, כדי לוודא שהתוצאות עדכניות ככל האפשר. אחרי קבלת תשובה או כשהמועד האחרון מגיע, כל התמונות שאפשר לגשת אליהן מוצגות ברשת של כלי לבחירת תמונות, כולל התמונות שמאוחסנות באופן מקומי במכשיר והתמונות שסונכרנו מהענן.
- בזמן שהמשתמש גולל, הכלי לבחירת תמונות מאחזר תמונות ממוזערות של מדיה מספק המדיה בענן כדי להציג אותן בממשק המשתמש.
- כשהמשתמש מסיים את הפעילות והתוצאות כוללות קובץ מדיה בענן, כלי לבחירת תמונות מבקש תיאורי קבצים לתוכן, יוצר URI ומעניק גישה לקובץ לאפליקציה שקוראת לו.
- עכשיו האפליקציה יכולה לפתוח את ה-URI ויש לה גישה לקריאה בלבד לתוכן המדיה. כברירת מחדל, מטא-נתונים רגישים מצונזרים. כדי לתאם את חילופי הנתונים בין אפליקציית Android לבין ספק המדיה בענן, כלי בחירת התמונות משתמש במערכת הקבצים FUSE.
בעיות נפוצות
ריכזנו כאן כמה נקודות חשובות שכדאי לזכור כשמתכננים את ההטמעה:
איך להימנע מקבצים כפולים
מכיוון שאין דרך לבדוק את מצב המדיה בענן בכלי לבחירת תמונות ב-Android, CloudMediaProvider צריך לספק את MEDIA_STORE_URI בשורת הסמן של כל קובץ שקיים גם בענן וגם במכשיר המקומי. אחרת, המשתמש יראה קבצים כפולים בכלי לבחירת תמונות.
אופטימיזציה של גדלי התמונות לתצוגה מקדימה
חשוב מאוד שהקובץ שמוחזר מ-onOpenPreview לא יהיה התמונה ברזולוציה מלאה, ושהוא יתאים ל-Size שמבוקש. תמונה גדולה מדי תגרום לזמני טעינה ארוכים בממשק המשתמש, ותמונה קטנה מדי עלולה להיות מפוקסלת או מטושטשת בהתאם לגודל המסך של המכשיר.
טיפול בכיוון הנכון
אם התמונות הממוזערות שמוחזרות בפונקציה onOpenPreview לא מכילות את נתוני ה-EXIF שלהן, צריך להחזיר אותן בכיוון הנכון כדי למנוע סיבוב שגוי של התמונות הממוזערות ברשת התצוגה המקדימה.
מניעת גישה לא מורשית
בודקים אם יש MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION לפני שמחזירים נתונים למתקשר מ-ContentProvider. כך אפליקציות לא מורשות לא יוכלו לגשת לנתונים בענן.
המחלקות CloudMediaProvider
המחלקה CloudMediaProvider, שנגזרת מ-android.content.ContentProvider, כוללת methods כמו אלה שמוצגים בדוגמה הבאה:
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);
}
המחלקה CloudMediaProviderContract
בנוסף למחלקת ההטמעה הראשית CloudMediaProvider, כלי בחירת התמונות ל-Android כולל את המחלקה CloudMediaProviderContract.
בשיעור הזה נסביר על יכולת הפעולה ההדדית בין כלי לבחירת תמונות לבין ספק המדיה בענן, כולל היבטים כמו MediaCollectionInfo לפעולות סנכרון, עמודות Cursor צפויות ותוספות 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
מערכת ההפעלה משתמשת בשיטה onGetMediaCollectionInfo() כדי להעריך את התוקף של פריטי המדיה בענן ששמורים במטמון, וכדי לקבוע את הסנכרון הנדרש עם ספק המדיה בענן. בגלל הפוטנציאל לקריאות תכופות של מערכת ההפעלה, onGetMediaCollectionInfo() נחשב קריטי לביצועים. חשוב להימנע מפעולות ארוכות או מתופעות לוואי שעלולות להשפיע לרעה על הביצועים. מערכת ההפעלה שומרת במטמון את התשובות הקודמות מהשיטה הזו ומשווה אותן לתשובות הבאות כדי לקבוע את הפעולות המתאימות.
Kotlin
abstract fun onGetMediaCollectionInfo(extras: Bundle): Bundle
Java
@NonNull
public abstract Bundle onGetMediaCollectionInfo(@NonNull Bundle extras);
חבילת MediaCollectionInfo שמוחזרת כוללת את הקבועים הבאים:
onQueryMedia
השיטה onQueryMedia() משמשת לאכלוס רשת התמונות הראשית בכלי לבחירת תמונות בתצוגות שונות. יכול להיות שהקריאות האלה רגישות לזמן האחזור, ואפשר לקרוא להן כחלק מסנכרון פרואקטיבי ברקע, או במהלך הפעלת הכלי לבחירת תמונות כשנדרש מצב סנכרון מלא או מצטבר. ממשק המשתמש של כלי לבחירת תמונות לא ימתין לתגובה ללא הגבלת זמן כדי להציג תוצאות, ויכול להיות שהבקשות האלה יפסיקו לפעול בגלל חוסר פעילות, לצורך ממשק המשתמש. הסמן שיוחזר עדיין ינסה לעבור עיבוד במסד הנתונים של בוחר התמונות עבור סשנים עתידיים.
השיטה הזו מחזירה Cursor שמייצג את כל פריטי המדיה באוסף המדיה, מסוננים לפי התוספים שסופקו וממוינים בסדר כרונולוגי הפוך של MediaColumns#DATE_TAKEN_MILLIS (הפריטים האחרונים מוצגים ראשונים).
חבילת CloudMediaProviderContract שמוחזרת כוללת את הקבועים הבאים:
EXTRA_ALBUM_IDEXTRA_LOOPING_PLAYBACK_ENABLEDEXTRA_MEDIA_COLLECTION_IDEXTRA_PAGE_SIZEEXTRA_PAGE_TOKENEXTRA_PREVIEW_THUMBNAILEXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLEDEXTRA_SYNC_GENERATIONMANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSIONPROVIDER_INTERFACE
ספק המדיה בענן צריך להגדיר את CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID כחלק מ-Bundle שמוחזר. אם לא מגדירים את הערך הזה, זו שגיאה והערך Cursor שמוחזר לא תקין. אם ספק המדיה בענן טיפל במסננים כלשהם בתוספים שסופקו, הוא צריך להוסיף את המפתח ל-ContentResolver#EXTRA_HONORED_ARGS כחלק מ-Cursor#setExtras שמוחזר.
onQueryDeletedMedia
השיטה onQueryDeletedMedia() משמשת כדי לוודא שפריטים שנמחקו בחשבון בענן מוסרים בצורה נכונה מממשק המשתמש של כלי לבחירת תמונות. בגלל הרגישות הפוטנציאלית של השיחות האלה לזמן האחזור, הן עשויות להתבצע כחלק מ:
- סנכרון יזום ברקע
- סשנים של כלי לבחירת תמונות (כשנדרש מצב סנכרון מלא או מצטבר)
ממשק המשתמש של בוחר התמונות מתעדף חוויית משתמש רספונסיבית ולא ימתין לתגובה ללא הגבלת זמן. כדי לשמור על אינטראקציות חלקות, יכול להיות שיהיו פסיקות זמן. המערכת עדיין תנסה לעבד כל Cursor שיוחזר ולהוסיף אותו למסד הנתונים של בוחר התמונות לסשנים עתידיים.
השיטה הזו מחזירה Cursor שמייצג את כל קובצי המדיה שנמחקו בכל אוסף המדיה בגרסה הנוכחית של הספק, כפי שמוחזר על ידי onGetMediaCollectionInfo(). אפשר לסנן את הפריטים האלה לפי תוספות.
ספק המדיה בענן צריך להגדיר את CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID כחלק מCursor#setExtras שמוחזר. אם לא מגדירים את הערך הזה, זו שגיאה והיא מבטלת את התוקף של Cursor. אם הספק טיפל במסננים כלשהם בתוספים שסופקו, הוא צריך להוסיף את המפתח ל-ContentResolver#EXTRA_HONORED_ARGS.
onQueryAlbums
השיטה onQueryAlbums() משמשת לאחזור רשימה של אלבומים ב-Cloud שזמינים בספק שירותי הענן, והמטא-נתונים שמשויכים אליהם. פרטים נוספים מופיעים כאן.CloudMediaProviderContract.AlbumColumns
השיטה הזו מחזירה Cursor שמייצג את כל הפריטים באלבום באוסף המדיה. אפשר לסנן את הפריטים לפי התוספים שצוינו ולמיין אותם בסדר כרונולוגי הפוך של AlbumColumns#DATE_TAKEN_MILLIS , כך שהפריטים האחרונים יופיעו ראשונים. ספק המדיה בענן צריך להגדיר את הערך
CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID כחלק מהערך המוחזר
Cursor. אם לא מגדירים את הערך הזה, זו שגיאה והערך Cursor שמוחזר לא תקין. אם הספק טיפל במסננים כלשהם בתוספים שסופקו, הוא צריך להוסיף את המפתח ל-ContentResolver#EXTRA_HONORED_ARGS כחלק מ-Cursor שמוחזר.
onOpenMedia
השיטה onOpenMedia() אמורה להחזיר את המדיה בגודל המלא שזוהתה על ידי mediaId שסופק. אם השיטה הזו חוסמת בזמן הורדת תוכן למכשיר, צריך לבדוק מעת לעת את CancellationSignal שסופק כדי לבטל בקשות שהוזנחו.
onOpenPreview
השיטה onOpenPreview() צריכה להחזיר תמונה ממוזערת של size שסופק לפריט עם mediaId שסופק. התמונה הממוזערת צריכה להיות בפורמט המקורי CloudMediaProviderContract.MediaColumns#MIME_TYPE והרזולוציה שלה צפויה להיות נמוכה בהרבה מהרזולוציה של הפריט שמוחזר על ידי onOpenMedia. אם השיטה הזו חסומה בזמן הורדת תוכן למכשיר, צריך לבדוק מעת לעת את CancellationSignal שסופק כדי לבטל בקשות שהוזנחו.
onCreateCloudMediaSurfaceController
השיטה onCreateCloudMediaSurfaceController() צריכה להחזיר CloudMediaSurfaceController שמשמש לעיבוד התצוגה המקדימה של פריטי מדיה, או null אם עיבוד התצוגה המקדימה לא נתמך.
CloudMediaSurfaceController אחראי לעיבוד התצוגה המקדימה של פריטי מדיה במופעים נתונים של Surface. השיטות של המחלקה הזו אמורות להיות אסינכרוניות, ואסור להן לחסום את הפעולה על ידי ביצוע פעולה כבדה. מופע יחיד של CloudMediaSurfaceController אחראי לעיבוד של כמה פריטי מדיה שמשויכים לכמה פלטפורמות.
CloudMediaSurfaceController תומך ברשימת הפונקציות הבאות להחזרת ערך (callback) של מחזור החיים:
onConfigChangeonDestroyonMediaPauseonMediaPlayonMediaSeekToonPlayerCreateonPlayerReleaseonSurfaceChangedonSurfaceCreatedonSurfaceDestroyed