Criar um provedor de mídia em nuvem para Android

Um provedor de mídia em nuvem fornece conteúdo de mídia em nuvem adicional para o Android seletor de fotos. 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. Uma instância de mídia em nuvem provedor também pode dar informações sobre álbuns, que podem ser procurados no Seletor de fotos do Android.

Antes de começar

Considere os seguintes itens antes de começar a criar sua nuvem provedor de mídia.

Qualificação

O Android está executando um programa piloto para permitir que apps indicados pelo OEM se tornem a nuvem provedores de mídia. Somente apps indicados por OEMs estão qualificados para participar. este programa para se tornar um provedor de mídia em nuvem para Android neste momento. Cada O OEM pode indicar até três apps. Depois de aprovados, esses apps ficam acessíveis como provedores de mídia em nuvem em qualquer dispositivo com tecnologia Android que use GMS nos quais estejam instalado.

O Android mantém uma lista do lado do servidor de todos os provedores de nuvem qualificados. Cada OEM É possível escolher um provedor de nuvem padrão usando uma sobreposição configurável. Indicados os apps precisam atender a todos os requisitos técnicos e passar em todos os testes de qualidade. Para saber mais sobre o processo do programa piloto do provedor de mídia em nuvem OEM e requisitos, preencha o formulário de consulta.

Decida se você precisa criar um provedor de mídia em nuvem

Os provedores de mídia em nuvem são aplicativos ou serviços que atuam como um fonte principal para fazer backup e recuperar fotos e vídeos da nuvem. Caso seu app tenha uma biblioteca de conteúdo útil, mas ela normalmente não é usada como de armazenamento de fotos, considere criar um provedor de documentos como alternativa.

Um provedor de nuvem ativo por perfil

Pode haver no máximo um provedor de mídia em nuvem ativo por vez para cada serviço Android de usuário. Os usuários podem remover ou mudar o provedor de mídia em nuvem selecionado aplicativo 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, esse app como o provedor atual automaticamente.
  • Se houver mais de um provedor de nuvem qualificado no dispositivo e um dos eles corresponderem 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 dos eles corresponderem ao padrão escolhido pelo OEM, nenhum app será selecionado.

Crie 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, a o MediaProvider do dispositivo local e um CloudMediaProvider.

Diagrama de sequência mostrando o fluxo do seletor de fotos para um provedor de mídia em nuvem
Figura 1: Diagrama de sequência de eventos durante uma sessão de seleção de fotos.
  1. O sistema inicializa o provedor de nuvem preferido do usuário e periodicamente sincroniza metadados de mídia com o back-end do seletor de fotos do Android.
  2. Quando um app Android inicia o seletor de fotos antes de mostrar um local mesclado. ou grade de itens da nuvem para o usuário, o seletor de fotos faz uma verificação sincronização incremental com o provedor de nuvem para garantir que os resultados sejam atualizados possível. Depois de receber uma resposta ou quando o prazo for atingido, o a grade do seletor de fotos agora mostra todas as fotos acessíveis, combinando as armazenadas localmente em seu dispositivo com aqueles sincronizados a partir da nuvem.
  3. Enquanto o usuário rola, o seletor de fotos busca miniaturas de mídia do provedor de mídia em nuvem seja exibido na interface.
  4. Quando o usuário conclui a sessão e os resultados incluem uma mídia da nuvem item, o seletor de fotos solicita descritores de arquivo para o conteúdo, gera uma URI e concede acesso ao arquivo para o aplicativo de chamada.
  5. O app agora pode abrir o URI e tem acesso somente leitura à mídia conteúdo. 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 os app Android e o provedor de mídia em nuvem.

Problemas comuns

Aqui estão algumas considerações importantes para considerar ao 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 no cursor. de qualquer arquivo que exista na nuvem e no dispositivo local, ou a o usuário vai encontrar 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 o imagem com resolução melhor e segue a Size solicitada. Imagem muito grande levará a tempos de carregamento na interface, e uma imagem muito pequena poderá ficar pixelada desfocadas com base no tamanho da tela do dispositivo.

Processar a orientação correta

Se as miniaturas retornadas no onOpenPreview não tiverem os dados EXIF, elas devem ser retornados 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 os dados ao o autor da chamada do ContentProvider. Isso vai impedir que apps não autorizados e acessar dados na nuvem.

A classe CloudMediaProvider

Derivado de android.content.ContentProvider, o CloudMediaProvider inclui métodos como os mostrados no exemplo abaixo:

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 CloudMediaProviderProvider

Além da classe de implementação principal CloudMediaProvider, a O seletor de fotos do Android incorpora uma classe CloudMediaProviderContract. Esta aula mostra a interoperabilidade entre o seletor de fotos e a nuvem provedor de mídia, abrangendo aspectos como MediaCollectionInfo para operações de sincronização, colunas Cursor antecipadas 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 com o provedor de mídia em nuvem. Devido ao potencial de erros frequentes chamadas pelo sistema operacional, onGetMediaCollectionInfo() é considerada essencial para o desempenho, é crucial evitar operações de longa duração ou lado que podem afetar negativamente o desempenho. O sistema operacional armazena em cache respostas anteriores desse método e as compara com as 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 em o seletor de fotos em diversas visualizações. Essas chamadas podem ser sensíveis à latência e pode ser chamado como parte de uma sincronização proativa em segundo plano ou durante o seletor de fotos sessões quando um estado de sincronização completo ou incremental é necessário. O seletor de fotos interface do usuário não espera indefinidamente por uma resposta exibir resultados e pode definir o tempo limite dessas solicitações para fins de interface do usuário. O cursor retornado ainda tentará ser processada no banco de dados do seletor de fotos para de conteúdo.

Esse método retorna um Cursor que representa todos os itens de mídia na mídia coleção opcionalmente filtrada pelos extras fornecidos e ordenada ao contrário ordem cronológica de MediaColumns#DATE_TAKEN_MILLIS (itens mais recentes) primeiro).

O pacote CloudMediaProviderContract retornado inclui o seguinte: constantes:

O provedor de mídia em nuvem precisa definir CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte da Bundle. Não definir isso é um erro e invalida o Cursor retornado. Se o provedor de mídia em nuvem manipulou todos os filtros nos extras fornecidos, deve adicionar a chave para o ContentResolver#EXTRA_HONORED_ARGS como parte da Cursor#setExtras.

onQueryExcluídoMedia

O método onQueryDeletedMedia() é usado para garantir que os itens excluídos na Google Cloud são removidas corretamente da interface do usuário do seletor de fotos. Devido a a 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 esperará indefinidamente por uma resposta. Para manter interações suaves, de tempo limite podem ocorrer. O 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 da toda a coleção de mídia na versão atual do provedor, conforme retornado por onGetMediaCollectionInfo(). Esses itens podem ser filtrados opcionalmente por extras. O provedor de mídia em nuvem precisa definir CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte da Cursor#setExtras Não definir isso é um erro e invalida o Cursor. Se o provedor manipulou todos os filtros nos extras fornecidos, deverá adicionar a chave ao o ContentResolver#EXTRA_HONORED_ARGS.

onQueryAlbums

O método onQueryAlbums() é usado para buscar uma lista de álbuns do Cloud que 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 mídia coleção opcionalmente filtrada pelos extras fornecidos e ordenada ao contrário ordem cronológica de AlbumColumns#DATE_TAKEN_MILLIS , itens mais recentes primeiro. O provedor de mídia em nuvem precisa definir CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID como parte da Cursor. Não definir isso é um erro e invalida o Cursor retornado. Se o provedor manipulou todos os filtros nos extras fornecidos, deverá adicionar a chave ao o ContentResolver#EXTRA_HONORED_ARGS como parte do Cursor retornado.

onOpenMedia

O método onOpenMedia() deve retornar a mídia em tamanho original identificada por o mediaId fornecido. Se esse método bloquear o download de conteúdo para o dispositivo, verifique periodicamente o CancellationSignal fornecido para cancelar abandonados.

onOpenPreview

O método onOpenPreview() precisa retornar uma miniatura do size para o item do mediaId fornecido. A miniatura deve estar no CloudMediaProviderContract.MediaColumns#MIME_TYPE original e espera-se que ter uma resolução muito menor do que o item retornado por onOpenMedia. Se esse método está bloqueada durante o download de conteúdo para o dispositivo, verifique periodicamente verifica o CancellationSignal fornecido para cancelar solicitações abandonadas.

onCreateCloudMediaSurfaceController

O método onCreateCloudMediaSurfaceController() retorna uma CloudMediaSurfaceController usado para renderizar a visualização de itens de mídia ou null se a renderização da visualização não for compatível.

O CloudMediaSurfaceController gerencia a renderização da visualização dos itens de mídia em determinadas instâncias de Surface. Os métodos dessa classe precisam ser assíncronas e não devem bloquear por meio de operações pesadas. Um único A instância CloudMediaSurfaceController é responsável por renderizar vários itens de mídia associados a várias plataformas.

O CloudMediaSurfaceController é compatível com a seguinte lista de callbacks do ciclo de vida: