Um provedor de mídia na nuvem oferece conteúdo adicional de mídia na nuvem ao seletor de fotos do Android. Os usuários podem selecionar fotos ou vídeos fornecidos pelo
provedor de mídia na nuvem quando um app usa ACTION_PICK_IMAGES ou
ACTION_GET_CONTENT para solicitar arquivos de mídia do usuário. Um provedor de mídia em nuvem também pode dar informações sobre álbuns, que podem ser navegados no seletor de fotos do Android.
Antes de começar
Considere os seguintes itens antes de começar a criar seu provedor de mídia na nuvem.
Qualificação
O Android está executando um programa piloto para permitir que apps indicados por OEMs se tornem provedores de mídia na nuvem. No momento, apenas os apps indicados por OEMs podem participar desse programa para se tornar um provedor de mídia na nuvem para Android. Cada OEM pode indicar até três apps. Depois de aprovados, esses apps ficam acessíveis como provedores de mídia na nuvem em qualquer dispositivo Android com GMS em que eles estejam instalados.
O Android mantém uma lista do lado do servidor de todos os provedores de nuvem qualificados. Cada OEM pode escolher um provedor de nuvem padrão usando uma sobreposição configurável. Os apps indicados precisam atender a todos os requisitos técnicos e passar em todos os testes de qualidade. Para saber mais sobre o processo e os requisitos do programa piloto de provedores de mídia em nuvem para OEMs, preencha o formulário de consulta.
Decidir se você precisa criar um provedor de mídia em nuvem
Os provedores de mídia em nuvem são apps ou serviços que atuam como a principal fonte dos usuários para fazer backup e recuperar fotos e vídeos da nuvem. Se o app tiver uma biblioteca de conteúdo útil, mas não for usado normalmente como uma solução de armazenamento de fotos, considere criar um provedor de documentos em vez disso.
Um provedor de nuvem ativo por perfil
Pode haver no máximo um provedor de mídia na nuvem ativo por vez para cada perfil do Android. Os usuários podem remover ou mudar o app de provedor de mídia em nuvem selecionado a qualquer momento nas configurações do seletor de fotos.
Por padrão, o seletor de fotos do Android tenta escolher um provedor de nuvem automaticamente.
- Se houver apenas um provedor de nuvem qualificado no dispositivo, ele será selecionado automaticamente como o provedor atual.
Se houver mais de um provedor de nuvem qualificado no dispositivo e um deles corresponder ao padrão escolhido pelo OEM, o app escolhido pelo OEM será selecionado.
Se houver mais de um provedor de nuvem qualificado no dispositivo e nenhum deles corresponder ao padrão escolhido pelo OEM, nenhum app será selecionado.
Criar seu provedor de mídia em nuvem
O diagrama a seguir ilustra a sequência de eventos antes e durante uma sessão de seleção de fotos entre o app Android, o seletor de fotos do Android, o MediaProvider do dispositivo local e um CloudMediaProvider.
- O sistema inicializa o provedor de nuvem preferido do usuário e sincroniza periodicamente os metadados de mídia com o back-end do seletor de fotos do Android.
- Quando um app Android inicia o seletor de fotos, antes de mostrar uma grade de itens locais ou na nuvem mesclados ao usuário, o seletor de fotos realiza uma sincronização incremental sensível à latência com o provedor de nuvem para garantir que os resultados estejam o mais atualizados possível. Depois de receber uma resposta ou quando o prazo é atingido, a grade do seletor de fotos mostra todas as fotos acessíveis, combinando as armazenadas localmente no dispositivo com as sincronizadas da nuvem.
- Enquanto o usuário rola a tela, o seletor de fotos busca miniaturas de mídia do provedor de mídia na nuvem para mostrar na interface.
- Quando o usuário conclui a sessão e os resultados incluem um item de mídia na nuvem, o seletor de fotos solicita descritores de arquivo para o conteúdo, gera um URI e concede acesso ao arquivo para o aplicativo de chamada.
- Agora o app pode abrir o URI e tem acesso somente leitura ao conteúdo de mídia. Por padrão, os metadados sensíveis são editados. O seletor de fotos usa o sistema de arquivos FUSE para coordenar a troca de dados entre o app Android e o provedor de mídia na nuvem.
Problemas comuns
Confira algumas considerações importantes ao pensar na sua implementação:
Evitar arquivos duplicados
Como o seletor de fotos do Android não tem como inspecionar o estado da mídia na nuvem,
o CloudMediaProvider precisa fornecer o MEDIA_STORE_URI na linha
do cursor de qualquer arquivo que exista na nuvem e no dispositivo local. Caso contrário, o
usuário vai ver arquivos duplicados no seletor de fotos.
Otimizar tamanhos de imagem para exibição de visualização
É muito importante que o arquivo retornado de onOpenPreview não seja a imagem de resolução completa e siga o Size solicitado. Uma imagem muito grande
vai gerar tempos de carregamento na interface, e uma imagem muito pequena pode ficar pixelada ou
embaçada com base no tamanho da tela do dispositivo.
Processar a orientação correta
Se as miniaturas retornadas em onOpenPreview não contiverem os dados EXIF, elas
deverão ser retornadas na orientação correta para evitar que sejam giradas
incorretamente na grade de visualização.
Impedir acessos não autorizados
Verifique o MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION antes de retornar dados para
o chamador do ContentProvider. Isso impede que apps não autorizados acessem dados da nuvem.
A classe CloudMediaProvider
Derivada de android.content.ContentProvider, a classe CloudMediaProvider
inclui métodos como os mostrados no exemplo a seguir:
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);
}
A classe CloudMediaProviderContract
Além da classe de implementação CloudMediaProvider principal, o seletor de fotos do Android incorpora uma classe CloudMediaProviderContract.
Essa classe descreve a interoperabilidade entre o seletor de fotos e o provedor de mídia na nuvem, abrangendo aspectos como MediaCollectionInfo para operações de sincronização, colunas Cursor previstas e extras 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
O método onGetMediaCollectionInfo() é usado pelo sistema operacional para
avaliar a validade dos itens de mídia na nuvem armazenados em cache e determinar a
sincronização necessária com o provedor de mídia na nuvem. Devido ao potencial de chamadas frequentes pelo sistema operacional, onGetMediaCollectionInfo() é considerado essencial para o desempenho. É crucial evitar operações de longa duração ou efeitos colaterais que possam afetar negativamente o desempenho. O sistema operacional armazena em cache
respostas anteriores desse método e as compara com respostas subsequentes
para determinar as ações adequadas.
Kotlin
abstract fun onGetMediaCollectionInfo(extras: Bundle): Bundle
Java
@NonNull
public abstract Bundle onGetMediaCollectionInfo(@NonNull Bundle extras);
O pacote MediaCollectionInfo retornado inclui as seguintes constantes:
onQueryMedia
O método onQueryMedia() é usado para preencher a grade de fotos principal no
seletor de fotos em várias visualizações. Essas chamadas podem ser sensíveis à latência e
podem ser chamadas como parte de uma sincronização proativa em segundo plano ou durante sessões
do seletor de fotos quando um estado de sincronização completo ou incremental é necessário. A interface do usuário do seletor de fotos não espera indefinidamente por uma resposta para mostrar resultados e pode atingir o tempo limite dessas solicitações para fins de interface do usuário. O cursor retornado ainda tentará ser processado no banco de dados do seletor de fotos para sessões futuras.
Esse método retorna um Cursor que representa todos os itens de mídia na
coleção de mídia, filtrados opcionalmente pelos extras fornecidos e classificados em ordem
cronológica inversa de MediaColumns#DATE_TAKEN_MILLIS (itens mais recentes
primeiro).
O pacote CloudMediaProviderContract retornado inclui as seguintes constantes:
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
O provedor de mídia na nuvem precisa definir
CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte do
Bundle retornado. Não definir isso é um erro e invalida o Cursor retornado. Se o provedor de mídia na nuvem processou filtros nos extras fornecidos, ele precisa adicionar a chave ao ContentResolver#EXTRA_HONORED_ARGS como parte do Cursor#setExtras retornado.
onQueryDeletedMedia
O método onQueryDeletedMedia() é usado para garantir que os itens excluídos na
conta da nuvem sejam removidos corretamente da interface do usuário do seletor de fotos. Devido à possível sensibilidade à latência, essas chamadas podem ser iniciadas como parte de:
- Sincronização proativa em segundo plano
- Sessões do seletor de fotos (quando um estado de sincronização completa ou incremental é necessário)
A interface do usuário do seletor de fotos prioriza uma experiência responsiva e não espera indefinidamente por uma resposta. Para manter interações fluidas, podem ocorrer tempos limite. Qualquer Cursor retornado ainda vai tentar ser processado
no banco de dados do seletor de fotos para sessões futuras.
Esse método retorna um Cursor que representa todos os itens de mídia excluídos na
coleção de mídia inteira na versão atual do provedor, conforme retornado por
onGetMediaCollectionInfo(). Esses itens podem ser filtrados por extras.
O provedor de mídia na nuvem precisa definir o
CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte do
Cursor#setExtras retornado. Não fazer isso é um erro e invalida o Cursor. Se o provedor processou filtros nos extras fornecidos, ele precisa adicionar a chave ao ContentResolver#EXTRA_HONORED_ARGS.
onQueryAlbums
O método onQueryAlbums() é usado para buscar uma lista de álbuns do Cloud disponíveis no provedor de nuvem e os metadados associados. Consulte
CloudMediaProviderContract.AlbumColumns para mais detalhes.
Esse método retorna um Cursor que representa todos os itens do álbum na
biblioteca de mídia, filtrados opcionalmente pelos extras fornecidos e classificados em ordem
cronológica inversa de AlbumColumns#DATE_TAKEN_MILLIS, com os itens mais recentes
primeiro. O provedor de mídia na nuvem precisa definir o
CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte do
Cursor retornado. Não definir isso é um erro e invalida o Cursor retornado. Se
o provedor processou filtros nos extras fornecidos, ele precisa adicionar a chave ao
ContentResolver#EXTRA_HONORED_ARGS como parte do Cursor retornado.
onOpenMedia
O método onOpenMedia() precisa retornar a mídia de tamanho completo identificada pelo mediaId fornecido. Se esse método bloquear durante o download de conteúdo para o
dispositivo, verifique periodicamente o CancellationSignal fornecido para interromper
solicitações abandonadas.
onOpenPreview
O método onOpenPreview() precisa retornar uma miniatura do size fornecido para o item do mediaId fornecido. A miniatura precisa estar no
CloudMediaProviderContract.MediaColumns#MIME_TYPE original e ter uma resolução
muito menor do que o item retornado por onOpenMedia. Se esse método
for bloqueado durante o download de conteúdo para o dispositivo, verifique periodicamente
o CancellationSignal fornecido para interromper solicitações abandonadas.
onCreateCloudMediaSurfaceController
O método onCreateCloudMediaSurfaceController() precisa retornar um
CloudMediaSurfaceController usado para renderizar a prévia dos itens de mídia ou
null se a renderização da prévia for indisponível.
O CloudMediaSurfaceController gerencia a renderização da prévia de itens de mídia
em determinadas instâncias de Surface. Os métodos dessa classe são assíncronos e não devem ser bloqueados pela execução de operações pesadas. Uma única instância de CloudMediaSurfaceController é responsável por renderizar vários itens de mídia associados a várias superfícies.
O CloudMediaSurfaceController tem suporte para a seguinte lista de
callbacks de ciclo de vida:
onConfigChangeonDestroyonMediaPauseonMediaPlayonMediaSeekToonPlayerCreateonPlayerReleaseonSurfaceChangedonSurfaceCreatedonSurfaceDestroyed