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

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

بطاقة وسائط

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

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

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

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

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

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

وحدة تحكم بطاقة التشغيل

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

في دالة setupController، يتم تحليل ViewGroup لطرق عرض معيّنة باستخدام ID، مع 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. إذا كانت بيانات التعريف فارغة، فسيتم إخفاء طرق العرض. يمكن للفئات الفرعية من وحدة التحكم إلغاء هذا المنطق إذا لزم الأمر.

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 من Fragment أو Activity من أجل الحصول على LifecycleOwner لمراقبي LiveData.

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

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

PlaybackCardViewModel لحفظ الحالة

PlaybackCardViewModel هو نموذج عرض يحفظ الحالة ويرتبط بجزء أو نشاط، ويجب استخدامه لإعادة إنشاء محتوى بطاقة الوسائط في حال حدث تغيير في الإعدادات (مثل التبديل من المظهر الفاتح إلى المظهر الداكن عندما يقود المستخدم السيارة في نفق). يعالج PlaybackCardViewModel التلقائي تخزين نُسخ من MediaModel لتشغيلها، والتي يمكن من خلالها retrievingPlaybackViewModel و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 constructor باستخدام mDataModel وmItemsRepository لمثيلَي PlaybackViewModel وMediaItemsRepository على التوالي.

عرض سجلّ مصادر الوسائط التي تم تشغيلها سابقًا

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

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

يوفّر PlaybackCardViewModel واجهة برمجة تطبيقات LiveData API تُسمى getHistoryList() ل retrieving the media history list. ويعرِض هذا الإجراء بيانات LiveData تتضمّن قائمة بملفّات MediaSource التي تم تشغيلها من قبل. يمكن استخدام هذه البيانات لتعبئة عنصر 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.