Um provedor de mídia em nuvem oferece mais conteúdo de mídia em nuvem para o seletor de fotos do
Android. Os usuários podem selecionar fotos ou vídeos fornecidos pelo
provedor de mídia em 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 fornecer informações sobre álbuns, que podem ser navegados no
seletor de fotos do Android.
Antes de começar
Considere os itens a seguir antes de começar a criar seu provedor de mídia em nuvem.
Qualificação
O Android está executando um programa piloto para permitir que apps indicados pelo OEM se tornem provedores de mídia em nuvem. No momento, somente os apps indicados por OEMs estão qualificados para participar desse programa e se tornarem provedores de mídia em 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 com tecnologia GMS Android em que estão 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 provedor de mídia em nuvem do OEM, preencha o formulário de consulta.
Decida se é necessário criar um provedor de mídia em nuvem
Os provedores de mídia em nuvem destinam-se a 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 tem uma biblioteca de conteúdo útil, mas normalmente não é usada como uma solução de armazenamento de fotos, crie um provedor de documentos.
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 do 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 tentará escolher um provedor de nuvem automaticamente.
- Se houver apenas um provedor de nuvem qualificado no dispositivo, esse app será selecionado como o provedor atual automaticamente.
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.
Crie seu provedor de mídia na 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, a
MediaProvider
do dispositivo local e uma CloudMediaProvider
.
- O sistema inicializa o provedor de nuvem preferido do usuário e sincroniza periodicamente os metadados de mídia no 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 da nuvem mesclados ao usuário, o seletor executa uma sincronização incremental sensível à latência com o provedor de nuvem para garantir que os resultados estejam os mais atualizados possível. Depois de receber uma resposta, ou quando o prazo for atingido, a grade do seletor de fotos vai mostrar todas as fotos acessíveis, combinando aquelas armazenadas localmente no seu dispositivo com as sincronizadas na nuvem.
- Enquanto o usuário rola a tela, o seletor de fotos busca miniaturas de mídia do provedor de mídia da nuvem para exibição na interface.
- Quando o usuário conclui a sessão e os resultados incluem um item de mídia em nuvem, o seletor de fotos solicita descritores de arquivo para o conteúdo, gera um URI e concede acesso ao arquivo ao aplicativo que fez a chamada.
- Agora, o app poderá abrir o URI e terá acesso somente leitura ao conteúdo da mídia. Por padrão, os metadados confidenciais são editados. O seletor de fotos aproveita o sistema de arquivos FUSE para coordenar a troca de dados entre o app Android e o provedor de mídia em nuvem.
Problemas comuns
Veja algumas considerações importantes a serem consideradas ao considerar sua implementação:
Evite arquivos duplicados
Como o seletor de fotos do Android não tem como inspecionar o estado da mídia na nuvem,
a CloudMediaProvider
precisa fornecer a MEDIA_STORE_URI
na linha do
cursor de qualquer arquivo que exista na nuvem e no dispositivo local, ou o
usuário 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 máxima e cumpra o Size
solicitado. Uma imagem muito grande
gera tempos de carregamento na interface e uma imagem muito pequena pode ficar pixelada ou
desfocada com base no tamanho da tela do dispositivo.
Processar a orientação correta
Se as miniaturas retornadas em onOpenPreview
não contiverem dados EXIF, elas
precisarão ser retornadas na orientação correta para evitar que as miniaturas sejam giradas
incorretamente na grade de visualização.
Impedir acessos não autorizados
Verifique o MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION
antes de retornar dados ao
autor da chamada do ContentProvider. Isso impedirá que apps não autorizados acessem dados na nuvem.
A classe CloudMediaProvider
Derivada do 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 principal CloudMediaProvider
, o
seletor de fotos do Android incorpora uma classe CloudMediaProviderContract
.
Esta classe descreve a interoperabilidade entre o seletor de fotos e o provedor de mídia
em nuvem, abrangendo aspectos como MediaCollectionInfo
para
operações de sincronização, colunas Cursor
previstas e extras de 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 em nuvem. Devido ao potencial de chamadas frequentes
do sistema operacional, o método onGetMediaCollectionInfo()
é considerado essencial para o desempenho. É fundamental evitar operações de longa duração ou efeitos
colaterais que possam afetar negativamente o desempenho. O sistema operacional armazena em cache as respostas anteriores desse método e as compara com respostas subsequentes para determinar as ações apropriadas.
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 uma resposta para exibir resultados e
pode expirar essas solicitações para fins da 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, opcionalmente filtrado pelos extras fornecidos e classificados em ordem cronológica
inversa de MediaColumns#DATE_TAKEN_MILLIS
(os itens mais recentes
primeiro).
O pacote CloudMediaProviderContract
retornado inclui as seguintes
constantes:
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
O provedor de mídia em nuvem precisa definir
CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID
como parte da
Bundle
retornada. Não definir isso é um erro e invalida o Cursor
retornado. Se
o provedor de mídia em nuvem processou algum filtro nos extras fornecidos, ele precisa adicionar
a chave ao ContentResolver#EXTRA_HONORED_ARGS
como parte da Cursor#setExtras
retornada.
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 à
potencial 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 completo ou incremental é necessário)
A interface do usuário do seletor de fotos prioriza uma experiência do usuário responsiva e
não espera indefinidamente por uma resposta. Para manter interações suaves,
tempos limite podem ocorrer. As Cursor
retornadas ainda tentarão ser processadas
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 em
toda a coleção de mídia dentro da versão atual do provedor, conforme retornado por
onGetMediaCollectionInfo()
. Esses itens podem ser filtrados por extras.
O provedor de mídia em nuvem precisa definir o
CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID
como parte da mensagem
Cursor#setExtras
retornada. Não definir isso é um erro e invalida o Cursor
. Se
o provedor processou algum filtro 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 a eles. Consulte
CloudMediaProviderContract.AlbumColumns
para mais detalhes.
Esse método retorna um Cursor
que representa todos os itens de álbum na coleção de mídia, opcionalmente filtrado pelos extras fornecidos e classificados em ordem cronológica inversa de AlbumColumns#DATE_TAKEN_MILLIS
, os itens mais recentes primeiro. O provedor de mídia em nuvem precisa definir o
CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID
como parte da
Cursor
retornada. Não definir isso é um erro e invalida o Cursor
retornado. Se
o provedor processou algum filtro 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 original identificada pelo
mediaId
fornecido. Se esse método for bloqueado durante o download de conteúdo para o
dispositivo, verifique periodicamente o CancellationSignal
fornecido para cancelar
solicitações abandonadas.
onOpenPreview
O método onOpenPreview()
precisa retornar uma miniatura do
size
fornecido para o item do mediaId informado. A miniatura precisa estar no
CloudMediaProviderContract.MediaColumns#MIME_TYPE
original e precisa ter uma resolução muito menor do que o item retornado por onOpenMedia
. Se esse método
for bloqueado ao fazer o download de conteúdo no dispositivo, verifique periodicamente
o CancellationSignal
fornecido para cancelar solicitações abandonadas.
onCreateCloudMediaSurfaceController
O método onCreateCloudMediaSurfaceController()
retornará um
CloudMediaSurfaceController
usado para renderizar a visualização de itens de mídia ou
null
se a renderização de visualização não tiver suporte.
O CloudMediaSurfaceController
gerencia a renderização da visualização de itens de mídia
em determinadas instâncias de Surface
. Os métodos dessa classe devem ser
assíncronos e não devem ser bloqueados com a realização de operações pesadas. Uma única
instância de CloudMediaSurfaceController
é responsável pela renderização de vários
itens de mídia associados a várias plataformas.
O CloudMediaSurfaceController
oferece suporte à lista de
callbacks do ciclo de vida abaixo:
onConfigChange
onDestroy
onMediaPause
onMediaPlay
onMediaSeekTo
onPlayerCreate
onPlayerRelease
onSurfaceChanged
onSurfaceCreated
onSurfaceDestroyed