Wdrażanie karty multimedialnej w systemie AAOS

Karta multimediów to samodzielna grupa ViewGroup, która wyświetla metadane multimediów, takie jak tytuł, okładka albumu itp., oraz elementy sterujące odtwarzaniem, takie jak Odtwórz, Wstrzymaj, Pomiń, a nawet działania niestandardowe udostępniane przez aplikację multimedialną innej firmy. Karta multimediów może też wyświetlać kolejkę elementów multimedialnych, takich jak playlista.

Karta multimediów

Karta multimediów

Karta multimediów

Rysunek 1. Przykładowe implementacje karty multimedialnej

Jak karty multimediów są implementowane w AAOS?

Grupy widoku, które wyświetlają informacje o multimediach, obserwują aktualizacje LiveData z modelu car-media-common library data (PlaybackViewModel), aby wypełnić dane w grupie widoku. Każda aktualizacja LiveData odpowiada podzbiorowi informacji o multimediach, które uległy zmianie, takich jak MediaItemMetadata, PlaybackStateWrapperMediaSource.

Takie podejście prowadzi do powtarzania kodu (każda aplikacja klienta dodaje obserwatorów do każdego elementu LiveData, a wiele podobnych widoków ma przypisane zaktualizowane dane), dlatego utworzyliśmy PlaybackCardController.

PlaybackCardController

Do biblioteki car-media-common został dodany element PlaybackCardController, aby ułatwić tworzenie karty medialnej. To publiczna klasa utworzona z ViewGroup (mView), PlaybackViewModel (mDataModel), PlaybackCardViewModel (mViewModel) i instancji MediaItemsRepository (mItemsRepository).

W funkcji setupController grupa ViewGroup jest analizowana pod kątem określonych widoków za pomocą identyfikatora mView.findViewById(R.id.xxx) i przypisywana do chronionych obiektów View.

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);

         // ...
}

Każda aktualizacja LiveData z PlaybackViewModel jest obserwowana w chronionej metodzie i wykonywana w ramach interakcji z widokami odpowiednimi do otrzymanych danych. Na przykład obserwator na MediaItemMetadata ustawia tytuł w mTitle TextView i przekazuje MediaItemMetadata.ArtworkRef do albumu ImageBinder mAlbumArtBinder. Jeśli metadane mają wartość null, widoki są ukryte. W razie potrzeby podklasy klasy Controller mogą zastąpić tę logikę.

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);
        }
    }

Rozszerzenie klasy PlaybackCardController

Aplikacje klienckie, które chcą tworzyć karty multimedialne, powinny rozszerzać klasę PlaybackCardController, jeśli mają dodatkowe funkcje, które chcą obsługiwać w każdej aktualizacji LiveData. Obecni klienci w AAOS postępują zgodnie z tym wzorcem. Najpierw należy utworzyć podklasę PlaybackCardController, taką jak MediaCardController. Następnie klasa MediaCardController powinna dodać stałą wewnętrzną klasę Builder, która rozszerza klasę 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
    // ...

  }
}

Tworzenie wystąpienia klasy PlaybackCardController lub jej podklasy

Aby klasa Controller mogła mieć obiekt LifecycleOwner dla obserwatorów LiveData, należy utworzyć jej instancję z Fragmentu lub Activity.

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

mViewModel jest wystąpieniem klasy PlaybackCardViewModel (lub jej podklasy).

PlaybackCardViewModel do zapisywania stanu

PlaybackCardViewModel to ViewModel zapisujący stan powiązany z Fragmentem lub Aktywnością, który powinien służyć do odtwarzania zawartości karty multimedialnej w przypadku zmiany konfiguracji (np. przejścia z motywu jasnego na ciemny, gdy użytkownik jedzie przez tunel). Domyślna usługa PlaybackCardViewModel odpowiada za przechowywanie instancji MediaModel na potrzeby odtwarzania, z których można pobrać PlaybackViewModelMediaItemsRepository. Użyj funkcjiPlaybackCardViewModel, aby śledzić stan kolejki, historii i menu przepełnienia za pomocą udostępnionych metod getter i setter.

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;
    }
}

Ta klasa może zostać rozszerzona, jeśli trzeba śledzić dodatkowe stany.

Wyświetlanie kolejki na karcie multimedialnej

Interfejs PlaybackViewModel udostępnia interfejsy LiveData API, które umożliwiają wykrycie, czy MediaSource obsługuje kolejkę, oraz pobranie listy obiektów MediaItemMetadata w tej kolejce. Chociaż tych interfejsów API można używać bezpośrednio do wypełniania obiektu RecyclerView informacjami o kolejce, do biblioteki car-media-common dodaliśmy klasę PlaybackQueueController, aby usprawnić ten proces. Układ każdego elementu w CarUiRecyclerView jest określany przez aplikację klienta, a także przez opcjonalny układ nagłówka. Aplikacja klienta może też ograniczyć liczbę elementów wyświetlanych w kolejce podczas jazdy za pomocą niestandardowych ograniczeń UXR.

Konstruktor i metody ustawiające PlaybackQueueController są pokazane w tym przykładzie. Zasoby układu queueResourceheaderResource mogą być przekazywane jako Resources.ID_NULL, jeśli w pierwszym przypadku kontener zawiera już CarUiRecyclerViewid queue_list, a w drugim przypadku kolejka nie ma nagłówka.

   /**
    * 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;
    }

Układ każdego elementu kolejki powinien zawierać identyfikatory widoków, które mają być wyświetlane, odpowiadające tym używanym w klasie wewnętrznej 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);

            // ...
}

Aby wyświetlić kolejkę na karcie multimedialnej utworzonej za pomocą klasy PlaybackCardController (lub jej podklasy), konstruktor PlaybackCardController może tworzyć obiekt PlaybackQueueController, używając do tego obiektu mDataModel i mItemsRepository odpowiednio dla instancji PlaybackViewModel i MediaItemsRepository.

wyświetlać historię wcześniej odtwarzanych źródeł multimediów.

Z tej sekcji dowiesz się, jak wyświetlać i wyświetlać historię wcześniej odtwarzanych źródeł multimediów.

Pobieranie listy historii za pomocą interfejsu PlaybackCardViewModel API

PlaybackCardViewModel udostępnia interfejs LiveData API o nazwie getHistoryList(), który służy do pobierania listy historii multimediów. Zwraca on dane LiveData zawierające listę źródeł multimediów, które były odtwarzane wcześniej. Tych danych można używać do wypełniania obiektu CarUiRecyclerView. Aby usprawnić proces, do biblioteki car-media-common dodano klasę PlaybackHistoryController, która jest podobna do klasy PlaybackQueueController.

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;
    }
}

Interfejs historii Surface z PlaybackHistoryController

Użyj nowego elementu PlaybackHistoryController, aby wypełnić dane historyczne w elementach CarUiRecyclerView. Konstruktory i główne funkcje tej klasy są następujące: Kontener przekazany przez aplikację klienta powinien zawierać CarUiRecyclerView o identyfikatorze history_list. CarUiRecyclerViewwyświetla elementy listy i opcjonalny nagłówek. Z aplikacji klienta można przekazać oba układy elementu listy i nagłówka. Jeśli jako headerResource ustawiona jest wartość Resources.ID_NULL, nagłówek nie jest wyświetlany. Gdy PlaybackCardViewModel zostanie przekazany do kontrolera, ten będzie monitorować LiveData<List<MediaSource>> pobrany z 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() {
    }
}

Układ każdego elementu powinien zawierać identyfikatory widoków, które mają być wyświetlane, odpowiadające tym, które są używane w wewnętrznej klasie 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);
// ...
}

Aby wyświetlić listę historii na karcie multimedialnej utworzonej za pomocą klasy PlaybackCardController (lub jej podklasy), możesz skonstruować obiekt PlaybackHistoryController w konstruktorze klasy PlaybackCardController.