クラウド メディア プロバイダは、Android
写真選択ツールに追加のクラウド メディア コンテンツを提供します。アプリが ACTION_PICK_IMAGES または ACTION_GET_CONTENT を使用してユーザーにメディア ファイルをリクエストする場合、ユーザーはクラウド メディア プロバイダから提供された写真や動画を選択できます。クラウド メディア
プロバイダは、アルバムに関する情報も提供できます。これは
Android 写真選択ツールで閲覧できます。
始める前に
クラウド メディア プロバイダの構築を始める前に、次の点を考慮してください。
利用資格
Android では、OEM が指定したアプリをクラウド メディア プロバイダにできる試験運用プログラムを実施しています。現時点では、OEM が指定したアプリのみがこのプログラムに参加して Android のクラウド メディア プロバイダになることができます 。各 OEM は最大 3 つのアプリを指定できます。承認されると、これらのアプリはインストールされている GMS Android デバイスでクラウド メディア プロバイダとしてアクセスできるようになります。
Android は、対象となるすべてのクラウド プロバイダのサーバーサイド リストを管理しています。各 OEM は、構成可能なオーバーレイを使用してデフォルトのクラウド プロバイダを選択できます。指定されたアプリは、すべての技術要件を満たし、すべての品質テストに合格する必要があります。OEM クラウド メディア プロバイダの試験運用プログラムのプロセスと 要件について 詳しくは、お問い合わせフォームにご記入ください。
クラウド メディア プロバイダを作成する必要があるかどうかを判断する
クラウド メディア プロバイダは、ユーザーがクラウドから写真や動画をバックアップして取得するための主要なソースとして機能するアプリまたはサービスを想定しています。 アプリに便利なコンテンツのライブラリがあるものの、通常は写真ストレージ ソリューションとして使用されていない場合は、ドキュメント プロバイダの作成を検討してください。
プロファイルごとに 1 つのアクティブなクラウド プロバイダ
Android プロファイルごとに、一度にアクティブにできるクラウド メディア プロバイダは 1 つのみです。ユーザーは、写真選択ツールの設定から、選択したクラウド メディア プロバイダ アプリをいつでも削除または変更できます。
デフォルトでは、Android 写真選択ツールはクラウド プロバイダを自動的に選択しようとします。
- デバイスに対象となるクラウド プロバイダが 1 つしかない場合、そのアプリが現在のプロバイダとして自動的に選択されます。
デバイスに対象となるクラウド プロバイダが複数あり、そのうちの 1 つが OEM が選択したデフォルトと一致する場合、OEM が選択したアプリが選択されます。
デバイスに対象となるクラウド プロバイダが複数あり、そのうちの 1 つも OEM が選択したデフォルトと一致しない場合、アプリは選択されません。
クラウド メディア プロバイダを構築する
次の図は、Android アプリ、Android フォトピッカー、ローカル デバイスの MediaProvider、CloudMediaProvider の間で、写真選択セッションの前と最中に発生するイベントのシーケンスを示しています。
- システムは、ユーザーが優先するクラウド プロバイダを初期化し、メディア メタデータを Android 写真選択ツールのバックエンドに定期的に同期します。
- Android アプリが写真選択ツールを起動すると、写真選択ツールは、ローカルまたはクラウドの統合アイテム グリッドをユーザーに表示する前に、クラウド プロバイダとの間でレイテンシの影響を受けやすい増分同期を実行し、結果が可能な限り最新の状態になるようにします。レスポンスを受信した後、または期限に達すると、写真選択ツール グリッドにアクセス可能なすべての写真が表示されます。これには、デバイスにローカルに保存されている写真と、クラウドから同期された写真が含まれます。
- ユーザーがスクロールすると、写真選択ツールはクラウド メディア プロバイダからメディア サムネイルを取得して UI に表示します。
- ユーザーがセッションを完了し、結果にクラウド メディア アイテムが含まれている場合、写真選択ツールはコンテンツのファイル記述子をリクエストし、URI を生成して、呼び出し元のアプリケーションにファイルへのアクセス権を付与します。
- アプリは URI を開くことができ、メディア コンテンツへの読み取り専用アクセス権を持ちます。デフォルトでは、機密性の高いメタデータは編集されます。写真選択ツールは FUSE ファイル システムを利用して、Android アプリとクラウド メディア プロバイダ間のデータ交換を調整します。
一般的な問題
実装を検討する際は、次の点に注意してください。
重複したファイルを避ける
Android 写真選択ツールにはクラウド メディアの状態を検査する方法がないため、CloudMediaProvider は、クラウドとローカル デバイスの両方に存在するファイルのカーソル行に MEDIA_STORE_URI を提供する必要があります。そうしないと、写真選択ツールに重複したファイルが表示されます。
プレビュー表示用に画像サイズを最適化する
onOpenPreview から返されるファイルは、フル解像度の画像ではなく、リクエストされた Size に準拠していることが非常に重要です。画像が大きすぎると、UI での読み込みに時間がかかります。画像が小さすぎると、デバイスの画面サイズによってはピクセル化されたり、ぼやけたりする可能性があります。
正しい向きを処理する
onOpenPreview で返されるサムネイルに EXIF データが含まれていない場合は、プレビュー グリッドでサムネイルが誤って回転しないように、正しい向きで返す必要があります。
不正アクセスの防止
ContentProvider から呼び出し元にデータを返す前に、MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION を確認します。これにより、不正なアプリがクラウド データにアクセスすることを防ぐことができます。
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 クラス
Android 写真選択ツールには、プライマリ CloudMediaProvider 実装クラスに加えて、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
クラウド メディア プロバイダは、返された Bundle の一部として CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID を設定する必要があります。これを設定しないとエラーになり、返された Cursor が無効になります。クラウド メディア プロバイダが指定されたエクストラでフィルタを処理した場合は、返された Cursor#setExtras の一部として、キーを ContentResolver#EXTRA_HONORED_ARGS に追加する必要があります。
onQueryDeletedMedia
onQueryDeletedMedia() メソッドは、クラウド アカウントで削除されたアイテムが写真選択ツールのユーザー インターフェースから正しく削除されるようにするために使用されます。レイテンシの影響を受けやすいため、これらの呼び出しは次の処理の一部として開始されることがあります。
- バックグラウンドのプロアクティブな同期
- 写真選択ツール セッション(完全同期または増分同期の状態が必要な場合)
フォトピッカーのユーザー インターフェースは、レスポンシブなユーザー エクスペリエンスを優先し、レスポンスを無期限に待機することはありません。スムーズなインタラクションを維持するため、タイムアウトが発生する可能性があります。返された Cursor は、今後のセッションのためにフォトピッカーのデータベースに処理されます。
このメソッドは、onGetMediaCollectionInfo() によって返される現在のプロバイダ バージョン内のメディア コレクション全体で削除されたすべてのメディア アイテムを表す Cursor を返します。これらのアイテムは、必要に応じてエクストラでフィルタできます。
クラウド メディア プロバイダは、返された
Cursor#setExtras の一部として
CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID を設定する必要があります。これを設定しないとエラーになり、Cursor が無効になります。プロバイダが指定されたエクストラでフィルタを処理した場合は、キーを ContentResolver#EXTRA_HONORED_ARGS に追加する必要があります。
onQueryAlbums
onQueryAlbums() メソッドは、クラウド プロバイダで使用可能なクラウド アルバムのリストとその関連メタデータを取得するために使用されます。詳細については、
CloudMediaProviderContract.AlbumColumns をご覧ください。
このメソッドは、メディア コレクション内のすべてのアルバム アイテムを表す Cursor を返します。必要に応じて、指定されたエクストラでフィルタし、AlbumColumns#DATE_TAKEN_MILLIS の逆時系列順(最新のアイテムが最初)で並べ替えます。クラウド メディア プロバイダは、返された Cursor の一部として CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID を設定する必要があります。これを設定しないとエラーになり、返された Cursor が無効になります。プロバイダが指定されたエクストラでフィルタを処理した場合は、返された Cursor の一部として、キーを ContentResolver#EXTRA_HONORED_ARGS に追加する必要があります。
onOpenMedia
onOpenMedia() メソッドは、指定された mediaId で識別されるフルサイズのメディアを返す必要があります。デバイスにコンテンツをダウンロード中にこのメソッドがブロックされた場合は、指定された CancellationSignal を定期的に確認して、破棄されたリクエストを中止する必要があります。
onOpenPreview
onOpenPreview() メソッドは、指定された mediaId のアイテムに対して、指定された
size のサムネイルを返す必要があります。サムネイルは元の CloudMediaProviderContract.MediaColumns#MIME_TYPE であり、onOpenMedia によって返されるアイテムよりも解像度がはるかに低いことが想定されます。デバイスにコンテンツをダウンロード中にこのメソッドがブロックされた場合は、指定された CancellationSignal を定期的に確認して、破棄されたリクエストを中止する必要があります。
onCreateCloudMediaSurfaceController
onCreateCloudMediaSurfaceController() メソッドは、メディア アイテムのプレビューのレンダリングに使用される
CloudMediaSurfaceController を返す必要があります。プレビュー レンダリングが対象外の場合は
null を返します。
CloudMediaSurfaceController は、指定された Surface インスタンスでのメディア アイテムのプレビューのレンダリングを管理します
。このクラスのメソッドは非同期を想定しており、負荷の高いオペレーションを実行してブロックしないでください。1 つの CloudMediaSurfaceController インスタンスは、複数のサーフェスに関連付けられた複数のメディア アイテムのレンダリングを担当します。
CloudMediaSurfaceController は、次のライフサイクル コールバックのリストをサポートしています。
onConfigChangeonDestroyonMediaPauseonMediaPlayonMediaSeekToonPlayerCreateonPlayerReleaseonSurfaceChangedonSurfaceCreatedonSurfaceDestroyed