雲端媒體供應商會為 Android 裝置提供額外的雲端媒體內容
相片挑選工具。使用者可以選取
應用程式使用 ACTION_PICK_IMAGES
或
ACTION_GET_CONTENT
:向使用者要求媒體檔案。雲端媒體
提供者也可以提供專輯的相關資訊,您可以在
Android 相片挑選工具。
事前準備
開始建構雲端之前,請將下列項目納入考量 媒體供應商
適用資格
Android 正在執行前測計畫,讓原始設備製造商 (OEM) 提名的應用程式邁向雲端 媒體供應商只有原始設備製造商 (OEM) 提名的應用程式才能參加 。每項 原始設備製造商 (OEM) 最多可以提名 3 個應用程式。經過核准後,這些應用程式便可在 任何具備 GMS Android 裝置的雲端媒體供應商 已安裝。
Android 會維護一份伺服器端清單,列出所有符合資格的雲端服務供應商。所有原始設備製造商 (OEM) 您可以使用可設定的疊加層,選擇預設的雲端服務供應商。已提名 應用程式必須符合所有技術相關規定,並通過所有品質測試。學習 進一步瞭解原始設備製造商 (OEM) 雲端媒體供應商前測計畫的程序 請填寫諮詢表單。
判斷是否需要建立雲端媒體供應商
雲端媒體供應商是指以使用者的名義應用程式或服務 備份及擷取雲端中相片和影片的主要來源。 如果應用程式有豐富的實用內容資料庫,但通常不會做為 相片儲存解決方案,請考慮建立文件供應程式 。
每個設定檔只能有一個有效的雲端服務供應商
每個 Android 裝置最多可以同時設定一個有效的雲端媒體供應商 設定檔。使用者可能會移除或變更所選的雲端媒體供應商 應用程式。
根據預設,Android 相片挑選工具會嘗試選擇雲端服務供應商 。
- 如果裝置上只有一個符合資格的雲端服務供應商,該應用程式會成為 已自動選取為目前的供應商。
如果裝置上有多個符合資格的雲端服務供應商,以及其中一個 這些項目與原始設備製造商 (OEM) 選擇的預設應用程式相符,系統就會選取原始設備製造商 (OEM) 選擇的應用程式。
如果裝置上有多個符合資格的雲端服務供應商,且沒有任何 這些項目與原始設備製造商 (OEM) 選定的預設值相符,否則系統不會選取任何應用程式。
打造雲端媒體供應商
下圖說明事件發生前和期間發生的事件順序
Android 應用程式、Android 相片挑選工具、
本機裝置的 MediaProvider
,以及 CloudMediaProvider
。
- 系統會初始化使用者偏好的雲端服務供應商,並定期執行 將媒體中繼資料同步到 Android 相片挑選工具後端。
- Android 應用程式啟動相片挑選工具時,會顯示合併的本機檔案 或雲端項目格,而相片挑選工具會執行易受延遲影響。 與雲端供應商逐步同步,確保結果符合現況 。收到回應或超過期限時, 相片挑選工具格狀檢視畫面現在會顯示所有無障礙相片,合併儲存的相片 然後再透過雲端同步處理檔案
- 當使用者捲動畫面時,相片挑選工具會從 要在 UI 中顯示的雲端媒體供應商。
- 使用者完成工作階段,且結果包含雲端媒體 相片挑選工具會要求內容的檔案描述元,因此會產生 URI,並將檔案存取權授予呼叫應用程式。
- 應用程式現在可以開啟 URI,並且具備媒體的唯讀存取權 內容。根據預設,系統會遮蓋敏感的中繼資料。相片挑選工具 會運用 FUSE 檔案系統來協調 Android 應用程式和雲端媒體供應商。
常見問題
考量以下幾點: 實作:
避免重複檔案
由於 Android 相片挑選工具無法檢查雲端媒體狀態
CloudMediaProvider
需要提供遊標中的 MEDIA_STORE_URI
儲存在雲端和本機裝置上的任何檔案資料列,或者
使用者會在相片挑選工具中看到重複的檔案。
針對預覽畫面調整圖片大小
請務必確保 onOpenPreview
傳回的檔案不完整
解析度圖片,並遵循要求的 Size
。圖片太大
會在 UI 中產生載入時間,而且如果圖片太小,可能表示有像素化或像素化
會隨著裝置螢幕大小而模糊不清
處理正確的方向
如果 onOpenPreview
中傳回的縮圖不含 EXIF 資料,
應以正確的方向傳回,以免縮圖旋轉
未正確顯示在預覽畫面格狀檢視中
防範未經授權的存取行為
將資料傳回至MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION
ContentProvider 的呼叫端。這可防止未經授權的應用程式
存取雲端資料
CloudMediaProvider 類別
衍生自 android.content.ContentProvider
,CloudMediaProvider
類別包含的方法,如以下範例所示:
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_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
雲端媒體供應商必須設定
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
支援下列清單:
生命週期回呼:
onConfigChange
onDestroy
onMediaPause
onMediaPlay
onMediaSeekTo
onPlayerCreate
onPlayerRelease
onSurfaceChanged
onSurfaceCreated
onSurfaceDestroyed