Поставщик облачных медиафайлов предоставляет дополнительный контент из облачного хранилища для выбора фотографий в Android . Пользователи могут выбирать фотографии или видео, предоставленные поставщиком облачных медиафайлов, когда приложение использует ACTION_PICK_IMAGES или ACTION_GET_CONTENT для запроса медиафайлов у пользователя. Поставщик облачных медиафайлов также может предоставлять информацию об альбомах , которые можно просматривать в выборе фотографий Android.
Прежде чем начать
Перед началом создания облачного медиапровайдера примите во внимание следующие моменты.
Критерии отбора
Android запускает пилотную программу, позволяющую приложениям, предложенным производителями оборудования (OEM), стать поставщиками облачных медиафайлов. На данный момент в этой программе могут участвовать только приложения, предложенные производителями оборудования . Каждый производитель оборудования может предложить до 3 приложений. После одобрения эти приложения станут доступны в качестве поставщиков облачных медиафайлов на любом устройстве Android с поддержкой GMS, на котором они установлены.
Android поддерживает на стороне сервера список всех подходящих облачных провайдеров. Каждый OEM-производитель может выбрать облачного провайдера по умолчанию с помощью настраиваемого оверлея . Номинированные приложения должны соответствовать всем техническим требованиям и пройти все тесты качества. Чтобы узнать больше о процессе и требованиях пилотной программы OEM-производителей облачных медиа-провайдеров, заполните форму запроса .
Решите, нужно ли вам создавать облачного медиапровайдера.
Облачные медиа-провайдеры предназначены для использования в качестве приложений или сервисов, которые выступают в качестве основного источника резервного копирования и восстановления фотографий и видео из облака. Если ваше приложение содержит библиотеку полезного контента, но обычно не используется в качестве решения для хранения фотографий, вам следует рассмотреть возможность создания вместо него поставщика документов .
Один активный облачный провайдер на профиль
Для каждого профиля Android может быть активен не более одного облачного медиапровайдера одновременно. Пользователи могут в любое время удалить или изменить выбранное приложение облачного медиапровайдера в настройках выбора фотографий.
По умолчанию средство выбора фотографий в Android попытается автоматически выбрать поставщика облачных услуг.
- Если на устройстве установлен только один подходящий облачный провайдер, это приложение будет автоматически выбрано в качестве текущего провайдера.
Если на устройстве установлено несколько подходящих облачных провайдеров, и хотя бы один из них совпадает с выбранным производителем приложением по умолчанию, то будет выбрано приложение, выбранное производителем.
Если на устройстве установлено более одного подходящего облачного провайдера, и ни один из них не соответствует выбранному производителем по умолчанию, приложение не будет выбрано.
Создайте своего поставщика облачных медиафайлов.
На следующей диаграмме показана последовательность событий до и во время сеанса выбора фотографий между приложением Android, средством выбора фотографий Android, локальным MediaProvider и CloudMediaProvider .
- Система инициализирует выбранного пользователем облачного провайдера и периодически синхронизирует метаданные медиафайлов с бэкэндом приложения для выбора фотографий в Android.
- Когда приложение для Android запускает средство выбора фотографий, перед отображением пользователю объединенной сетки локальных или облачных элементов, средство выбора фотографий выполняет синхронизацию с облачным провайдером с учетом задержки, чтобы гарантировать максимальную актуальность результатов. После получения ответа или по истечении крайнего срока сетка средства выбора фотографий отображает все доступные фотографии, объединяя фотографии, хранящиеся локально на вашем устройстве, с фотографиями, синхронизированными из облака.
- Пока пользователь прокручивает страницу, средство выбора фотографий получает миниатюры медиафайлов из облачного хранилища и отображает их в пользовательском интерфейсе.
- Когда пользователь завершает сессию и результаты включают медиафайл из облачного хранилища, средство выбора фотографий запрашивает дескрипторы файлов для этого контента, генерирует URI и предоставляет доступ к файлу вызывающему приложению.
- Теперь приложение может открывать URI и имеет доступ только для чтения к содержимому медиафайлов. По умолчанию конфиденциальные метаданные скрыты. Функция выбора фотографий использует файловую систему FUSE для координации обмена данными между приложением Android и облачным медиапровайдером.
Общие проблемы
Вот несколько важных моментов, которые следует учитывать при планировании внедрения:
Избегайте дублирования файлов.
Поскольку средство выбора фотографий в Android не имеет возможности проверять состояние медиафайлов в облаке, CloudMediaProvider должен указывать MEDIA_STORE_URI в строке курсора для любого файла, который существует как в облаке, так и на локальном устройстве, иначе пользователь увидит дубликаты файлов в средстве выбора фотографий.
Оптимизируйте размеры изображений для предварительного просмотра.
Очень важно, чтобы файл, возвращаемый функцией onOpenPreview не был изображением в полном разрешении, а соответствовал запрошенному Size . Слишком большое изображение приведет к увеличению времени загрузки в пользовательском интерфейсе, а слишком маленькое может быть пикселизированным или размытым в зависимости от размера экрана устройства.
Обработайте в правильной ориентации.
Если миниатюры, возвращаемые в onOpenPreview не содержат EXIF-данных, их следует возвращать в правильной ориентации, чтобы избежать неправильного поворота миниатюр в сетке предварительного просмотра.
Предотвратить несанкционированный доступ
Перед возвратом данных из ContentProvider вызывающей стороне проверьте наличие разрешения MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION . Это предотвратит несанкционированный доступ приложений к облачным данным.
Класс CloudMediaProvider
Класс CloudMediaProvider , являющийся производным от android.content.ContentProvider , включает методы, подобные тем, что показаны в следующем примере:
Котлин
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 .
Котлин
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() считается критически важным для производительности; он необходим для предотвращения длительных операций или побочных эффектов, которые могут негативно повлиять на производительность. Операционная система кэширует предыдущие ответы этого метода и сравнивает их с последующими ответами для определения соответствующих действий.
Котлин
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() . Эти элементы могут быть дополнительно отфильтрованы по параметрам extras. Поставщик облачных медиафайлов должен установить CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID в качестве части возвращаемого Cursor#setExtras Отсутствие этого параметра является ошибкой и делает Cursor недействительным. Если поставщик обрабатывал какие-либо фильтры в предоставленных параметрах extras, он должен добавить этот ключ в ContentResolver#EXTRA_HONORED_ARGS .
onQueryAlbums
Метод onQueryAlbums() используется для получения списка альбомов Cloud, доступных у данного облачного провайдера, и связанных с ними метаданных. Дополнительные сведения см. в CloudMediaProviderContract.AlbumColumns .
Этот метод возвращает Cursor , представляющий все элементы альбома в медиаколлекции, которые при необходимости фильтруются с помощью предоставленных дополнительных параметров и сортируются в обратном хронологическом порядке AlbumColumns#DATE_TAKEN_MILLIS , начиная с самых последних элементов. Поставщик облачных медиафайлов должен установить CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID в качестве части возвращаемого Cursor . Отсутствие этого параметра является ошибкой и делает возвращаемый Cursor недействительным. Если поставщик обрабатывал какие-либо фильтры в предоставленных дополнительных параметрах, он должен добавить ключ в 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 отвечает за отображение нескольких медиафайлов, связанных с несколькими поверхностями.
Контроллер CloudMediaSurfaceController поддерживает следующий список обратных вызовов жизненного цикла:
-
onConfigChange -
onDestroy -
onMediaPause -
onMediaPlay -
onMediaSeekTo -
onPlayerCreate -
onPlayerRelease -
onSurfaceChanged -
onSurfaceCreated -
onSurfaceDestroyed