יצירת ספק מדיה בענן ל-Android

ספק מדיה בענן מספק ל-Android תוכן נוסף של מדיה בענן לבחירת תמונות. המשתמשים יכולים לבחור תמונות או סרטונים שמסופקים על ידי ספק מדיה בענן כשאפליקציה משתמשת ב-ACTION_PICK_IMAGES או ACTION_GET_CONTENT כדי לבקש קובצי מדיה מהמשתמש. מדיה בענן הספק יכול גם לספק מידע על אלבומים, שניתן לעיין בו הכלי לבחירת תמונות ב-Android.

לפני שמתחילים

לפני שמתחילים לבנות את הענן, כדאי לחשוב על הנושאים הבאים ספק מדיה.

מי רשאי להשתמש בפיצ'ר

ב-Android יש תוכנית פיילוט שמאפשרת להפוך אפליקציות שקשורות ל-OEM (יצרן ציוד מקורי) לענן ספקי מדיה שונים. רק אפליקציות שיצרני ציוד מקורי (OEM) זכאים להשתתף בהן התוכנית הזו להפוך לספק מדיה בענן ל-Android כרגע. כל אחד ה-OEM (יצרן הציוד המקורי) יכול להציע עד 3 אפליקציות. לאחר האישור, האפליקציות האלה יהיו נגישות בתור ספקי מדיה בענן בכל מכשיר מבוסס-Android של GMS שבו הם מותקנת.

ב-Android מוצגת רשימה בצד השרת של כל ספקי שירותי הענן שעומדים בדרישות. כל OEM אתם יכולים לבחור ספק ברירת מחדל של שירותי ענן באמצעות שכבת-על שניתן להגדיר. מועמד שהאפליקציות צריכות לעמוד בכל הדרישות הטכניות ולעבור את כל בדיקות האיכות. למידה מידע נוסף על תהליך תוכנית הפיילוט של ספק מדיה בענן ל-OEM (יצרן ציוד מקורי) הדרישות הרלוונטיות, מלאו את טופס הבקשה.

איך להחליט אם אתם צריכים ליצור ספק מדיה בענן

ספקי מדיה בענן מיועדים להיות אפליקציות או שירותים שפועלים כמשתמשים המקור העיקרי לגיבוי ולאחזור תמונות וסרטונים מהענן. אם לאפליקציה יש ספרייה של תוכן שימושי, אבל בדרך כלל היא לא משמשת פתרון לאחסון תמונות, כדאי ליצור ספק מסמכים במקום זאת.

ספק שירותי ענן פעיל אחד לכל פרופיל

יכול להיות לכל היותר ספק פעיל אחד של מדיה בענן בכל רגע נתון לכל Android פרופיל. משתמשים יכולים להסיר או לשנות את ספק המדיה בענן שנבחר בכל שלב דרך ההגדרות של בורר התמונות.

כברירת מחדל, הכלי לבחירת תמונות ב-Android ינסה לבחור ספק שירותי ענן באופן אוטומטי.

  • אם יש במכשיר רק ספק שירותי ענן אחד שעומד בדרישות, האפליקציה הזו הספק הנוכחי נבחר באופן אוטומטי.
  • אם יש במכשיר יותר מספק אחד של שירותי ענן שעומד בדרישות, וגם הם תואמים לברירת המחדל שנבחרה על ידי ה-OEM, האפליקציה שנבחרה על ידי ה-OEM תיבחר.

  • אם יש במכשיר יותר מספק אחד של שירותי ענן שעומד בדרישות, ואף אחד הם תואמים לברירת המחדל של ה-OEM (יצרן הציוד המקורי), לא תיבחר אף אפליקציה.

יצירת ספק מדיה בענן

התרשים הבא מדגים את רצף האירועים גם לפני וגם במהלך סשן בחירת תמונות בין האפליקציה ל-Android, בורר התמונות ב-Android, MediaProvider של המכשיר המקומי, וגם CloudMediaProvider.

תרשים רצף שמציג את התהליך מבורר התמונות לספק מדיה בענן
איור 1: תרשים של רצף אירועים במהלך סשן של בחירת תמונות.
  1. המערכת מפעילה את ספק הענן המועדף על המשתמש ומדי פעם מסנכרן מטא-נתונים של מדיה עם הקצה העורפי של הכלי לבחירת תמונות ב-Android.
  2. כשאפליקציה ל-Android מפעילה את בורר התמונות, לפני הצגה של תמונה מקומית ממוזגת או רשת פריטים בענן למשתמש, הכלי לבחירת תמונות מבצע סנכרון מצטבר עם ספק שירותי הענן כדי להבטיח שהתוצאות עדכניות ככל האפשר. לאחר קבלת תשובה, או כשמגיע המועד האחרון, ברשת של הכלי לבחירת תמונות מוצגות עכשיו כל התמונות שאפשר לגשת אליהן, תוך שילוב של התמונות ששמורות באופן מקומי במכשיר שלך, עם אלו שסונכרנו מהענן.
  3. בזמן שהמשתמש גולל, הכלי לבחירת תמונות מאחזר תמונות ממוזערות של מדיה של ספק המדיה בענן להצגה בממשק המשתמש.
  4. כשהמשתמש משלים את הסשן, והתוצאות כוללות מדיה בענן הפריט, בוחר התמונות מבקש מתארי קבצים עבור התוכן, יוצר URI, ומעניק גישה לקובץ לאפליקציה שמפעילה את הקריאה.
  5. האפליקציה יכולה עכשיו לפתוח את ה-URI ויש לה גישת קריאה בלבד למדיה תוכן. כברירת מחדל, המטא-נתונים הרגישים מצונזרים. הכלי לבחירת תמונות משתמשת במערכת הקבצים של FUSE כדי לתאם את חילופי הנתונים בין אפליקציית Android וספק המדיה בענן.

בעיות נפוצות

הנה כמה שיקולים חשובים שכדאי לזכור כשמביאים בחשבון הטמעה:

נמנעים מכפילויות של קבצים

כי בכלי לבחירת תמונות ב-Android אין דרך לבדוק את מצב המדיה בענן, הפונקציה CloudMediaProvider צריכה לספק את הערך MEDIA_STORE_URI בסמן של כל קובץ שקיים גם בענן וגם במכשיר המקומי, המשתמש יראה קבצים כפולים בכלי לבחירת תמונות.

אופטימיזציה של גודל התמונה לתצוגה המקדימה

חשוב מאוד שהקובץ שמוחזר מ-onOpenPreview אינו מלא ברזולוציה הזו, שעומדת בדרישות של Size. התמונה גדולה מדי יהיו זמני טעינה בממשק המשתמש, והתמונה קטנה מדי עלולה להיות מפוקסלת מטושטשות בהתאם לגודל המסך של המכשיר.

נקודת אחיזה בכיוון הנכון

אם התמונות הממוזערות שמוחזרות בonOpenPreview לא מכילות את נתוני ה-EXIF שלהן, צריך להחזיר את התמונה בכיוון הנכון כדי למנוע סיבוב של התמונות הממוזערות. בתצוגת התצוגה המקדימה.

מניעת גישה לא מורשית

כדאי לבדוק את MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION לפני החזרת הנתונים אל מבצע הקריאה החוזרת מה-ContentProvider. הפעולה הזו תמנע מאפליקציות לא מורשות גישה לנתוני ענן.

המחלקה CloudMediaProvider

נגזר מ-android.content.ContentProvider, CloudMediaProvider class כולל שיטות כמו אלה שמוצגות בדוגמה הבאה:

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

ב-GetMediaCollectionInfo

מערכת ההפעלה משתמשת בשיטה 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 שמוחזרת כוללת את הפריטים הבאים קבועים:

ספק המדיה בענן צריך להגדיר CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID כחלק מההחזרה Bundle. אם לא מגדירים את הפעולה הזו, זו שגיאה ומבטלת את התוקף של Cursor שהוחזר. אם המיקום ספק המדיה בענן טיפל במסננים בכל התוספות שסופקו, הוא חייב להוסיף את המפתח אל ContentResolver#EXTRA_HONORED_ARGS כחלק Cursor#setExtras.

onQuerydeleteMedia

השיטה onQueryDeletedMedia() משמשת כדי לוודא שפריטים נמחקו יוסרו כמו שצריך מממשק המשתמש של הכלי לבחירת תמונות. בעקבות את הרגישות הפוטנציאלית לזמן האחזור, יכול להיות שהקריאות האלה יופעלו כחלק מהגורמים הבאים:

  • סנכרון יזום ברקע
  • סשנים בכלי לבחירת תמונות (כשנדרש מצב סנכרון מלא או מצטבר)

לממשק המשתמש של הכלי לבחירת תמונות יש עדיפות לחוויית משתמש רספונסיבית, לא יחכו לתגובה במשך זמן רב. כדי לשמור על אינטראקציות חלקות, עשויים להיות זמנים קצובים לתפוגה. עדיין יתבצע ניסיון לעבד את כל Cursor שהוחזר למסד הנתונים של הכלי לבחירת תמונות בסשנים עתידיים.

השיטה הזו מחזירה את הערך Cursor שמייצג את כל פריטי המדיה שנמחקו את כל אוסף המדיה בגרסת הספק הנוכחית כפי שהוחזר על ידי onGetMediaCollectionInfo(). אפשר לסנן את הפריטים האלה לפי תוספות. ספק המדיה בענן צריך להגדיר את CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID כחלק מההחזרה Cursor#setExtras אם לא מגדירים את האפשרות הזו, זו שגיאה ומבטלים את התוקף של Cursor. אם המיקום הספק טיפל במסננים בכל התוספות שסופקו, עליו להוסיף את המפתח כדי ContentResolver#EXTRA_HONORED_ARGS.

onQueryAlbums

השיטה onQueryAlbums() משמשת לאחזור רשימה של אלבומי ענן זמינים בספק שירותי הענן, ובמטא-נתונים שמשויכים אליהם. צפייה CloudMediaProviderContract.AlbumColumns אפשר למצוא פרטים נוספים.

השיטה הזו מחזירה את הערך Cursor שמייצג את כל הפריטים באלבום במדיה אופציונלי: סינון לפי התוספות שסופקו ומיון בסדר הפוך סדר כרונולוגי של AlbumColumns#DATE_TAKEN_MILLIS , הפריטים העדכניים ביותר קודם. ספק המדיה בענן צריך להגדיר את CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID כחלק מההחזרה Cursor. אם לא מגדירים את הפעולה הזו, זו שגיאה ומבטלת את התוקף של Cursor שהוחזר. אם המיקום הספק טיפל במסננים בכל התוספות שסופקו, עליו להוסיף את המפתח כדי ContentResolver#EXTRA_HONORED_ARGS כחלק מה-Cursor שהוחזר.

ב-OpenMedia

השיטה onOpenMedia() אמורה להחזיר את המדיה בגודל מלא שזוהה על ידי mediaId שצוין. אם השיטה הזו חוסמת במהלך הורדת תוכן במכשיר, עליך לבדוק מדי פעם את ה-CancellationSignal שסופק כדי לבטל בקשות שננטשו.

ב-OpenPreview

השיטה onOpenPreview() אמורה להחזיר תמונה ממוזערת של size לפריט של מזהה המדיה שצוין. התמונה הממוזערת צריכה להיות הגרסה המקורית של CloudMediaProviderContract.MediaColumns#MIME_TYPE וצפויה תהיה ברזולוציה נמוכה הרבה יותר מאשר הפריט שהוחזר על ידי onOpenMedia. אם השיטה הזו חסום בזמן הורדת תוכן למכשיר, רצוי מדי פעם לבדוק את CancellationSignal שסופקה כדי לבטל בקשות שננטשו.

onCreateCloudMediaSurfaceController

השיטה onCreateCloudMediaSurfaceController() אמורה להחזיר CloudMediaSurfaceController משמש לעיבוד התצוגה המקדימה של פריטי מדיה, או null אם אין תמיכה ברינדור של תצוגה מקדימה.

השדה CloudMediaSurfaceController מנהל את הרינדור של התצוגה המקדימה של פריטי מדיה במופעים נתונים של Surface. השיטות בכיתה הזו נועדו אסינכרונית, ולא תיחסם על ידי ביצוע פעולה עמוסה. סינגל מכונה אחת (CloudMediaSurfaceController) אחראית לעיבוד של מספר מודעות פריטי מדיה שמשויכים למספר פלטפורמות.

CloudMediaSurfaceController תומך ברשימה הבאה של קריאות חוזרות (callback) במחזור החיים: