클라우드 미디어 제공업체는 Android
사진 선택 도구에 추가 클라우드 미디어 콘텐츠를 제공합니다. 앱이 ACTION_PICK_IMAGES 또는 ACTION_GET_CONTENT를 사용하여 사용자에게 미디어 파일을 요청할 때 사용자는 클라우드 미디어 제공업체에서 제공하는 사진 또는 동영상을 선택할 수 있습니다. 클라우드 미디어 제공업체는 Android 사진 선택 도구에서 탐색할 수 있는 앨범에 관한 정보도 제공할 수 있습니다.
시작하기 전에
클라우드 미디어 제공업체 빌드를 시작하기 전에 다음 항목을 고려하세요.
자격 요건
Android는 OEM에서 지정한 앱이 클라우드 미디어 제공업체가 될 수 있도록 허용하는 파일럿 프로그램을 실행하고 있습니다. 현재 OEM에서 지정한 앱만 이 프로그램에 참여하여 Android용 클라우드 미디어 제공업체가 될 수 있습니다. 각 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 데이터가 포함되어 있지 않으면 미리보기 그리드에서 미리보기 이미지가 잘못 회전되지 않도록 올바른 방향으로 반환되어야 합니다.
무단 액세스 방지
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
}
자바
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"
}
}
자바
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
자바
@NonNull
public abstract Bundle onGetMediaCollectionInfo(@NonNull Bundle extras);
반환된 MediaCollectionInfo 번들에는 다음 상수가 포함되어 있습니다.
onQueryMedia
onQueryMedia() 메서드는 다양한 뷰에서
사진 선택 도구의 기본 사진 그리드를 채우는 데 사용됩니다. 이러한 호출은 지연 시간에 민감할 수 있으며 백그라운드 사전 동기화의 일부로 또는 전체 또는 증분 동기화 상태가 필요한 사진 선택 도구 세션 중에 호출될 수 있습니다. 사진 선택 도구 사용자 인터페이스는 결과를 표시하기 위해 응답을 무기한 기다리지 않으며 사용자 인터페이스 목적으로 이러한 요청을 시간 초과할 수 있습니다. 반환된 커서는 향후 세션을 위해 사진 선택 도구의 데이터베이스로 계속 처리하려고 시도합니다.
이 메서드는 제공된 추가 항목으로 선택적으로 필터링되고 MediaColumns#DATE_TAKEN_MILLIS (최신 항목 먼저)의 역시간순으로 정렬된 미디어 컬렉션의 모든 미디어 항목을 나타내는 Cursor를 반환합니다.
반환된 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를 참고하세요.
이 메서드는 제공된 추가 항목으로 선택적으로 필터링되고 AlbumColumns#DATE_TAKEN_MILLIS(최신 항목 먼저)의 역시간순으로 정렬된 미디어 컬렉션의 모든 앨범 항목을 나타내는 Cursor를 반환합니다. 클라우드 미디어 제공업체는 반환된 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 인스턴스에서 미디어 항목의 미리보기를 렌더링하는 것을 관리합니다. 이 클래스의 메서드는 비동기식으로 설계되었으며 과도한 작업을 실행하여 차단해서는 안 됩니다. 단일 CloudMediaSurfaceController 인스턴스는 여러 서페이스와 연결된 여러 미디어 항목을 렌더링합니다.
CloudMediaSurfaceController는 다음 수명 주기 콜백 목록을 지원합니다.
onConfigChangeonDestroyonMediaPauseonMediaPlayonMediaSeekToonPlayerCreateonPlayerReleaseonSurfaceChangedonSurfaceCreatedonSurfaceDestroyed