云媒体提供程序可向 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
。
- 系统会初始化用户的首选云服务提供商,并定期 将媒体元数据同步到 Android 照片选择器后端。
- 当 Android 应用启动照片选择器时,在显示合并的局部变量之前 或云项网格,那么照片选择器会执行延迟敏感型 与云服务提供商进行增量同步,以确保结果是最新的 。收到回复后或到达截止时间时, 照片选择器网格现在显示所有可访问的照片,并合并存储的照片 与从云端同步的映像进行比较
- 当用户滚动屏幕时,照片选择器会从 显示在界面中的云媒体提供程序
- 当用户完成会话且结果包含云媒体时 照片选择器请求内容的文件描述符,生成 URI 的 URI,并向调用方应用授予文件访问权限。
- 应用现在能够打开 URI 并拥有媒体的只读权限 内容。默认情况下,敏感元数据会被遮盖。照片选择器 利用 FUSE 文件系统来协调 Android 应用和云媒体提供程序。
常见问题
在评估自己的广告系列时 实现:
避免文件重复
由于 Android 照片选择器无法检查云媒体状态,
CloudMediaProvider
需要在光标中提供 MEDIA_STORE_URI
行,或者
用户在照片选择器中看到重复的文件。
优化预览显示的图片大小
请务必确保从 onOpenPreview
返回的文件不是完整的
分辨率的图片,并且遵循请求的 Size
。图片过大
会导致界面加载时间过长,过小的图片可能会像素化或
使视频模糊不清。
处理正确的屏幕方向
如果在 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
extra。
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()
方法用于填充主照片网格
选择照片选择器。这些调用可能对延迟时间比较敏感,
可以在后台主动同步过程中或在照片选择器期间进行调用
需要完全同步或增量同步状态时,Google Analytics 的会话。照片选择器
界面不会无限期地等待响应显示结果,
出于用户界面目的,这些请求可能会超时返回的游标
仍会尝试处理到照片选择器的数据库中,以供日后使用
会话。
此方法会返回一个 Cursor
,表示媒体中的所有媒体项
按提供的 extra 过滤并反向排序的集合(可选)
按时间顺序 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
失效。如果
云媒体提供商处理了所提供的 extra 中的所有过滤器,它必须添加
作为返回的 ContentResolver#EXTRA_HONORED_ARGS
的一部分的键
Cursor#setExtras
。
onQueryDeletedMedia
onQueryDeletedMedia()
方法用于确保已删除
已从照片选择器界面正确移除。由于
可能的延迟敏感度,那么这些调用可能会作为以下过程的一部分发起:
- 后台主动同步
- 照片选择器会话(需要完全或增量同步状态时)
照片选择器的界面优先考虑自适应的用户体验,
不会无限期地等待响应。为了保持顺畅的互动
可能会发生超时系统仍会尝试处理返回的 Cursor
写入照片选择器的数据库,供将来的会话使用。
此方法会返回一个 Cursor
,它代表
返回
onGetMediaCollectionInfo()
。这些项可以选择按 extra 进行过滤。
云媒体提供商必须将
CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID
(作为返回的部分)
Cursor#setExtras
如果未设置此字段,则会出现错误,并且会使 Cursor
失效。如果
提供程序处理了所提供的 extra 中的所有过滤器,它必须将键添加到
ContentResolver#EXTRA_HONORED_ARGS
。
onQueryAlbums
onQueryAlbums()
方法用于提取云端影集列表,
及其关联的元数据。请参阅
CloudMediaProviderContract.AlbumColumns
,了解更多详情。
此方法会返回一个 Cursor
,表示媒体中的所有影集项
按提供的 extra 过滤并反向排序的集合(可选)
按时间顺序(AlbumColumns#DATE_TAKEN_MILLIS
)从新到旧
。云媒体提供商必须将
CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID
(作为返回的部分)
Cursor
。如果未设置此字段,则会出现错误,并且会使返回的 Cursor
失效。如果
提供程序处理了所提供的 extra 中的所有过滤器,它必须将键添加到
将 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
实例负责渲染多个
与多个 surface 关联的媒体内容。
CloudMediaSurfaceController
支持以下列表:
生命周期回调:
onConfigChange
onDestroy
onMediaPause
onMediaPlay
onMediaSeekTo
onPlayerCreate
onPlayerRelease
onSurfaceChanged
onSurfaceCreated
onSurfaceDestroyed