استخدام بطاقة وسائط في نظام التشغيل Android Automotive (AAOS)

بطاقة الوسائط هي ViewGroup مستقلة تعرض البيانات الوصفية للوسائط، مثل العنوان وصورة الألبوم وغير ذلك، وتوفّر عناصر تحكّم في التشغيل، مثل تشغيل وإيقاف مؤقت وتخطّي، وحتى الإجراءات المخصّصة التي يوفّرها تطبيق الوسائط التابع لجهة خارجية. ويمكن أن تعرض بطاقة الوسائط أيضًا قائمة بعناصر الوسائط، مثل قائمة تشغيل.

بطاقة الوسائط

بطاقة الوسائط

بطاقة الوسائط

الشكل 1. أمثلة على عمليات تنفيذ "بطاقة الوسائط"

كيف يتم تنفيذ بطاقات الوسائط في AAOS؟

تتلقّى ViewGroups التي تعرض معلومات الوسائط تحديثات LiveData من نموذج البيانات في مكتبة car-media-common، أي PlaybackViewModel، لتعبئة ViewGroup. يتوافق كل تعديل في LiveData مع مجموعة فرعية من معلومات الوسائط التي تم تغييرها، مثل MediaItemMetadata وPlaybackStateWrapper وMediaSource.

بما أنّ هذا الأسلوب يؤدي إلى تكرار الرمز البرمجي (يضيف كل تطبيق عميل أدوات مراقبة إلى كل جزء من LiveData ويتم تعيين البيانات المعدَّلة إلى العديد من طرق العرض المشابهة)، أنشأنا PlaybackCardController.

PlaybackCardController

تمت إضافة PlaybackCardController إلى مكتبة car-media-common للمساعدة في إنشاء بطاقة وسائط. هذه فئة عامة يتم إنشاؤها باستخدام ViewGroup (mView) وPlaybackViewModel (mDataModel) وPlaybackCardViewModel (mViewModel) ومثيل MediaItemsRepository (mItemsRepository).

في الدالة setupController، يتم تحليل ViewGroup بحثًا عن طرق عرض معيّنة حسب المعرّف، مع mView.findViewById(R.id.xxx)، ويتم تعيينها إلى عناصر 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);

         // ...
}

يتم رصد كل تعديل على LiveData من PlaybackViewModel في طريقة محمية، ويتم تنفيذ تفاعلات مع طرق العرض ذات الصلة بالبيانات المستلَمة. على سبيل المثال، يضبط مراقب على MediaItemMetadata العنوان على mTitle TextView ويمرّر MediaItemMetadata.ArtworkRef إلى غلاف الألبوم ImageBinder mAlbumArtBinder. إذا كانت البيانات الوصفية فارغة، سيتم إخفاء المشاهدات. يمكن لفئات فرعية من Controller تجاوز هذه المنطقية إذا لزم الأمر.

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

توسيع نطاق PlaybackCardController

يجب أن توسّع تطبيقات العميل التي تريد إنشاء بطاقة وسائط PlaybackCardController إذا كانت لديها إمكانات إضافية تريد التعامل معها في كل تعديل على LiveData. يتبع العملاء الحاليون في AAOS هذا النمط. أولاً، يجب إنشاء فئة فرعية PlaybackCardController، مثل MediaCardController. بعد ذلك، يجب أن يضيف MediaCardController فئة Builder ثابتة داخلية توسّع فئة 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
    // ...

  }
}

إنشاء مثيل لفئة PlaybackCardController أو فئة فرعية

يجب إنشاء مثيل لفئة Controller من جزء أو نشاط من أجل توفير LifecycleOwner لمراقبي LiveData.

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

mViewModel هو مثيل من PlaybackCardViewModel (أو فئة فرعية).

‫PlaybackCardViewModel لحفظ الحالة

PlaybackCardViewModel هو ViewModel يحفظ الحالة ويكون مرتبطًا بـ Fragment أو Activity، ويجب استخدامه لإعادة إنشاء محتوى بطاقة الوسائط في حال حدوث تغيير في الإعدادات (مثل التبديل من المظهر الفاتح إلى الداكن عند مرور المستخدم بنفق). يتولّى PlaybackCardViewModel التلقائي تخزين مثيلات MediaModel لتشغيلها، ويمكن استرداد PlaybackViewModel وMediaItemsRepository منها. استخدِم PlaybackCardViewModel لتتبُّع حالة قائمة الانتظار والسجلّ وقائمة الخيارات الإضافية من خلال دوال الحصول على البيانات وتعيينها المتوفّرة.

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

يمكن توسيع نطاق هذه الفئة إذا كان من الضروري تتبُّع حالات إضافية.

عرض قائمة انتظار في بطاقة وسائط

يوفر PlaybackViewModel واجهات برمجة تطبيقات LiveData لرصد ما إذا كان MediaSource يتوافق مع قائمة انتظار ولعرض قائمة بكائنات MediaItemMetadata في قائمة الانتظار. على الرغم من أنّه يمكن استخدام واجهات برمجة التطبيقات هذه مباشرةً لتعبئة عنصر RecyclerView بمعلومات قائمة الانتظار، تمت إضافة فئة PlaybackQueueController إلى مكتبة car-media-common لتسهيل هذه العملية. يتم تحديد التنسيق لكل عنصر في CarUiRecyclerView من خلال تطبيق العميل، بالإضافة إلى تنسيق عنوان اختياري. يمكن لتطبيق العميل أيضًا اختيار الحدّ من عدد العناصر المعروضة في قائمة الانتظار أثناء القيادة من خلال قيود مخصّصة على تجربة المستخدم.

يتم عرض الدالة الإنشائية PlaybackQueueController وعناصر الضبط في المثال التالي. يمكن تمرير موارد التنسيق queueResource وheaderResource كـ Resources.ID_NULL إذا كان الحاوية في الحالة الأولى تحتوي على CarUiRecyclerView مع id queue_list، وإذا كان قائمة الانتظار في الحالة الثانية لا تحتوي على عنوان.

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

يجب أن يحتوي تنسيق كل عنصر في قائمة الانتظار على معرّفات طرق العرض التي يريد عرضها والتي تتوافق مع المعرّفات المستخدَمة في الفئة الداخلية 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);

            // ...
}

لعرض قائمة انتظار في بطاقة وسائط تم إنشاؤها باستخدام PlaybackCardController (أو فئة فرعية)، يمكن إنشاء PlaybackQueueController في الدالة الإنشائية PlaybackCardController باستخدام mDataModel وmItemsRepository لكائنَي PlaybackViewModel وMediaItemsRepository على التوالي.

عرض سجلّ MediaSources التي تم تشغيلها سابقًا

في هذا القسم، ستتعرّف على كيفية عرض سجلّ مصادر الوسائط التي تم تشغيلها سابقًا.

الحصول على قائمة السجلّ باستخدام PlaybackCardViewModel API

توفّر PlaybackCardViewModel واجهة برمجة تطبيقات LiveData باسم getHistoryList() لاسترداد قائمة سجلّ الوسائط. تعرض هذه الطريقة LiveData تحتوي على قائمة بـ MediaSources التي تم تشغيلها من قبل. يمكن استخدام هذه البيانات لتعبئة عنصر CarUiRecyclerView. على غرار PlaybackQueueController، تمت إضافة فئة باسم PlaybackHistoryController إلى مكتبة car-media-common لتسهيل العملية.

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

واجهة مستخدم سجلّ Surface مع PlaybackHistoryController

استخدِم PlaybackHistoryController الجديد للمساعدة في تعبئة بيانات السجلّ في CarUiRecyclerView. في ما يلي الدوال الإنشائية والدوال الرئيسية لهذه الفئة. يجب أن يحتوي الحاوية التي تم تمريرها من تطبيق العميل على CarUiRecyclerView مع المعرّف history_list. تعرض CarUiRecyclerView عناصر القائمة وعنوانًا اختياريًا. يمكن تمرير كلتا التنسيقَين الخاصَين بعنصر القائمة والرأس من تطبيق العميل. وإذا تم ضبط Resources.ID_NULL على أنّه headerResource، لن يتم عرض الرأس. بعد تمرير PlaybackCardViewModel إلى وحدة التحكّم، تراقب وحدة التحكّم LiveData<List<MediaSource>> التي تم استرجاعها من 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() {
    }
}

يجب أن يحتوي التصميم لكل عنصر على معرّفات طرق العرض التي يريد عرضها والتي تتوافق مع المعرّفات المستخدَمة في الفئة الداخلية 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);
// ...
}

لعرض قائمة سجلّ في بطاقة وسائط تم إنشاؤها باستخدام PlaybackCardController (أو فئة فرعية)، يمكن إنشاء PlaybackHistoryController في الدالة الإنشائية PlaybackCardController.