建立 Android 雲端媒體供應商

雲端媒體供應商會為 Android 裝置提供額外的雲端媒體內容 相片挑選工具。使用者可以選取 應用程式使用 ACTION_PICK_IMAGESACTION_GET_CONTENT:向使用者要求媒體檔案。雲端媒體 提供者也可以提供專輯的相關資訊,您可以在 Android 相片挑選工具。

事前準備

開始建構雲端之前,請將下列項目納入考量 媒體供應商

適用資格

Android 正在執行前測計畫,讓原始設備製造商 (OEM) 提名的應用程式邁向雲端 媒體供應商只有原始設備製造商 (OEM) 提名的應用程式才能參加 。每項 原始設備製造商 (OEM) 最多可以提名 3 個應用程式。經過核准後,這些應用程式便可在 任何具備 GMS Android 裝置的雲端媒體供應商 已安裝。

Android 會維護一份伺服器端清單,列出所有符合資格的雲端服務供應商。所有原始設備製造商 (OEM) 您可以使用可設定的疊加層,選擇預設的雲端服務供應商。已提名 應用程式必須符合所有技術相關規定,並通過所有品質測試。學習 進一步瞭解原始設備製造商 (OEM) 雲端媒體供應商前測計畫的程序 請填寫諮詢表單

判斷是否需要建立雲端媒體供應商

雲端媒體供應商是指以使用者的名義應用程式或服務 備份及擷取雲端中相片和影片的主要來源。 如果應用程式有豐富的實用內容資料庫,但通常不會做為 相片儲存解決方案,請考慮建立文件供應程式

每個設定檔只能有一個有效的雲端服務供應商

每個 Android 裝置最多可以同時設定一個有效的雲端媒體供應商 設定檔。使用者可能會移除或變更所選的雲端媒體供應商 應用程式。

根據預設,Android 相片挑選工具會嘗試選擇雲端服務供應商 。

  • 如果裝置上只有一個符合資格的雲端服務供應商,該應用程式會成為 已自動選取為目前的供應商。
  • 如果裝置上有多個符合資格的雲端服務供應商,以及其中一個 這些項目與原始設備製造商 (OEM) 選擇的預設應用程式相符,系統就會選取原始設備製造商 (OEM) 選擇的應用程式。

  • 如果裝置上有多個符合資格的雲端服務供應商,且沒有任何 這些項目與原始設備製造商 (OEM) 選定的預設值相符,否則系統不會選取任何應用程式。

打造雲端媒體供應商

下圖說明事件發生前和期間發生的事件順序 Android 應用程式、Android 相片挑選工具、 本機裝置的 MediaProvider,以及 CloudMediaProvider

顯示相片挑選工具至雲端媒體供應商流程的循序圖
圖 1: 相片選取工作階段中的事件序列圖表。
  1. 系統會初始化使用者偏好的雲端服務供應商,並定期執行 將媒體中繼資料同步到 Android 相片挑選工具後端。
  2. Android 應用程式啟動相片挑選工具時,會顯示合併的本機檔案 或雲端項目格,而相片挑選工具會執行易受延遲影響。 與雲端供應商逐步同步,確保結果符合現況 。收到回應或超過期限時, 相片挑選工具格狀檢視畫面現在會顯示所有無障礙相片,合併儲存的相片 然後再透過雲端同步處理檔案
  3. 當使用者捲動畫面時,相片挑選工具會從 要在 UI 中顯示的雲端媒體供應商。
  4. 使用者完成工作階段,且結果包含雲端媒體 相片挑選工具會要求內容的檔案描述元,因此會產生 URI,並將檔案存取權授予呼叫應用程式。
  5. 應用程式現在可以開啟 URI,並且具備媒體的唯讀存取權 內容。根據預設,系統會遮蓋敏感的中繼資料。相片挑選工具 會運用 FUSE 檔案系統來協調 Android 應用程式和雲端媒體供應商。

常見問題

考量以下幾點: 實作:

避免重複檔案

由於 Android 相片挑選工具無法檢查雲端媒體狀態 CloudMediaProvider 需要提供遊標中的 MEDIA_STORE_URI 儲存在雲端和本機裝置上的任何檔案資料列,或者 使用者會在相片挑選工具中看到重複的檔案。

針對預覽畫面調整圖片大小

請務必確保 onOpenPreview 傳回的檔案不完整 解析度圖片,並遵循要求的 Size。圖片太大 會在 UI 中產生載入時間,而且如果圖片太小,可能表示有像素化或像素化 會隨著裝置螢幕大小而模糊不清

處理正確的方向

如果 onOpenPreview 中傳回的縮圖不含 EXIF 資料, 應以正確的方向傳回,以免縮圖旋轉 未正確顯示在預覽畫面格狀檢視中

防範未經授權的存取行為

將資料傳回至MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION ContentProvider 的呼叫端。這可防止未經授權的應用程式 存取雲端資料

CloudMediaProvider 類別

衍生自 android.content.ContentProviderCloudMediaProvider 類別包含的方法,如以下範例所示:

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 組合包含以下內容 常數:

雲端媒體供應商必須設定 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 失效。如果 供應器處理了所提供額外項目中的任何篩選器,則必須將鍵新增至 傳回 Cursor 中的 ContentResolver#EXTRA_HONORED_ARGS

onOpenMedia

onOpenMedia() 方法應會傳回由 提供的 mediaId。如果這個方法在將內容下載至 裝置,建議您定期檢查所提供的 CancellationSignal 以取消 以及放棄的請求

onOpenPreview

onOpenPreview() 方法應傳回提供的縮圖 size 代表所提供 mediaId 的項目。縮圖應該在 原始 CloudMediaProviderContract.MediaColumns#MIME_TYPE,預計在此值中 的解析度遠低於 onOpenMedia 傳回的項目。如果這個方法 將內容下載到裝置時遭到封鎖,建議您定期 查看提供的 CancellationSignal,取消已捨棄的要求。

onCreateCloudMediaSurfaceController

onCreateCloudMediaSurfaceController() 方法應會傳回 用於轉譯媒體項目預覽的 CloudMediaSurfaceController,或 null (如果不支援預覽顯示功能)。

CloudMediaSurfaceController 會管理媒體項目預覽的算繪作業 並在指定的 Surface 執行個體上發出這個類別的方法旨在 而且不應因執行任何繁重作業而遭到封鎖單一 CloudMediaSurfaceController 執行個體負責轉譯多個 與多個平台相關聯的媒體項目

CloudMediaSurfaceController支援下列清單: 生命週期回呼: