Implementar um card de mídia no AAOS

Um card de mídia é um ViewGroup independente que mostra metadados de mídia, como o título, a capa do álbum e muito mais, e exibe controles de reprodução, como Reproduzir, Pausar, Pular e até ações personalizadas fornecidas pelo app de mídia de terceiros. Um card de mídia também pode mostrar uma fila de itens de mídia, como uma playlist.

Cartão de mídia

Cartão de mídia

Cartão de mídia

Figura 1. Exemplos de implementação de cards de mídia.

Como os cards de mídia são implementados no AAOS?

Os ViewGroups que mostram informações de mídia observam as atualizações do LiveData do modelo de dados da biblioteca car-media-common, o PlaybackViewModel, para preencher o ViewGroup. Cada atualização do LiveData corresponde a um subconjunto de informações de mídia que mudaram, como MediaItemMetadata, PlaybackStateWrapper e MediaSource.

Como essa abordagem leva a um código repetido (cada app cliente adiciona observadores em cada parte do LiveData e muitas visualizações semelhantes são atribuídas aos dados atualizados), criamos o PlaybackCardController.

PlaybackCardController

O PlaybackCardController foi adicionado à biblioteca car-media-common para ajudar na criação de um card de mídia. Essa é uma classe pública construída com um ViewGroup (mView), PlaybackViewModel (mDataModel), PlaybackCardViewModel (mViewModel) e uma instância MediaItemsRepository (mItemsRepository).

Na função setupController, o ViewGroup é analisado para determinadas visualizações por ID, com mView.findViewById(R.id.xxx) e atribuído a objetos de visualização protegidos.

private void getViewsFromWidget() {
        mTitle = mView.findViewById(R.id.title);
        mAlbumCover = mView.findViewById(R.id.album_art);
        mDescription = mView.findViewById(R.id.album_title);
        mLogo = mView.findViewById(R.id.content_format);

        mAppIcon = mView.findViewById(R.id.media_widget_app_icon);
        mAppName = mView.findViewById(R.id.media_widget_app_name);

         // ...
}

Cada atualização do LiveData do PlaybackViewModel é observada em um método protegido e realiza interações com as visualizações relevantes para os dados recebidos. Por exemplo, um observador em MediaItemMetadata define o título no mTitle TextView e transmite o MediaItemMetadata.ArtworkRef para a arte do álbum ImageBinder mAlbumArtBinder. Se os metadados forem nulos, as visualizações serão ocultas. As subclasses do controlador podem substituir essa lógica, se necessário.

mDataModel.getMetadata().observe(mViewLifecycle, this::updateMetadata);
// ...

/** Update views with {@link MediaItemMetadata} */
protected void updateMetadata(MediaItemMetadata metadata) {
        if (metadata != null) {
            String defaultTitle = mView.getContext().getString(
                    R.string.metadata_default_title);
            updateTextViewAndVisibility(mTitle, metadata.getTitle(),    defaultTitle);
            updateTextViewAndVisibility(mSubtitle, metadata.getSubtitle());
            updateMediaLink(mSubtitleLinker,metadata.getSubtitleLinkMediaId());
            updateTextViewAndVisibility(mDescription, metadata.getDescription());
            updateMediaLink(mDescriptionLinker, metadata.getDescriptionLinkMediaId());
            updateMetadataAlbumCoverArtworkRef(metadata.getArtworkKey());
            updateMetadataLogoWithUri(metadata);
        } else {
            ViewUtils.setVisible(mTitle, false);
            ViewUtils.setVisible(mSubtitle, false);
            ViewUtils.setVisible(mAlbumCover, false);
            ViewUtils.setVisible(mDescription, false);
            ViewUtils.setVisible(mLogo, false);
        }
    }

Estender o PlaybackCardController

Os apps clientes que quiserem criar um card de mídia precisam estender o PlaybackCardController se tiverem mais recursos que gostariam de processar em cada atualização do LiveData. Os clientes atuais no AAOS seguem esse padrão. Primeiro, uma subclasse PlaybackCardController precisa ser criada, como MediaCardController. Em seguida, o MediaCardController precisa adicionar uma classe Builder interna estática que estenda a do PlaybackCardController.

public class MediaCardController extends PlaybackCardController {

    // extra fields specific to MediaCardController

    /** Builder for {@link MediaCardController}. Overrides build() method to
     * return NowPlayingController rather than base {@link PlaybackCardController}
     */
    public static class Builder extends PlaybackCardController.Builder {

        @Override
        public MediaCardController build() {
            MediaCardController controller = new MediaCardController(this);
            controller.setupController();
            return controller;
        }
    }

    public MediaCardController(Builder builder) {
        super(builder);
    // any other function calls needed in constructor
    // ...

  }
}

Instânciar o PlaybackCardController ou uma subclasse

A classe Controller precisa ser instanciada em um fragmento ou atividade para ter um LifecycleOwner para os observadores do LiveData.

mMediaCardController = (MediaCardController) new MediaCardController.Builder()
                    .setModels(mViewModel.getPlaybackViewModel(),
                            mViewModel,
                            mViewModel.getMediaItemsRepository())
                    .setViewGroup((ViewGroup) view)
                    .build();

mViewModel é uma instância da PlaybackCardViewModel (ou subclasse).

PlaybackCardViewModel para salvar o estado

O PlaybackCardViewModel é um ViewModel de salvamento de estado vinculado a um fragmento ou atividade que precisa ser usado para reconstruir o conteúdo do card de mídia caso ocorra uma mudança de configuração, como a troca do tema claro para o escuro quando um usuário passa por um túnel. O PlaybackCardViewModel padrão processa o armazenamento de instâncias de MediaModels para reprodução, de onde o PlaybackViewModel e o MediaItemsRepository podem ser recuperados. Use o PlaybackCardViewModel para rastrear o estado da fila, do histórico e do menu de overflow pelos getters e setters fornecidos.

public class PlaybackCardViewModel extends AndroidViewModel {

    private MediaModels mModels;
    private boolean mNeedsInitialization = true;
    private boolean mQueueVisible = false;
    private boolean mHistoryVisible = false;
    private boolean mOverflowExpanded = false;

    public PlaybackCardViewModel(@NonNull Application application) {
        super(application);
    }

    /** Initialize the PlaybackCardViewModel */
    public void init(MediaModels models) {
        mModels = models;
        mNeedsInitialization = false;
    }

    /**
     * Returns whether the ViewModel needs to be initialized. The ViewModel may
     * need re-initialization if a config change occurs or if the system kills
     * the Fragment.
     */
    public boolean needsInitialization() {
        return mNeedsInitialization;
    }

    public MediaItemsRepository getMediaItemsRepository() {
        return mModels.getMediaItemsRepository();
    }

    public PlaybackViewModel getPlaybackViewModel() {
        return mModels.getPlaybackViewModel();
    }

    public MediaSourceViewModel getMediaSourceViewModel() {
        return mModels.getMediaSourceViewModel();
    }

    public void setQueueVisible(boolean visible) {
        mQueueVisible = visible;
    }

    public boolean getQueueVisible() {
        return mQueueVisible;
    }

    public void setHistoryVisible(boolean visible) {
        mHistoryVisible = visible;
    }

    public boolean getHistoryVisible() {
        return mHistoryVisible;
    }

    public void setOverflowExpanded(boolean expanded) {
        mOverflowExpanded = expanded;
    }

    public boolean getOverflowExpanded() {
        return mOverflowExpanded;
    }
}

Essa classe pode ser estendida se outros estados precisarem ser rastreados.

Mostrar uma fila em um card de mídia

O PlaybackViewModel fornece APIs do LiveData para detectar se o MediaSource oferece suporte a uma fila e para recuperar a lista de objetos MediaItemMetadata na fila. Embora essas APIs possam ser usadas diretamente para preencher um objeto RecyclerView com as informações da fila, uma classe PlaybackQueueController foi adicionada à biblioteca car-media-common para agilizar esse processo. O layout de cada item no CarUiRecyclerView é especificado pelo app cliente e também como um layout de cabeçalho opcional. O app cliente também pode optar por limitar o número de itens mostrados na fila durante o estado de direção com restrições personalizadas de UXR.

O construtor e os setters PlaybackQueueController são mostrados no exemplo a seguir. Os recursos de layout queueResource e headerResource podem ser transmitidos como Resources.ID_NULL se, no primeiro caso, o contêiner já tiver um CarUiRecyclerView com id queue_list e, no segundo caso, a fila não tiver um cabeçalho.

   /**
    * Construct a PlaybackQueueController. If clients don't have a separate
    * layout for the queue, where the queue is already inflated within the
    * container, they should pass {@link Resources.ID_NULL} as the LayoutRes
    * resource. If clients don't require a UxrContentLimiter, they should pass
    * null for uxrContentLimiter and the int passed for uxrConfigurationId will
    * be ignored.
    */
    public PlaybackQueueController(
            ViewGroup container,
            @LayoutRes int queueResource,
            @LayoutRes int queueItemResource,
            @LayoutRes int headerResource,
            LifecycleOwner lifecycleOwner,
            PlaybackViewModel playbackViewModel,
            MediaItemsRepository itemsRepository,
            @Nullable LifeCycleObserverUxrContentLimiter uxrContentLimiter,
            int uxrConfigurationId) {
      // ...
    }

    public void setShowTimeForActiveQueueItem(boolean show) {
        mShowTimeForActiveQueueItem = show;
    }

    public void setShowIconForActiveQueueItem(boolean show) {
        mShowIconForActiveQueueItem = show;
    }

    public void setShowThumbnailForQueueItem(boolean show) {
        mShowThumbnailForQueueItem = show;
    }

    public void setShowSubtitleForQueueItem(boolean show) {
        mShowSubtitleForQueueItem = show;
    }

    /** Calls {@link RecyclerView#setVerticalFadingEdgeEnabled(boolean)} */
    public void setVerticalFadingEdgeLengthEnabled(boolean enabled) {
        mQueue.setVerticalFadingEdgeEnabled(enabled);
    }

    public void setCallback(PlaybackQueueCallback callback) {
        mPlaybackQueueCallback = callback;
    }

O layout de cada item da fila precisa conter os IDs das visualizações que ele quer mostrar e que correspondem aos usados na classe interna QueueViewHolder.

QueueViewHolder(View itemView) {
            super(itemView);
            mView = itemView;
            mThumbnailContainer = itemView.findViewById(R.id.thumbnail_container);
            mThumbnail = itemView.findViewById(R.id.thumbnail);
            mSpacer = itemView.findViewById(R.id.spacer);
            mTitle = itemView.findViewById(R.id.queue_list_item_title);
            mSubtitle = itemView.findViewById(R.id.queue_list_item_subtitle);
            mCurrentTime = itemView.findViewById(R.id.current_time);
            mMaxTime = itemView.findViewById(R.id.max_time);
            mTimeSeparator = itemView.findViewById(R.id.separator);
            mActiveIcon = itemView.findViewById(R.id.now_playing_icon);

            // ...
}

Para mostrar uma fila em um card de mídia criado com o PlaybackCardController (ou uma subclasse), o PlaybackQueueController pode ser construído no construtor PlaybackCardController usando mDataModel e mItemsRepository para as instâncias PlaybackViewModel e MediaItemsRepository, respectivamente.

Mostrar o histórico de MediaSources tocadas anteriormente

Nesta seção, você vai aprender a mostrar e exibir o histórico de fontes de mídia tocadas anteriormente.

Receber a lista de histórico com a API PlaybackCardViewModel

O PlaybackCardViewModel fornece uma API LiveData chamada getHistoryList() para extrair a lista de histórico de mídia. Ele retorna um LiveData contendo uma lista de MediaSources que foram tocadas antes. Esses dados podem ser usados para preencher um objeto CarUiRecyclerView. Semelhante ao PlaybackQueueController, uma classe chamada PlaybackHistoryController foi adicionada à biblioteca car-media-common para simplificar o processo.

public class PlaybackCardViewModel extends AndroidViewModel {

    public PlaybackCardViewModel(@NonNull Application application) {
    }

    /** Initialize the PlaybackCardViewModel */
    public void init(MediaModels models) {
    }

    public LiveData<List<MediaSource>> getHistoryList() {
        return mHistoryListData;
    }
}

Mostrar a interface do histórico com PlaybackHistoryController

Use o novo PlaybackHistoryController para preencher os dados históricos em um CarUiRecyclerView. Os construtores e as funções principais desta classe são os seguintes. O contêiner transmitido pelo app cliente precisa conter um CarUiRecyclerView com o ID history_list. O CarUiRecyclerView mostra os itens da lista e um cabeçalho opcional. Os dois layouts do item de lista e do cabeçalho podem ser transmitidos pelo app cliente. Se Resources.ID_NULL for definido como headerResource, o cabeçalho não será mostrado. Depois que o PlaybackCardViewModel é transmitido ao controlador, ele monitora o LiveData<List<MediaSource>> extraído de playbackCardViewModel.getHistoryList().

public class PlaybackHistoryController {

    public PlaybackHistoryController(
            LifecycleOwner lifecycleOwner,
            PlaybackCardViewModel playbackCardViewModel,
            ViewGroup container,
            @LayoutRes int itemResource,
            @LayoutRes int headerResource,
            int uxrConfigurationId) {
    }

    /**
     * Renders the view.
     */
    public void setupView() {
    }
}

O layout de cada item precisa conter os IDs das visualizações que ele quer mostrar e que correspondem aos usados na classe interna ViewHolder.

HistoryItemViewHolder(View itemView) {
            super(itemView);
            mContext = itemView.getContext();
            mActiveView = itemView.findViewById(R.id.history_card_container_active);
            mInactiveView = itemView.findViewById(R.id.history_card_container_inactive);
            mMetadataTitleView = itemView.findViewById(R.id.history_card_title_active);
            mAdditionalInfo = itemView.findViewById(R.id.history_card_subtitle_active);
            mAppIcon = itemView.findViewById(R.id.history_card_app_thumbnail);
            mAlbumArt = itemView.findViewById(R.id.history_card_album_art);
            mAppTitleInactive = itemView.findViewById(R.id.history_card_app_title_inactive);
            mAppIconInactive = itemView.findViewById(R.id.history_item_app_icon_inactive);
// ...
}

Para mostrar uma lista de histórico em um card de mídia criado com o PlaybackCardController (ou uma subclasse), o PlaybackHistoryController pode ser criado no construtor do PlaybackCardController.