إنشاء تطبيقات وسائط للسيارات

يساعدك Android Auto ونظام التشغيل Android Automotive في تقديم محتوى تطبيق الوسائط إلى المستخدمين في سياراتهم. يجب أن يوفر تطبيق الوسائط للسيارات خدمة متصفح الوسائط حتى يتمكن Android Auto أو نظام التشغيل Android Automotive أو أي تطبيق آخر مزوّد بمتصفّح للوسائط من اكتشاف المحتوى الخاص بك وعرضه.

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

يوضِّح هذا الدليل المكونات المطلوبة لكل من MediaBrowserService وMediaSession التي يحتاجها تطبيقك للعمل على Android Auto أو Android Automotive. بعد إكمال البنية الأساسية للوسائط، يمكنك إتاحة استخدام Android Auto وإتاحة استخدام Android Automotive في تطبيق الوسائط.

قبل البدء

  1. يمكنك مراجعة المستندات المتعلّقة بـ Android Media API.
  2. راجع إنشاء تطبيقات الوسائط للحصول على إرشادات التصميم.
  3. راجع المصطلحات والمفاهيم الأساسية المدرجة في هذا القسم.

المصطلحات والمفاهيم الرئيسية

خدمة متصفّحات الوسائط
خدمة Android ينفّذها تطبيق الوسائط لديك وتتوافق مع MediaBrowserServiceCompat API. يستخدم تطبيقك هذه الخدمة لعرض المحتوى الخاص به.
متصفح الوسائط
واجهة برمجة تطبيقات تستخدمها تطبيقات الوسائط لاكتشاف خدمات تصفُّح الوسائط وعرض محتواها. يستخدم Android Auto وAndroid Automotive متصفح وسائط للعثور على خدمة متصفح الوسائط في تطبيقك.
عنصر وسائط

ينظّم متصفّح الوسائط المحتوى الخاص به في شجرة من عناصر MediaItem. يمكن أن يحتوي عنصر الوسائط على إحدى العلامتين التاليتين أو كلتيهما:

  • FLAG_PLAYABLE: يشير إلى أنّ العنصر عبارة عن ورقة شجر في شجرة المحتوى. يمثل العنصر بثًا صوتيًا واحدًا، مثل أغنية في ألبوم أو فصلاً في كتاب مسموع أو حلقة من بودكاست.
  • FLAG_BROWSABLE: يشير إلى أنّ العنصر هو عقدة في شجرة المحتوى ولديه عناصر فرعية. على سبيل المثال، يمثل العنصر ألبومًا، وعناصره الثانوية هي الأغاني الموجودة في الألبوم.

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

محسَّنة للمركبات

نشاط لتطبيق متوافق مع نظام التشغيل Android Automotive يلتزم بإرشادات تصميم نظام التشغيل Android Automotive. لم يتم رسم واجهة هذه الأنشطة بواسطة نظام التشغيل Android Automotive، لذا يجب عليك التأكد من التزام تطبيقك بإرشادات التصميم. ويشمل ذلك عادةً استهدافات النقر وأحجام الخطوط الأكبر حجمًا ودعم الوضعَين النهاري والليلي ونِسب تباين أعلى.

يُسمح بعرض واجهات المستخدم المحسَّنة للمركبة فقط عندما لا تكون "القيود المفروضة على تجربة المستخدم في السيارة" (CUXR) سارية، لأنّ هذه الواجهات قد تتطلّب اهتمامًا كبيرًا أو تفاعلاً كبيرًا من المستخدم. لا تكون ملحقات أمان تجربة المستخدم (CUXR) سارية عند إيقاف السيارة أو ركنها، ولكنها سارية دائمًا عندما تكون السيارة قيد الحركة.

لا تحتاج إلى تصميم أنشطة لـ Android Auto لأن Android Auto يرسم واجهته الخاصة المحسّنة للمركبة باستخدام المعلومات من خدمة متصفح الوسائط.

ضبط ملفات البيان لتطبيقك

قبل أن تتمكن من إنشاء خدمة متصفح الوسائط، يجب عليك تهيئة ملفات البيان في تطبيقك.

تعريف خدمة متصفِّح الوسائط

يتم ربط كل من Android Auto ونظام التشغيل Android Automotive بتطبيقك من خلال خدمة متصفح الوسائط لتصفّح الوسائط. يُرجى تعريف خدمة متصفِّح الوسائط في ملف البيان للسماح لنظامَي التشغيل Android Auto ونظام التشغيل Android Automotive باكتشاف الخدمة والاتصال بتطبيقك.

يعرض مقتطف الرمز التالي كيفية الإعلان عن خدمة متصفح الوسائط في ملف البيان. أدرِج هذه الرمز في ملف البيان لوحدة نظام التشغيل Android Automotive وفي ملف البيان لتطبيق هاتفك.

<application>
    ...
    <service android:name=".MyMediaBrowserService"
             android:exported="true">
        <intent-filter>
            <action android:name="android.media.browse.MediaBrowserService"/>
        </intent-filter>
    </service>
    ...
</application>

تحديد رموز التطبيقات

يجب تحديد رموز التطبيقات التي يمكن لنظامَي التشغيل Android Auto وAndroid Automotive استخدامهما لتمثيل تطبيقك في واجهة مستخدم النظام. يجب إدخال نوعَين من الرموز:

  • رمز مشغّل التطبيقات
  • رمز تحديد المصدر

رمز مشغّل التطبيقات

يمثل رمز مشغّل التطبيقات تطبيقك في واجهة مستخدم النظام، مثل مشغّل التطبيقات وفي شريط الرموز. يمكنك تحديد أنّك تريد استخدام الرمز من تطبيقك للأجهزة الجوّالة لتمثيل تطبيق وسائط السيارة باستخدام بيان البيان التالي:

<application
    ...
    android:icon="@mipmap/ic_launcher"
    ...
/>

لاستخدام رمز مختلف عن رمز التطبيق المتوافق مع الأجهزة الجوّالة، يجب ضبط السمة android:icon في العنصر <service> لخدمة متصفح الوسائط في البيان:

<application>
    ...
    <service
        ...
        android:icon="@mipmap/auto_launcher"
        ...
    />
</application>

رمز تحديد المصدر

الشكل 1. رمز تحديد المصدر على بطاقة الوسائط

يُستخدم رمز تحديد المصدر في الأماكن التي تكون فيها الأولوية لمحتوى الوسائط، مثلاً على بطاقات الوسائط. يمكنك إعادة استخدام الرمز الصغير المستخدَم للإشعارات. يجب أن يكون هذا الرمز أحادي اللون. ويمكنك تحديد رمز يُستخدَم لتمثيل تطبيقك باستخدام بيان البيان التالي:

<application>
    ...
    <meta-data
        android:name="androidx.car.app.TintableAttributionIcon"
        android:resource="@drawable/ic_status_icon" />
    ...
</application>

إنشاء خدمة متصفح الوسائط

يمكنك إنشاء خدمة متصفح وسائط من خلال تمديد الفئة MediaBrowserServiceCompat. ويمكن لكل من Android Auto ونظام التشغيل Android Automotive بعد ذلك استخدام خدمتك لتنفيذ ما يلي:

  • تصفَّح التدرج الهرمي لمحتوى تطبيقك لعرض قائمة للمستخدم.
  • احصل على الرمز المميّز لكائن MediaSessionCompat في تطبيقك للتحكّم في تشغيل الصوت.

يمكنك أيضًا استخدام خدمة متصفح الوسائط للسماح للبرامج الأخرى بالوصول إلى محتوى الوسائط من تطبيقك. وقد تكون برامج الوسائط هذه تطبيقات أخرى على هاتف المستخدم، أو برامج أخرى عن بُعد.

سير عمل خدمة متصفِّح الوسائط

يصف هذا القسم كيفية تفاعل نظامي التشغيل Android Automotive وAndroid Auto مع خدمة متصفح الوسائط أثناء سير عمل المستخدم المعتاد.

  1. يشغِّل المستخدم تطبيقك على نظام التشغيل Android Automotive أو Android Auto.
  2. يتصل نظام التشغيل Android Automotive أو Android Auto بخدمة متصفّح الوسائط في تطبيقك باستخدام الطريقة onCreate(). لتنفيذ الطريقة onCreate()، عليك إنشاء وتسجيل كائن MediaSessionCompat وكائن معاودة الاتصال المتعلق به.
  3. يستدعي نظام التشغيل Android Automotive أو Android Auto طريقة onGetRoot() الخاصة بخدمتك للحصول على عنصر الوسائط الجذر في التسلسل الهرمي للمحتوى. لا يتم عرض عنصر الوسائط الجذر، بدلاً من ذلك، يتم استخدامه لاسترداد المزيد من المحتوى من تطبيقك.
  4. يستدعي نظام التشغيل Android Automotive أو Android Auto طريقة onLoadChildren() الخاصة بخدمتك للحصول على العناصر الثانوية لعنصر الوسائط الجذر. يعرض نظام التشغيل Android Automotive وAndroid Auto عناصر الوسائط هذه كأعلى مستوى من عناصر المحتوى. يمكنك الاطّلاع على مقالة تنظيم بنية قائمة الجذر في هذه الصفحة للحصول على مزيد من المعلومات حول ما يتوقعه النظام في هذا المستوى.
  5. إذا اختار المستخدم عنصر وسائط قابل للتصفّح، يتم استدعاء طريقة onLoadChildren() في خدمتك مرة أخرى لاسترداد العناصر الثانوية لعنصر القائمة المحدّد.
  6. إذا اختار المستخدم عنصر وسائط قابل للتشغيل، يستدعي نظام التشغيل Android Automotive أو Android Auto طريقة معاودة الاتصال بجلسة الوسائط المناسبة لتنفيذ هذا الإجراء.
  7. يمكن للمستخدم أيضًا البحث في المحتوى إذا كان تطبيقك يتيح ذلك. في هذه الحالة، يستدعي نظام التشغيل Android Automotive أو Android Auto طريقة onSearch() الخاصة بخدمتك.

بناء تسلسل هرمي للمحتوى

يتصل كل من Android Auto ونظام التشغيل Android Automotive بخدمة متصفح الوسائط في تطبيقك للتعرّف على المحتوى المتوفّر. يجب تنفيذ طريقتين في خدمة متصفّح الوسائط لإتاحة ذلك: onGetRoot() وonLoadChildren()

تنفيذ onGetRoot

تعرض الطريقة onGetRoot() في خدمتك معلومات حول العقدة الجذرية للتسلسل الهرمي للمحتوى. يستخدم Android Auto ونظام التشغيل Android Automotive هذه العقدة الجذر لطلب بقية المحتوى باستخدام الطريقة onLoadChildren().

يعرض مقتطف الرمز التالي تنفيذًا بسيطًا لطريقة onGetRoot():

Kotlin

override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle?
): BrowserRoot? =
    // Verify that the specified package is allowed to access your
    // content. You'll need to write your own logic to do this.
    if (!isValid(clientPackageName, clientUid)) {
        // If the request comes from an untrusted package, return null.
        // No further calls will be made to other media browsing methods.

        null
    } else MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null)

Java

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {

    // Verify that the specified package is allowed to access your
    // content. You'll need to write your own logic to do this.
    if (!isValid(clientPackageName, clientUid)) {
        // If the request comes from an untrusted package, return null.
        // No further calls will be made to other media browsing methods.

        return null;
    }

    return new MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null);
}

للاطّلاع على مثال أكثر تفصيلاً لهذه الطريقة، يمكنك الاطّلاع على الطريقة onGetRoot() في نموذج تطبيق Universal Android Music Player على GitHub.

إضافة التحقق من صحة الحزمة لـ onGetRoot()

عند إجراء اتصال بطريقة onGetRoot() الخاصة بخدمتك، تمرر حزمة الاتصال معلومات تعريفية إلى خدمتك. يمكن لخدمتك استخدام هذه المعلومات لتحديد ما إذا كان يمكن لهذه الحزمة الوصول إلى المحتوى أم لا. على سبيل المثال، يمكنك حظر الوصول إلى محتوى تطبيقك ضمن قائمة الحِزم المعتمَدة من خلال مقارنة clientPackageName بالقائمة المسموح بها والتحقّق من الشهادة المستخدَمة لتوقيع حزمة APK الخاصة بالحزمة. إذا تعذّر التحقّق من الحزمة، يمكنك عرض الرمز null لرفض الوصول إلى المحتوى.

لتوفير إمكانية الوصول إلى المحتوى لتطبيقات النظام، مثل Android Auto وAndroid Automotive، يجب أن تعرض الخدمة دائمًا قيمة غير فارغة BrowserRoot عندما تستدعي تطبيقات النظام هذه الطريقة onGetRoot(). يمكن أن يختلف توقيع تطبيق نظام التشغيل Android Automotive بناءً على العلامة التجارية للسيارة وطرازها، لذا عليك السماح باتصالات من جميع تطبيقات النظام لدعم نظام التشغيل Android Automotive بقوة.

يوضح مقتطف الرمز التالي كيف يمكن لخدمتك التحقق من أن حزمة الاتصال هي تطبيق نظام:

fun isKnownCaller(
    callingPackage: String,
    callingUid: Int
): Boolean {
    ...
    val isCallerKnown = when {
       // If the system is making the call, allow it.
       callingUid == Process.SYSTEM_UID -> true
       // If the app was signed by the same certificate as the platform
       // itself, also allow it.
       callerSignature == platformSignature -> true
       // ... more cases
    }
    return isCallerKnown
}

مقتطف الرمز هذا هو مقتطف من الفئة PackageValidator في نموذج تطبيق Universal Android Music Player على GitHub. راجِع تلك الفئة للاطّلاع على مثال أكثر تفصيلاً حول كيفية إجراء عملية التحقّق من صحة الحزمة لطريقة onGetRoot() الخاصة بخدمتك.

بالإضافة إلى السماح بتطبيقات النظام، يجب السماح لـ "مساعد Google" بالاتصال بجهاز MediaBrowserService. يُرجى العِلم أنّ "مساعد Google" يتضمّن أسماء حِزم منفصلة للهاتف، بما في ذلك Android Auto ونظام التشغيل Android Automotive.

تنفيذ onLoadChildren()

بعد استلام كائن العقدة الجذر، يعمل Android Auto ونظام التشغيل Android Automotive على إنشاء قائمة مستوى أعلى من خلال استدعاء onLoadChildren() في كائن العقدة الجذر للحصول على عناصره الثانوية. تنشئ تطبيقات العميل قوائم فرعية عن طريق استدعاء هذه الطريقة نفسها باستخدام كائنات العقدة الفرعية.

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

يعرض مقتطف الرمز التالي تنفيذًا بسيطًا لطريقة onLoadChildren():

Kotlin

override fun onLoadChildren(
    parentMediaId: String,
    result: Result<List<MediaBrowserCompat.MediaItem>>
) {
    // Assume for example that the music catalog is already loaded/cached.

    val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()

    // Check whether this is the root menu:
    if (MY_MEDIA_ROOT_ID == parentMediaId) {

        // Build the MediaItem objects for the top level
        // and put them in the mediaItems list.
    } else {

        // Examine the passed parentMediaId to see which submenu we're at
        // and put the children of that menu in the mediaItems list.
    }
    result.sendResult(mediaItems)
}

Java

@Override
public void onLoadChildren(final String parentMediaId,
    final Result<List<MediaBrowserCompat.MediaItem>> result) {

    // Assume for example that the music catalog is already loaded/cached.

    List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();

    // Check whether this is the root menu:
    if (MY_MEDIA_ROOT_ID.equals(parentMediaId)) {

        // Build the MediaItem objects for the top level
        // and put them in the mediaItems list.
    } else {

        // Examine the passed parentMediaId to see which submenu we're at
        // and put the children of that menu in the mediaItems list.
    }
    result.sendResult(mediaItems);
}

للحصول على مثال كامل لهذه الطريقة، يمكنك الاطّلاع على الطريقة onLoadChildren() في نموذج تطبيق Universal Android Music Player على GitHub.

تنظيم قائمة الجذر

الشكل 2. محتوى الجذر المعروض كعلامات تبويب تنقُّل.

يفرض كل من Android Auto وAndroid Automotive قيود محددة على هيكل القائمة الجذر. يتم إرسال هذه المعلومات إلى MediaBrowserService من خلال تلميحات الجذر التي يمكن قراءتها من خلال الوسيطة Bundle التي تم تمريرها إلى onGetRoot(). باتّباع هذه التلميحات، يتيح للنظام عرض المحتوى الجذر بشكل مثالي كعلامات تبويب للتنقّل. في حال عدم اتّباع هذه التلميحات، قد يحذف النظام بعض المحتوى الجذر أو قد يقل قابلية اكتشافه. يتم إرسال تلميحين:

استخدم الرمز التالي لقراءة تلميحات الجذر ذات الصلة:

Kotlin

import androidx.media.utils.MediaConstants

// Later, in your MediaBrowserServiceCompat.
override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle
): BrowserRoot {

  val maximumRootChildLimit = rootHints.getInt(
      MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
      /* defaultValue= */ 4)
  val supportedRootChildFlags = rootHints.getInt(
      MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
      /* defaultValue= */ MediaItem.FLAG_BROWSABLE)

  // Rest of method...
}

Java

import androidx.media.utils.MediaConstants;

// Later, in your MediaBrowserServiceCompat.
@Override
public BrowserRoot onGetRoot(
    String clientPackageName, int clientUid, Bundle rootHints) {

    int maximumRootChildLimit = rootHints.getInt(
        MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
        /* defaultValue= */ 4);
    int supportedRootChildFlags = rootHints.getInt(
        MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
        /* defaultValue= */ MediaItem.FLAG_BROWSABLE);

    // Rest of method...
}

يمكنك اختيار تشعّب المنطق في بنية المحتوى الهرمي استنادًا إلى قيم هذه التلميحات، لا سيّما إذا كان التدرّج الهرمي يتفاوت بين عمليات دمج MediaBrowser خارج نظامي التشغيل Android Auto وAndroid Automotive. على سبيل المثال، إذا كنت تعرض عادةً عنصرًا جذريًا قابلاً للتشغيل، قد تحتاج إلى دمجه ضمن عنصر جذر قابل للتصفح بدلاً من ذلك بسبب قيمة تلميح العلامات المتوافقة.

بالإضافة إلى التلميحات الجذرية، هناك بعض الإرشادات الإضافية التي يجب اتّباعها للمساعدة في ضمان عرض علامات التبويب بشكل مثالي:

  • تقديم رموز أحادية اللون، ويفضل أن تكون بيضاء، لكل عنصر من عناصر علامة التبويب
  • قدِّم تصنيفات قصيرة ولكن ذات معنى لكل عنصر في علامة التبويب. يؤدي إبقاء التسميات قصيرة إلى تقليل فرصة اقتطاع السلاسل.

عرض الأعمال الفنية للوسائط

يجب نقل العمل الفني لعناصر الوسائط كمعرّف موارد منتظم (URI) محلي باستخدام ContentResolver.SCHEME_CONTENT أو ContentResolver.SCHEME_ANDROID_RESOURCE. يجب أن يتحول عنوان URI المحلي هذا إلى صورة نقطية أو متجه قابل للرسم في موارد التطبيق. بالنسبة إلى كائنات MediaDescriptionCompat التي تمثل العناصر في التسلسل الهرمي للمحتوى، مرِّر معرّف الموارد المنتظم (URI) إلى setIconUri(). بالنسبة إلى كائنات MediaMetadataCompat التي تمثل العنصر الذي يتم تشغيله حاليًا، مرِّر معرّف الموارد المنتظم (URI) عبر putString()، باستخدام أي من المفاتيح التالية:

تصف الخطوات التالية كيفية تنزيل الأعمال الفنية من معرّف الموارد المنتظم (URI) للويب وعرضها من خلال معرّف موارد منتظم (URI) محلي. للحصول على مثال أكثر اكتمالاً، يمكنك الاطّلاع على تنفيذ openFile() والطرق المحيطة به في نموذج تطبيق Universal Android Music Player.

  1. أنشئ معرّف موارد منتظمًا (URI) content:// مطابق لمعرّف الموارد المنتظم (URI) على الويب. تعمل خدمة متصفح الوسائط وجلسة الوسائط على تمرير معرّف الموارد المنتظم (URI) للمحتوى هذا إلى Android Auto ونظام التشغيل Android Automotive.

    Kotlin

    fun Uri.asAlbumArtContentURI(): Uri {
      return Uri.Builder()
        .scheme(ContentResolver.SCHEME_CONTENT)
        .authority(CONTENT_PROVIDER_AUTHORITY)
        .appendPath(this.getPath()) // Make sure you trust the URI
        .build()
    }
    

    Java

    public static Uri asAlbumArtContentURI(Uri webUri) {
      return new Uri.Builder()
        .scheme(ContentResolver.SCHEME_CONTENT)
        .authority(CONTENT_PROVIDER_AUTHORITY)
        .appendPath(webUri.getPath()) // Make sure you trust the URI!
        .build();
    }
    
  2. أثناء تنفيذ ContentProvider.openFile()، تحقّق من توفُّر ملف لعنوان URI المقابل. وإذا لم يكن كذلك، نزِّل ملف الصورة واحتفظ به في ذاكرة التخزين المؤقت. يستخدم مقتطف الرمز التالي Glide.

    Kotlin

    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
      val context = this.context ?: return null
      val file = File(context.cacheDir, uri.path)
      if (!file.exists()) {
        val remoteUri = Uri.Builder()
            .scheme("https")
            .authority("my-image-site")
            .appendPath(uri.path)
            .build()
        val cacheFile = Glide.with(context)
            .asFile()
            .load(remoteUri)
            .submit()
            .get(DOWNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
    
        cacheFile.renameTo(file)
        file = cacheFile
      }
      return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
    }
    

    Java

    @Nullable
    @Override
    public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
        throws FileNotFoundException {
      Context context = this.getContext();
      File file = new File(context.getCacheDir(), uri.getPath());
      if (!file.exists()) {
        Uri remoteUri = new Uri.Builder()
            .scheme("https")
            .authority("my-image-site")
            .appendPath(uri.getPath())
            .build();
        File cacheFile = Glide.with(context)
            .asFile()
            .load(remoteUri)
            .submit()
            .get(DOWNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
    
        cacheFile.renameTo(file);
        file = cacheFile;
      }
      return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    }
    

لمزيد من التفاصيل حول موفّري المحتوى، يمكنك الاطّلاع على إنشاء موفّر محتوى.

تطبيق أنماط المحتوى

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

يمكنك استخدام أنماط المحتوى التالية:

عناصر القائمة

ويمنح نمط المحتوى هذا الأولوية للعناوين والبيانات الوصفية بدلاً من الصور.

عناصر الشبكة

ويعطي نمط المحتوى هذا الأولوية للصور على العناوين والبيانات الوصفية.

تعيين أنماط المحتوى التلقائية

يمكنك ضبط الإعدادات التلقائية العامة لكيفية عرض عناصر الوسائط من خلال تضمين ثوابت معيّنة في حزمة BrowserRoot الإضافية الخاصة بطريقة onGetRoot() في خدمتك. يقرأ Android Auto ونظام التشغيل Android Automotive هذه الحزمة ويبحثان عن تلك الثوابت لتحديد النمط المناسب.

يمكن استخدام العناصر الإضافية التالية كمفاتيح في الحزمة:

يمكن تعيين المفاتيح للقيم الثابتة التالية للتأثير على عرض هذه العناصر:

  • DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM: يتم عرض العناصر المقابلة على شكل عناصر قائمة.
  • DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM: يتم عرض العناصر المقابلة لها كعناصر على شكل شبكة.
  • DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM: يتم عرض العناصر المقابلة كعناصر قائمة "فئة". هذه هي نفسها عناصر القائمة العادية باستثناء أنه يتم تطبيق الهوامش حول أيقونات العناصر، لأن الأيقونات تبدو أفضل عندما تكون صغيرة. يجب أن تكون الأيقونات متجهات قابلة للتلوين. من المتوقع توفير هذا التلميح فقط للعناصر القابلة للتصفح.
  • DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM: يتم عرض العناصر المقابلة كعناصر في شبكة "الفئة". هذه هي نفسها عناصر الشبكة العادية، باستثناء أنه يتم تطبيق الهوامش حول أيقونات العناصر، لأن الأيقونات تبدو أفضل عندما تكون صغيرة. يجب أن تكون الأيقونات متجهات قابلة للتلوين. من المتوقع توفير هذا التلميح فقط للعناصر القابلة للتصفح.

يعرض مقتطف الرمز التالي كيفية ضبط نمط المحتوى التلقائي للعناصر القابلة للتصفّح على شكل شبكات والعناصر القابلة للتشغيل على قوائم:

Kotlin

import androidx.media.utils.MediaConstants

@Nullable
override fun onGetRoot(
    @NonNull clientPackageName: String,
    clientUid: Int,
    @Nullable rootHints: Bundle
): BrowserRoot {
    val extras = Bundle()
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM)
    return BrowserRoot(ROOT_ID, extras)
}

Java

import androidx.media.utils.MediaConstants;

@Nullable
@Override
public BrowserRoot onGetRoot(
    @NonNull String clientPackageName,
    int clientUid,
    @Nullable Bundle rootHints) {
    Bundle extras = new Bundle();
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM);
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM);
    return new BrowserRoot(ROOT_ID, extras);
}

تعيين أنماط محتوى لكل عنصر

تتيح لك Content Style API إلغاء نمط المحتوى التلقائي لأي عناصر فرعية لعنصر وسائط قابل للتصفّح، وكذلك أي عنصر وسائط نفسه.

لإلغاء الإعدادات التلقائية للعناصر الفرعية لعنصر الوسائط القابل للتصفّح، يمكنك إنشاء حزمة إضافية في MediaDescription من عنصر الوسائط وإضافة الاقتراحات نفسها المذكورة سابقًا. ينطبق DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE على العناصر الثانوية القابلة للتشغيل في هذا العنصر، بينما يسري DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE على العناصر الثانوية القابلة للتصفّح في ذلك العنصر.

لإلغاء الإعداد التلقائي لعنصر وسائط معيّن بنفسه، وليس عناصره الفرعية، يمكنك إنشاء حزمة إضافية في MediaDescription من عنصر الوسائط وإضافة تلميح مع المفتاح DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM. استخدِم القيم نفسها الموضّحة سابقًا لتحديد طريقة العرض التقديمي لهذا العنصر.

يعرض مقتطف الرمز التالي كيفية إنشاء سمة MediaItem قابلة للتصفّح تلغي نمط المحتوى التلقائي في الصفحة نفسها وعناصرها الثانوية. يقوم بنمط نفسه كعنصر قائمة فئات، وعناصره الثانوية القابلة للتصفح كعناصر قائمة، وعناصره الثانوية القابلة للتشغيل كعناصر شبكة:

Kotlin

import androidx.media.utils.MediaConstants

private fun createBrowsableMediaItem(
    mediaId: String,
    folderName: String,
    iconUri: Uri
): MediaBrowser.MediaItem {
    val mediaDescriptionBuilder = MediaDescription.Builder()
    mediaDescriptionBuilder.setMediaId(mediaId)
    mediaDescriptionBuilder.setTitle(folderName)
    mediaDescriptionBuilder.setIconUri(iconUri)
    val extras = Bundle()
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM)
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM)
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
    mediaDescriptionBuilder.setExtras(extras)
    return MediaBrowser.MediaItem(
        mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)
}

Java

import androidx.media.utils.MediaConstants;

private MediaBrowser.MediaItem createBrowsableMediaItem(
    String mediaId,
    String folderName,
    Uri iconUri) {
    MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
    mediaDescriptionBuilder.setMediaId(mediaId);
    mediaDescriptionBuilder.setTitle(folderName);
    mediaDescriptionBuilder.setIconUri(iconUri);
    Bundle extras = new Bundle();
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM);
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM);
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM);
    mediaDescriptionBuilder.setExtras(extras);
    return new MediaBrowser.MediaItem(
        mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE);
}

تجميع العناصر باستخدام تلميحات العناوين

لتجميع عناصر الوسائط ذات الصلة معًا، يمكنك استخدام تلميح لكل عنصر. يجب أن يذكر كل عنصر من الوسائط في مجموعة ما حزمة إضافية في MediaDescription تتضمّن عملية ربط بالمفتاح DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE وقيمة سلسلة متطابقة. قم بأقلمة هذه السلسلة، والتي يتم استخدامها كعنوان للمجموعة.

يعرض مقتطف الرمز التالي كيفية إنشاء MediaItem باستخدام عنوان مجموعة فرعية "Songs":

Kotlin

import androidx.media.utils.MediaConstants

private fun createMediaItem(
    mediaId: String,
    folderName: String,
    iconUri: Uri
): MediaBrowser.MediaItem {
    val mediaDescriptionBuilder = MediaDescription.Builder()
    mediaDescriptionBuilder.setMediaId(mediaId)
    mediaDescriptionBuilder.setTitle(folderName)
    mediaDescriptionBuilder.setIconUri(iconUri)
    val extras = Bundle()
    extras.putString(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
        "Songs")
    mediaDescriptionBuilder.setExtras(extras)
    return MediaBrowser.MediaItem(
        mediaDescriptionBuilder.build(), /* playable or browsable flag*/)
}

Java

import androidx.media.utils.MediaConstants;

private MediaBrowser.MediaItem createMediaItem(String mediaId, String folderName, Uri iconUri) {
   MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
   mediaDescriptionBuilder.setMediaId(mediaId);
   mediaDescriptionBuilder.setTitle(folderName);
   mediaDescriptionBuilder.setIconUri(iconUri);
   Bundle extras = new Bundle();
   extras.putString(
       MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
       "Songs");
   mediaDescriptionBuilder.setExtras(extras);
   return new MediaBrowser.MediaItem(
       mediaDescriptionBuilder.build(), /* playable or browsable flag*/);
}

يجب أن يمرر تطبيقك كل عناصر الوسائط التي تريد تجميعها معًا ككتلة متجاورة. على سبيل المثال، لنفترض أنك تريد عرض مجموعتين من عناصر الوسائط، وهما "الأغاني" و "الألبومات"، بهذا الترتيب، وأن تطبيقك يجتاز خمسة عناصر وسائط بالترتيب التالي:

  1. عنصر الوسائط "أ" مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  2. عنصر الوسائط "ب" مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")
  3. عنصر الوسائط ج مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  4. عنصر الوسائط د مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  5. عنصر الوسائط E مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")

بما أنّ عناصر الوسائط لمجموعة "الأغاني" ومجموعة "الألبومات" لا يتم الاحتفاظ بها معًا في كتل متجاورة، يفسر نظام التشغيل Android Auto ونظام التشغيل Android Automotive على أنّها المجموعات الأربع التالية:

  • المجموعة 1 المسماة "الأغاني" والتي تتضمن عنصر الوسائط أ
  • المجموعة 2 المسماة "الألبومات" التي تتضمن عنصر الوسائط ب
  • المجموعة 3 المسماة "الأغاني" والتي تحتوي على العنصرين "ج" و"د"
  • المجموعة 4 المسماة "الألبومات" التي تتضمن عنصر الوسائط E

لعرض هذه العناصر في مجموعتَين، يجب أن يمرِّر تطبيقك عناصر الوسائط بالترتيب التالي بدلاً من ذلك:

  1. عنصر الوسائط "أ" مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  2. عنصر الوسائط ج مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  3. عنصر الوسائط د مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  4. عنصر الوسائط "ب" مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")
  5. عنصر الوسائط E مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")

عرض مؤشرات بيانات وصفية إضافية

يمكنك تضمين مؤشرات بيانات وصفية إضافية لتقديم معلومات سريعة حول المحتوى في شجرة متصفح الوسائط وأثناء التشغيل. ضمن شجرة التصفح، يقرأ Android Auto ونظام التشغيل Android Automotive البيانات الإضافية المرتبطة بأحد العناصر ويبحثان عن ثوابت معيّنة لتحديد المؤشرات التي سيتم عرضها. أثناء تشغيل الوسائط، يقرأ Android Auto ونظام التشغيل Android Automotive البيانات الوصفية لجلسة الوسائط ويبحثان عن ثوابت معيّنة لتحديد المؤشرات التي سيتم عرضها.

الشكل 3. عرض تشغيل يتضمّن بيانات وصفية تحدّد الأغنية والفنّان بالإضافة إلى رمز يشير إلى المحتوى الفاضح

الشكل 4. يمكنك تصفّح طريقة العرض التي تتضمن نقطة للاطّلاع على المحتوى الذي لم يتم تشغيله في العنصر الأول وشريط التقدم للمحتوى الذي تم تشغيله جزئيًا في العنصر الثاني.

يمكن استخدام الثوابت التالية في كلّ من إضافات وصف MediaItem وMediaMetadata إضافات:

يمكن استخدام الثوابت التالية فقط في MediaItem إضافات الوصف:

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

يوضح مقتطف الرمز التالي كيفية عرض المؤشرات لعنصر وسائط فاضح مكتمل بنسبة 70%:

Kotlin

import androidx.media.utils.MediaConstants

val extras = Bundle()
extras.putLong(
    MediaConstants.METADATA_KEY_IS_EXPLICIT,
    MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
extras.putInt(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED)
extras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.7)
val description =
    MediaDescriptionCompat.Builder()
        .setMediaId(/*...*/)
        .setTitle(resources.getString(/*...*/))
        .setExtras(extras)
        .build()
return MediaBrowserCompat.MediaItem(description, /* flags */)

Java

import androidx.media.utils.MediaConstants;

Bundle extras = new Bundle();
extras.putLong(
    MediaConstants.METADATA_KEY_IS_EXPLICIT,
    MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT);
extras.putInt(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED);
extras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.7);
MediaDescriptionCompat description =
    new MediaDescriptionCompat.Builder()
        .setMediaId(/*...*/)
        .setTitle(resources.getString(/*...*/))
        .setExtras(extras)
        .build();
return new MediaBrowserCompat.MediaItem(description, /* flags */);

لعرض مؤشرات لعنصر الوسائط الذي يتم تشغيله حاليًا، يمكنك تعريف قيم Long للسمة METADATA_KEY_IS_EXPLICIT أو EXTRA_DOWNLOAD_STATUS في MediaMetadataCompat من mediaSession. لا يمكنك عرض مؤشري DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS أو DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE في طريقة عرض التشغيل.

يعرض مقتطف الرمز التالي كيفية الإشارة إلى أنّ الأغنية الحالية في عرض التشغيل عبارة عن محتوى فاضح وتم تنزيلها:

Kotlin

import androidx.media.utils.MediaConstants

mediaSession.setMetadata(
    MediaMetadataCompat.Builder()
        .putString(
            MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Song Name")
        .putString(
            MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name")
        .putString(
            MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
            albumArtUri.toString())
        .putLong(
            MediaConstants.METADATA_KEY_IS_EXPLICIT,
            MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
        .putLong(
            MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
            MediaDescriptionCompat.STATUS_DOWNLOADED)
        .build())

Java

import androidx.media.utils.MediaConstants;

mediaSession.setMetadata(
    new MediaMetadataCompat.Builder()
        .putString(
            MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Song Name")
        .putString(
            MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name")
        .putString(
            MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
            albumArtUri.toString())
        .putLong(
            MediaConstants.METADATA_KEY_IS_EXPLICIT,
            MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
        .putLong(
            MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
            MediaDescriptionCompat.STATUS_DOWNLOADED)
        .build());

تعديل شريط التقدّم في طريقة عرض التصفّح أثناء تشغيل المحتوى

كما ذكرنا سابقًا، يمكنك استخدام عنصر DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE الإضافي لعرض شريط تقدّم لمحتوى تم تشغيله جزئيًا في طريقة عرض "التصفّح". ومع ذلك، إذا استمر المستخدم في تشغيل المحتوى الذي تم تشغيله جزئيًا من Android Auto أو Android Automotive، فسيصبح هذا المؤشر غير دقيق بمرور الوقت.

لكي يبقى شريط التقدّم محدّثًا في Android Auto وAndroid Automotive، يمكنك توفير معلومات إضافية في MediaMetadataCompat وPlaybackStateCompat لربط المحتوى الجاري بعناصر الوسائط في طريقة عرض التصفّح. يجب استيفاء المتطلبات التالية لكي يتضمن عنصر الوسائط شريط تقدم يتم تحديثه تلقائيًا:

يوضّح مقتطف الرمز التالي كيفية الإشارة إلى أنّ العنصر الذي يتم تشغيله حاليًا مرتبط بعنصر في طريقة عرض التصفّح:

Kotlin

import androidx.media.utils.MediaConstants

// When the MediaItem is constructed to show in the browse view.
// Suppose the item was 25% complete when the user launched the browse view.
val mediaItemExtras = Bundle()
mediaItemExtras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.25)
val description =
    MediaDescriptionCompat.Builder()
        .setMediaId("my-media-id")
        .setExtras(mediaItemExtras)
        // ...and any other setters.
        .build()
return MediaBrowserCompat.MediaItem(description, /* flags */)

// Elsewhere, when the user has selected MediaItem for playback.
mediaSession.setMetadata(
    MediaMetadataCompat.Builder()
        .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "my-media-id")
        // ...and any other setters.
        .build())

val playbackStateExtras = Bundle()
playbackStateExtras.putString(
    MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID, "my-media-id")
mediaSession.setPlaybackState(
    PlaybackStateCompat.Builder()
        .setExtras(playbackStateExtras)
        // ...and any other setters.
        .build())

Java

import androidx.media.utils.MediaConstants;

// When the MediaItem is constructed to show in the browse view.
// Suppose the item was 25% complete when the user launched the browse view.
Bundle mediaItemExtras = new Bundle();
mediaItemExtras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.25);
MediaDescriptionCompat description =
    new MediaDescriptionCompat.Builder()
        .setMediaId("my-media-id")
        .setExtras(mediaItemExtras)
        // ...and any other setters.
        .build();
return MediaBrowserCompat.MediaItem(description, /* flags */);

// Elsewhere, when the user has selected MediaItem for playback.
mediaSession.setMetadata(
    new MediaMetadataCompat.Builder()
        .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "my-media-id")
        // ...and any other setters.
        .build());

Bundle playbackStateExtras = new Bundle();
playbackStateExtras.putString(
    MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID, "my-media-id");
mediaSession.setPlaybackState(
    new PlaybackStateCompat.Builder()
        .setExtras(playbackStateExtras)
        // ...and any other setters.
        .build());

الشكل 5. عرض تشغيل مع خيار "نتائج البحث" لعرض ملفات الوسائط المرتبطة بالبحث الصوتي للمستخدم

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

لعرض نتائج بحث قابلة للتصفّح، ضمِّن المفتاح الثابت BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED في حزمة العناصر الإضافية لطريقة onGetRoot() الخدمة، مع الربط بالقيمة المنطقية true.

يوضّح مقتطف الرمز التالي كيفية تفعيل الدعم في الطريقة onGetRoot():

Kotlin

import androidx.media.utils.MediaConstants

@Nullable
fun onGetRoot(
    @NonNull clientPackageName: String,
    clientUid: Int,
    @Nullable rootHints: Bundle
): BrowserRoot {
    val extras = Bundle()
    extras.putBoolean(
        MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true)
    return BrowserRoot(ROOT_ID, extras)
}

Java

import androidx.media.utils.MediaConstants;

@Nullable
@Override
public BrowserRoot onGetRoot(
    @NonNull String clientPackageName,
    int clientUid,
    @Nullable Bundle rootHints) {
    Bundle extras = new Bundle();
    extras.putBoolean(
        MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true);
    return new BrowserRoot(ROOT_ID, extras);
}

لبدء عرض نتائج بحث، يمكنك إلغاء طريقة onSearch() في خدمة متصفح الوسائط. يعيد Android Auto ونظام التشغيل Android Automotive إعادة توجيه عبارات بحث المستخدِم إلى هذه الطريقة عندما يستدعي مستخدم واجهة طلب بحث أو عنصر "نتائج البحث".

يمكنك تنظيم نتائج البحث من طريقة onSearch() الخاصة بخدمتك باستخدام عناصر العناوين لجعلها أكثر قابلية للتصفّح. على سبيل المثال، إذا كان تطبيقك يشغّل الموسيقى، يمكنك تنظيم نتائج البحث حسب الألبوم والفنّان والأغاني.

يعرض مقتطف الرمز التالي تنفيذًا بسيطًا للطريقة onSearch():

Kotlin

fun onSearch(query: String, extras: Bundle) {
  // Detach from results to unblock the caller (if a search is expensive).
  result.detach()
  object:AsyncTask() {
    internal var searchResponse:ArrayList
    internal var succeeded = false
    protected fun doInBackground(vararg params:Void):Void {
      searchResponse = ArrayList()
      if (doSearch(query, extras, searchResponse))
      {
        succeeded = true
      }
      return null
    }
    protected fun onPostExecute(param:Void) {
      if (succeeded)
      {
        // Sending an empty List informs the caller that there were no results.
        result.sendResult(searchResponse)
      }
      else
      {
        // This invokes onError() on the search callback.
        result.sendResult(null)
      }
      return null
    }
  }.execute()
}
// Populates resultsToFill with search results. Returns true on success or false on error.
private fun doSearch(
    query: String,
    extras: Bundle,
    resultsToFill: ArrayList
): Boolean {
  // Implement this method.
}

Java

@Override
public void onSearch(final String query, final Bundle extras,
                        Result<List<MediaItem>> result) {

  // Detach from results to unblock the caller (if a search is expensive).
  result.detach();

  new AsyncTask<Void, Void, Void>() {
    List<MediaItem> searchResponse;
    boolean succeeded = false;
    @Override
    protected Void doInBackground(Void... params) {
      searchResponse = new ArrayList<MediaItem>();
      if (doSearch(query, extras, searchResponse)) {
        succeeded = true;
      }
      return null;
    }

    @Override
    protected void onPostExecute(Void param) {
      if (succeeded) {
       // Sending an empty List informs the caller that there were no results.
       result.sendResult(searchResponse);
      } else {
        // This invokes onError() on the search callback.
        result.sendResult(null);
      }
    }
  }.execute()
}

/** Populates resultsToFill with search results. Returns true on success or false on error. */
private boolean doSearch(String query, Bundle extras, ArrayList<MediaItem> resultsToFill) {
    // Implement this method.
}

إجراءات التصفُّح المخصَّصة

إجراء تصفّح واحد مخصّص.

الشكل 6. إجراء تصفّح واحد مخصّص

تتيح لك "إجراءات التصفّح المخصَّصة" إضافة رموز وتصنيفات مخصّصة إلى عناصر MediaItem في تطبيقك ضمن تطبيق الوسائط في السيارة، والتعامل مع تفاعلات المستخدمين مع هذه الإجراءات. ويتيح لك ذلك توسيع وظائف تطبيق الوسائط بعدة طرق، مثل إضافة إجراءات "تنزيل" أو "إضافة إلى قائمة المحتوى التالي" أو "تشغيل راديو" أو "إضافة إلى المفضلة" أو "إزالة".

قائمة كاملة لإجراءات التصفّح المخصّصة

الشكل 7. المجموعة الكاملة لإجراء التصفّح المخصّص

إذا كانت هناك إجراءات مخصّصة أكثر مما يسمح به المصنّع الأصلي للجهاز، سيتم عرض قائمة كاملة للمستخدم.

كيف تعمل هذه الإعلانات؟

يتم تحديد كل إجراء تصفّح مخصّص من خلال:

  • معرّف إجراء (معرّف سلسلة فريد)
  • تصنيف الإجراء (النص المعروض للمستخدم)
  • عنوان URI لرمز الإجراء (متجه قابل للرسم يمكن تلوينه)

يمكنك تحديد قائمة من إجراءات التصفّح المخصَّصة على مستوى العالم كجزء من BrowseRoot. يمكنك بعد ذلك إرفاق مجموعة فرعية من هذه الإجراءات بـ MediaItem.

عندما يتفاعل مستخدم مع إجراء تصفّح مخصّص، يتلقّى تطبيقك معاودة الاتصال في onCustomAction(). يمكنك بعد ذلك معالجة الإجراء وتعديل قائمة الإجراءات للسمة MediaItem إذا لزم الأمر. ويكون ذلك مفيدًا في الإجراءات التي يتم تحديدها مثل "المُفضَّلة" و "التنزيل". بالنسبة إلى الإجراءات التي لا تحتاج إلى تعديل، مثل "تشغيل الراديو"، لن تحتاج إلى تعديل قائمة الإجراءات.

إجراءات التصفُّح المخصَّصة في جذر عقدة التصفُّح

الشكل 8. شريط أدوات إجراء التصفّح المخصّص

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

كيفية تنفيذ إجراءات التصفّح المخصّص

في ما يلي خطوات إضافة إجراءات التصفُّح المخصَّصة إلى مشروعك:

  1. إلغاء طريقتين في تنفيذ MediaBrowserServiceCompat:
  2. تحليل حدود الإجراءات في وقت التشغيل:
    • في onGetRoot()، يمكنك الحصول على أقصى عدد مسموح به من الإجراءات لكل MediaItem باستخدام المفتاح BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT في rootHints Bundle. يشير الحد 0 إلى أن الميزة غير لا يدعمها النظام.
  3. أنشئ القائمة العامة لإجراءات التصفّح المخصَّصة:
    • بالنسبة إلى كل إجراء، أنشِئ عنصر Bundle باستخدام المفاتيح التالية: * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID: رقم تعريف الإجراء * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL: تصنيف الإجراء * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI: معرّف الموارد المنتظم (URI) لرمز الإجراء * أضِف جميع كائنات Bundle للإجراء إلى القائمة.
  4. إضافة القائمة العامة إلى BrowseRoot:
  5. إضافة إجراءات إلى كائنات MediaItem:
    • يمكنك إضافة إجراءات إلى كائنات MediaItem فردية عن طريق تضمين قائمة معرّفات الإجراءات في إضافات MediaDescriptionCompat باستخدام المفتاح DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST. يجب أن تكون هذه القائمة مجموعة فرعية من قائمة الإجراءات العامة التي حدّدتها في BrowseRoot.
  6. التعامل مع الإجراءات وعرض مستوى التقدّم أو النتائج:
    • في onCustomAction، تعامل مع الإجراء استنادًا إلى معرّف الإجراء وأي بيانات أخرى تحتاج إليها. يمكنك الحصول على معرّف MediaItem الذي أدّى إلى تشغيل الإجراء من العناصر الإضافية باستخدام المفتاح EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID..
    • يمكنك تعديل قائمة الإجراءات لـ MediaItem من خلال تضمين المفتاح EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM في مستوى التقدم أو حزمة النتائج.

إليك بعض التغييرات التي يمكنك إجراؤها في BrowserServiceCompat لبدء استخدام إجراءات التصفّح المخصّصة.

إلغاء BrowserServiceCompat

يجب إلغاء الطُرق التالية في "MediaBrowserServiceCompat".

public void onLoadItem(String itemId, @NonNull Result<MediaBrowserCompat.MediaItem> result)

public void onCustomAction(@NonNull String action, Bundle extras, @NonNull Result<Bundle> result)

الحد الأقصى لإجراءات التحليل

يجب التحقق لمعرفة عدد إجراءات التصفّح المخصصة المتوافقة.

public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) {
    rootHints.getInt(
            MediaConstants.BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT, 0)
}

إنشاء إجراء تصفّح مخصّص

يجب تعبئة كل إجراء في Bundle منفصل.

  • معرّف الإجراء
    bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID,
                    "<ACTION_ID>")
    
  • تصنيف الإجراء
    bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL,
                    "<ACTION_LABEL>")
    
  • معرّف الموارد المنتظم (URI) لرمز الإجراء
    bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI,
                    "<ACTION_ICON_URI>")
    

إضافة إجراءات تصفّح مخصص إلى Parceable ArrayList

إضافة جميع عناصر إجراءات التصفّح المخصّصة Bundle في ArrayList

private ArrayList<Bundle> createCustomActionsList(
                                        CustomBrowseAction browseActions) {
    ArrayList<Bundle> browseActionsBundle = new ArrayList<>();
    for (CustomBrowseAction browseAction : browseActions) {
        Bundle action = new Bundle();
        action.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID,
                browseAction.mId);
        action.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL,
                getString(browseAction.mLabelResId));
        action.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI,
                browseAction.mIcon);
        browseActionsBundle.add(action);
    }
    return browseActionsBundle;
}

إضافة قائمة إجراءات التصفُّح المخصَّصة إلى جذر التصفُّح

public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
                             Bundle rootHints) {
    Bundle browserRootExtras = new Bundle();
    browserRootExtras.putParcelableArrayList(
            BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST,
            createCustomActionsList()));
    mRoot = new BrowserRoot(ROOT_ID, browserRootExtras);
    return mRoot;
}

إضافة إجراءات إلى MediaItem

MediaDescriptionCompat buildDescription (long id, String title, String subtitle,
                String description, Uri iconUri, Uri mediaUri,
                ArrayList<String> browseActionIds) {

    MediaDescriptionCompat.Builder bob = new MediaDescriptionCompat.Builder();
    bob.setMediaId(id);
    bob.setTitle(title);
    bob.setSubtitle(subtitle);
    bob.setDescription(description);
    bob.setIconUri(iconUri);
    bob.setMediaUri(mediaUri);

    Bundle extras = new Bundle();
    extras.putStringArrayList(
          DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST,
          browseActionIds);

    bob.setExtras(extras);
    return bob.build();
}
MediaItem mediaItem = new MediaItem(buildDescription(...), flags);

إنشاء نتيجة واحدة (onCustomAction)

  • تحليل MediaId من Bundle extras:
    @Override
    public void onCustomAction(
              @NonNull String action, Bundle extras, @NonNull Result<Bundle> result){
      String mediaId = extras.getString(MediaConstans.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID);
    }
    
  • للحصول على النتائج غير المتزامنة، يمكنك فصل النتيجة. result.detach()
  • إنشاء حزمة النتائج
    • رسالة إلى المستخدم
      mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE,
                mContext.getString(stringRes))
      
    • تعديل العنصر(استخدامه لتعديل الإجراءات في عنصر)
      mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM, mediaId);
      
    • فتح طريقة عرض التشغيل
      //Shows user the PBV without changing the playback state
      mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM, null);
      
    • تعديل عقدة التصفُّح
      //Change current browse node to mediaId
      mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE, mediaId);
      
  • إذا حدث خطأ، اتّصِل بالرقم result.sendError(resultBundle)..
  • إذا تم تعديل مستوى التقدّم، اتّصِل بالرقم result.sendProgressUpdate(resultBundle).
  • يمكنك إنهاء العملية من خلال الاتصال بالرقم result.sendResult(resultBundle).

تعديل حالة الإجراء

وباستخدام طريقة result.sendProgressUpdate(resultBundle) مع مفتاح EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM، يمكنك تعديل MediaItem لتعكس الحالة الجديدة للإجراء. وهذا يتيح لك تقديم ملاحظات في الوقت الفعلي للمستخدم حول التقدم المحرز ونتيجة عمله.

مثال: إجراء التنزيل

في ما يلي مثال على كيفية استخدام هذه الميزة لتنفيذ إجراء تنزيل بثلاث حالات:

  1. التنزيل: هذه هي الحالة الأولية للإجراء. وعندما يختار المستخدم هذا الإجراء، يمكنك تبديله إلى "جارٍ التنزيل" وطلب الرمز sendProgressUpdate من أجل تعديل واجهة المستخدم.
  2. التنزيل: تشير هذه الحالة إلى أنّ عملية التنزيل قيد التقدّم. يمكنك استخدام هذه الحالة لإظهار شريط التقدم أو مؤشر آخر للمستخدم.
  3. تم التنزيل: تشير هذه الحالة إلى اكتمال عملية التنزيل. عند انتهاء عملية التنزيل، يمكنك تبديل "التنزيل" بـ "تم التنزيل" والاتصال sendResult باستخدام مفتاح EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM للإشارة إلى ضرورة إعادة تحميل العنصر. بالإضافة إلى ذلك، يمكنك استخدام مفتاح EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE لعرض رسالة نجاح للمستخدم.

يتيح لك هذا المنهج تقديم ملاحظات واضحة للمستخدم حول عملية التنزيل وحالتها الحالية. يمكنك إضافة المزيد من التفاصيل باستخدام الرموز لعرض حالات تنزيل 25% أو 50% أو 75%.

مثال: إجراء مفضّل

مثال آخر هو الإجراء المفضل ذو حالتين:

  1. المفضلة: يتم عرض هذا الإجراء للعناصر غير الموجودة في قائمة المفضلة لدى المستخدم. عندما يختار المستخدم هذا الإجراء، يمكنك تبديله بالخيار "المُفضَّل" واستدعاء sendResult باستخدام مفتاح EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM لتحديث واجهة المستخدم.
  2. المفضّلة: يتم عرض هذا الإجراء للعناصر في قائمة "المفضّلة" لدى المستخدم. عندما يختار المستخدم هذا الإجراء، يمكنك تبديله إلى "المُفضَّلة" واستدعاء sendResult باستخدام مفتاح EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM لتحديث واجهة المستخدم.

يوفر هذا النهج طريقة واضحة ومتسقة للمستخدمين لإدارة العناصر المفضلة لديهم.

توضّح هذه الأمثلة مرونة إجراءات التصفُّح المخصَّصة وكيفية استخدامها لتنفيذ مجموعة متنوعة من الوظائف مع تقديم ملاحظات في الوقت الفعلي لتحسين تجربة المستخدم في تطبيق وسائط السيارة.

للحصول على مثال كامل لتنفيذ هذه الميزة، يمكنك الرجوع إلى مشروع TestMediaApp.

تفعيل عنصر التحكّم في التشغيل

يرسل نظاما Android Auto وAndroid Automotive أوامر التحكّم في التشغيل من خلال MediaSessionCompat ضِمن الخدمة. يجب تسجيل جلسة وتنفيذ طرق معاودة الاتصال المرتبطة بها.

.

تسجيل جلسة وسائط

في طريقة onCreate() الخاصة بخدمة متصفح الوسائط، أنشِئ MediaSessionCompat، ثم سجِّل جلسة الوسائط عن طريق طلب الرمز setSessionToken().

يوضح مقتطف الرمز التالي كيفية إنشاء جلسة وسائط وتسجيلها:

Kotlin

override fun onCreate() {
    super.onCreate()
    ...
    // Start a new MediaSession.
    val session = MediaSessionCompat(this, "session tag").apply {
        // Set a callback object that implements MediaSession.Callback
        // to handle play control requests.
        setCallback(MyMediaSessionCallback())
    }
    sessionToken = session.sessionToken
    ...
}

Java

public void onCreate() {
    super.onCreate();
    ...
    // Start a new MediaSession.
    MediaSessionCompat session = new MediaSessionCompat(this, "session tag");
    setSessionToken(session.getSessionToken());

    // Set a callback object that implements MediaSession.Callback
    // to handle play control requests.
    session.setCallback(new MyMediaSessionCallback());
    ...
}

عند إنشاء كائن جلسة وسائط، يمكنك تعيين كائن استدعاء يُستخدم للتعامل مع طلبات التحكم في التشغيل. يمكنك إنشاء كائن معاودة الاتصال هذا من خلال توفير تنفيذ للفئة MediaSessionCompat.Callback لتطبيقك. يناقش القسم التالي كيفية تنفيذ هذا الكائن.

تنفيذ طلبات التشغيل

عندما يطلب المستخدم تشغيل عنصر وسائط من تطبيقك، يستخدم نظام التشغيل Android Automotive وAndroid Auto الفئة MediaSessionCompat.Callback من عنصر MediaSessionCompat في تطبيقك الذي حصل عليه من خدمة متصفِّح الوسائط في تطبيقك. عندما يريد المستخدم التحكّم في تشغيل المحتوى، مثل إيقاف التشغيل مؤقتًا أو التخطّي إلى المسار التالي، يستدعي Android Auto ونظام التشغيل Android Automotive إحدى طرق عنصر معاودة الاتصال.

لمعالجة تشغيل المحتوى، على تطبيقك تمديد الفئة MediaSessionCompat.Callback المجرّدة وتنفيذ الطرق التي يتوافق معها تطبيقك.

نفِّذ جميع طرق معاودة الاتصال التالية المناسبة لنوع المحتوى الذي يقدمه تطبيقك:

onPrepare()
تم استدعاؤه عند تغيير مصدر الوسائط. يستدعي نظام التشغيل Android Automotive أيضًا هذه الطريقة بعد بدء التشغيل مباشرةً. يجب أن ينفِّذ تطبيق الوسائط هذه الطريقة.
onPlay()
يتم استدعاؤه إذا اختار المستخدم التشغيل بدون اختيار عنصر معيّن. يجب أن يشغّل التطبيق المحتوى التلقائي، أو إذا تم إيقاف التشغيل مؤقتًا باستخدام onPause()، سيستأنف التطبيق التشغيل.

ملاحظة: يجب ألا يبدأ تطبيقك تشغيل الموسيقى تلقائيًا عند اتصال نظام التشغيل Android Automotive أو Android Auto بخدمة متصفِّح الوسائط. للحصول على مزيد من المعلومات، يمكنك الاطّلاع على القسم حول ضبط حالة التشغيل الأولية.

onPlayFromMediaId()
يتم استدعاؤه عندما يختار المستخدم تشغيل عنصر معيّن. يتم تمرير الطريقة إلى المعرّف الذي خصّصته خدمة متصفّح الوسائط لعنصر الوسائط في التدرج الهرمي للمحتوى.
onPlayFromSearch()
يتم استدعاؤه عندما يختار المستخدم التشغيل من طلب بحث. يجب أن يتخذ التطبيق خيارًا مناسبًا استنادًا إلى سلسلة البحث التي تم تمريرها.
onPause()
يتم استدعاؤه عندما يختار المستخدم إيقاف التشغيل مؤقتًا.
onSkipToNext()
يتم استدعاؤه عندما يختار المستخدم التخطّي إلى العنصر التالي.
onSkipToPrevious()
يتم استدعاؤه عندما يختار المستخدم التخطّي إلى العنصر السابق.
onStop()
يتم استدعاؤه عندما يختار المستخدم إيقاف التشغيل.

يمكنك إلغاء هذه الطرق في تطبيقك لتوفير أي وظائف مطلوبة. لن تحتاج إلى استخدام طريقة إذا لم تكن وظائفها متوافقة مع تطبيقك. على سبيل المثال، إذا كان تطبيقك يبث بثًا مباشرًا، مثل بث رياضي، لن تحتاج إلى استخدام طريقة onSkipToNext(). يمكنك بدلاً من ذلك استخدام طريقة التنفيذ التلقائية لـ onSkipToNext().

لا يحتاج تطبيقك إلى أي منطق خاص لتشغيل المحتوى من خلال مكبرات الصوت في السيارة. عندما يتلقى تطبيقك طلبًا لتشغيل المحتوى، يمكنه تشغيل الصوت بنفس الطريقة التي يتم بها تشغيل المحتوى من خلال مكبرات الصوت أو سماعات الرأس لهاتف المستخدم. يرسل Android Auto ونظام التشغيل Android Automotive تلقائيًا المحتوى الصوتي إلى نظام السيارة لتشغيله عبر مكبرات الصوت بالسيارة.

لمزيد من المعلومات حول تشغيل المحتوى الصوتي، يُرجى الاطّلاع على نظرة عامة على MediaPlayer ونظرة عامة على تطبيق الصوت ونظرة عامة على ExoPlayer.

ضبط إجراءات التشغيل العادي

يعرض Android Auto ونظام التشغيل Android Automotive عناصر التحكّم في التشغيل استنادًا إلى الإجراءات التي تم تفعيلها في العنصر PlaybackStateCompat.

يجب أن يتيح تطبيقك تلقائيًا تنفيذ الإجراءات التالية:

بالإضافة إلى ذلك، يمكن لتطبيقك تنفيذ الإجراءات التالية إذا كانت ذات صلة بمحتوى التطبيق:

بالإضافة إلى ذلك، لديك خيار إنشاء قائمة انتظار التشغيل التي يمكن عرضها للمستخدم، ولكنها ليست مطلوبة. للقيام بذلك، عليك استدعاء الطريقتَين setQueue() وsetQueueTitle()، وتفعيل إجراء ACTION_SKIP_TO_QUEUE_ITEM وتحديد معاودة الاتصال onSkipToQueueItem().

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

أزرار عرض Android Auto ونظام التشغيل Android Automotive لكل إجراء مفعَّل بالإضافة إلى قائمة انتظار التشغيل. عند النقر على الأزرار، يستدعي النظام معاودة الاتصال المقابلة لها من MediaSessionCompat.Callback.

حجز مساحة غير مستخدَمة

يحجز تطبيق Android Auto ونظام التشغيل Android Automotive مساحة في واجهة المستخدم لتنفيذ الإجراءَين ACTION_SKIP_TO_PREVIOUS وACTION_SKIP_TO_NEXT. إذا كان تطبيقك لا يتيح إحدى هذه الوظائف، سيستخدم Android Auto ونظام التشغيل Android Automotive المساحة لعرض أي إجراءات مخصّصة تنشئها.

إذا كنت لا ترغب في ملء هذه المساحات بإجراءات مخصصة، يمكنك حجزها بحيث يترك نظاما التشغيل Android Auto ونظام التشغيل Android Automotive المساحة فارغة إذا كان تطبيقك لا يتيح استخدام الوظيفة المقابلة. للقيام بذلك، عليك استدعاء الطريقة setExtras() مع حزمة إضافية تحتوي على ثوابت تتوافق مع الدوال المحجوزة. SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT يتجاوب مع ACTION_SKIP_TO_NEXT، وSESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV يتطابق مع ACTION_SKIP_TO_PREVIOUS. استخدِم هذه الثوابت كمفاتيح في الحزمة واستخدِم true المنطقية لقيمها.

ضبط حالة التشغيل الأولية

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

لتنفيذ ذلك، اضبط PlaybackStateCompat الأولي لجلسة الوسائط على STATE_STOPPED أو STATE_PAUSED أو STATE_NONE أو STATE_ERROR.

تستمر جلسات الوسائط في Android Auto ونظام التشغيل Android Automotive فقط لمدة القيادة، لذلك يبدأ المستخدمون هذه الجلسات ويوقفونها بشكل متكرر. لتعزيز تجربة سلسة بين محركات الأقراص، يجب تتبع حالة الجلسة السابقة للمستخدم، بحيث عندما يتلقى تطبيق الوسائط طلب السيرة الذاتية، يمكنه تلقائيًا المتابعة من حيث توقف، على سبيل المثال آخر عنصر من الوسائط تم تشغيله وPlaybackStateCompat وقائمة المحتوى التالي.

إضافة إجراءات تشغيل مخصّصة

يمكنك إضافة إجراءات تشغيل مخصّصة لعرض الإجراءات الإضافية التي يتيحها تطبيق الوسائط. إذا سمحت المساحة (ولم يتم حجزها)، يضيف Android الإجراءات المخصّصة إلى عناصر التحكّم في النقل. بخلاف ذلك، سيتم عرض الإجراءات المخصصة في القائمة الكاملة. تظهر الإجراءات المخصّصة بترتيب إضافتها إلى PlaybackStateCompat.

استخدِم الإجراءات المخصّصة لتوفير سلوك مختلف عن الإجراءات العادية. ولا تستخدمها لاستبدال أو تكرار الإجراءات القياسية.

يمكنك إضافة إجراءات مخصّصة باستخدام الطريقة addCustomAction() في الفئة PlaybackStateCompat.Builder.

يعرض مقتطف الرمز التالي كيفية إضافة إجراء مخصّص "بدء قناة راديو":

Kotlin

stateBuilder.addCustomAction(
    PlaybackStateCompat.CustomAction.Builder(
        CUSTOM_ACTION_START_RADIO_FROM_MEDIA,
        resources.getString(R.string.start_radio_from_media),
        startRadioFromMediaIcon
    ).run {
        setExtras(customActionExtras)
        build()
    }
)

Java

stateBuilder.addCustomAction(
    new PlaybackStateCompat.CustomAction.Builder(
        CUSTOM_ACTION_START_RADIO_FROM_MEDIA,
        resources.getString(R.string.start_radio_from_media),
        startRadioFromMediaIcon)
    .setExtras(customActionExtras)
    .build());

للاطّلاع على مثال أكثر تفصيلاً لهذه الطريقة، يمكنك الاطّلاع على الطريقة setCustomAction() في نموذج تطبيق Universal Android Music Player على GitHub.

بعد إنشاء الإجراء المخصّص، يمكن أن تستجيب جلسة الوسائط على الإجراء من خلال تجاوز طريقة onCustomAction().

يعرض مقتطف الرمز التالي كيفية استجابة تطبيقك لإجراء "بدء قناة راديو":

Kotlin

override fun onCustomAction(action: String, extras: Bundle?) {
    when(action) {
        CUSTOM_ACTION_START_RADIO_FROM_MEDIA -> {
            ...
        }
    }
}

Java

@Override
public void onCustomAction(@NonNull String action, Bundle extras) {
    if (CUSTOM_ACTION_START_RADIO_FROM_MEDIA.equals(action)) {
        ...
    }
}

للاطّلاع على مثال أكثر تفصيلاً لهذه الطريقة، يمكنك الاطّلاع على الطريقة onCustomAction في نموذج تطبيق Universal Android Music Player على GitHub.

رموز الإجراءات المخصصة

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

إذا كان الإجراء المخصّص له حالة مختلفة، مثل تفعيل أو إيقاف إعداد التشغيل، يمكنك توفير رموز مختلفة للحالات المختلفة، لكي يلاحظ المستخدمون تغييرًا عند اختيار الإجراء.

توفير أنماط رموز بديلة للإجراءات غير المفعّلة

في حال عدم توفُّر إجراء مخصَّص للسياق الحالي، يمكنك تبديل رمز الإجراء المخصّص برمز بديل يوضّح أنّ الإجراء غير مفعَّل.

الشكل 6. نماذج من رموز الإجراءات المخصصة غير التقليدية.

تحديد تنسيق الصوت

للإشارة إلى أنّ الوسائط التي يتم تشغيلها حاليًا تستخدم تنسيقًا صوتيًا خاصًا، يمكنك تحديد الرموز التي يتم عرضها في السيارات التي تتوافق مع هذه الميزة. يمكنك ضبط KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI وKEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI في حزمة الميزات الإضافية لعنصر الوسائط الذي يتم تشغيله حاليًا (تم تمريره إلى MediaSession.setMetadata()). تأكد من ضبط كلا الخيارين الإضافيين لتلائم التنسيقات المختلفة.

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

يمكنك تهيئة عنصر الوسائط المشغَّل حاليًا بحيث يكون العنوان الفرعي أو الوصف أو كليهما روابط إلى عناصر وسائط أخرى. وهذا يتيح للمستخدم الانتقال بسرعة إلى العناصر ذات الصلة، على سبيل المثال، قد ينتقل إلى أغانٍ أخرى للفنان نفسه، أو حلقات أخرى من هذا البودكاست، إلخ. وإذا كانت السيارة تدعم هذه الميزة، فيمكن للمستخدمين النقر على الرابط للتصفح إلى ذلك المحتوى.

لإضافة روابط، اضبط البيانات الوصفية KEY_SUBTITLE_LINK_MEDIA_ID (للرابط من العنوان الفرعي) أو KEY_DESCRIPTION_LINK_MEDIA_ID (للرابط من الوصف). لمعرفة التفاصيل، يُرجى الاطّلاع على المستندات المرجعية لحقول بيانات التعريف هذه.

إتاحة الإجراءات الصوتية

يجب أن يتيح تطبيق الوسائط تنفيذ الإجراءات الصوتية للمساعدة في توفير تجربة آمنة ومريحة للسائقين تساعد في تقليل مصادر تشتيت الانتباه. على سبيل المثال، إذا كان تطبيقك يشغّل عنصر وسائط واحدًا، يمكن للمستخدم قول"تشغيل [عنوان الأغنية]" لطلب تشغيل أغنية مختلفة من التطبيق بدون النظر إلى شاشة السيارة أو لمسها. يمكن للمستخدمين بدء طلبات البحث من خلال النقر على الأزرار المناسبة على عجلة القيادة أو قول الكلمات المفتاح "OK Google".

.

عندما يرصد نظام التشغيل Android Auto أو Android Automotive إجراءً صوتيًا ويفسّره، يتم تنفيذ هذا الإجراء الصوتي في التطبيق من خلال onPlayFromSearch(). وعند تلقّي معاودة الاتصال هذه، يعثر التطبيق على محتوى يطابق سلسلة "query" ويبدأ التشغيل.

ويمكن للمستخدمين تحديد فئات مختلفة من المصطلحات في استعلامهم، وهي: النوع أو الفنان أو الألبوم أو اسم الأغنية أو المحطة الإذاعية أو قائمة التشغيل وغيرها. عند توفير إتاحة البحث، يجب مراعاة جميع الفئات المناسبة لتطبيقك. إذا اكتشف Android Auto أو Android Automotive أن طلب بحث معيّن يتناسب مع فئات معيّنة، يتم إلحاق إضافات في معلَمة extras. يمكن إرسال العناصر الإضافية التالية:

استخدِم سلسلة query فارغة يمكن إرسالها بواسطة Android Auto أو Android Automotive إذا لم يحدّد المستخدم عبارات البحث. على سبيل المثال، إذا قال المستخدم "تشغيل بعض الموسيقى". في هذه الحالة، قد يختار تطبيقك بدء مقطع صوتي تم تشغيله مؤخرًا أو تم اقتراحه حديثًا.

إذا تعذّرت معالجة عملية بحث بسرعة، لا تحظر في onPlayFromSearch(). بدلاً من ذلك، يمكنك ضبط حالة التشغيل على STATE_CONNECTING وإجراء البحث في سلسلة محادثات غير متزامنة.

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

بالإضافة إلى طلبات البحث "play"، يتعرّف Android Auto ونظام التشغيل Android Automotive على طلبات البحث الصوتية للتحكم في التشغيل مثل "إيقاف الموسيقى مؤقتًا" و "الأغنية التالية"، ويطابق هذه الطلبات مع عمليات معاودة الاتصال بجلسة وسائط مناسبة، مثل onPause() وonSkipToNext().

للاطّلاع على مثال تفصيلي حول كيفية تنفيذ إجراءات التشغيل التي تستخدم الصوت في تطبيقك، يُرجى مراجعة "مساعد Google" وتطبيقات الوسائط.

تنفيذ إجراءات الوقاية من مصادر التشتيت

نظرًا لأن هاتف المستخدم متصل بمكبرات الصوت في السيارة أثناء استخدام Android Auto، فيجب عليك اتخاذ احتياطات إضافية للمساعدة في منع تشتيت انتباه السائق.

إيقاف المنبّهات في السيارة

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

ولاستيفاء هذا الشرط، يمكن لتطبيقك استخدام CarConnection كإشارة قبل تشغيل أي مقطع صوتي. يمكن لتطبيقك التحقّق مما إذا كان الهاتف يتم عرضه على شاشة سيارة من خلال مراقبة LiveData لمعرفة نوع اتصال السيارة والتحقّق مما إذا كانت القيمة تساوي CONNECTION_TYPE_PROJECTION.

إذا كان هاتف المستخدم يتم عرضه، يجب أن تنفّذ تطبيقات الوسائط التي تتوافق مع المنبّهات أحد الإجراءات التالية:

  • إيقاف المنبّه
  • شغِّل المنبّه على STREAM_ALARM واعرض واجهة مستخدم على شاشة الهاتف لإيقاف المنبّه.

التعامل مع إعلانات الوسائط

يعرض Android Auto بشكل تلقائي إشعارًا عندما تتغير البيانات الوصفية للوسائط أثناء جلسة تشغيل الصوت. عندما ينتقل تطبيق وسائط من تشغيل الموسيقى إلى عرض إعلان، فإن عرض إشعار للمستخدم يشتت الانتباه. ولمنع Android Auto من عرض إشعار في هذه الحالة، يجب ضبط مفتاح البيانات الوصفية للوسائط METADATA_KEY_IS_ADVERTISEMENT على METADATA_VALUE_ATTRIBUTE_PRESENT، كما هو موضّح في مقتطف الرمز التالي:

Kotlin

import androidx.media.utils.MediaConstants

override fun onPlayFromMediaId(mediaId: String, extras: Bundle?) {
    MediaMetadataCompat.Builder().apply {
        if (isAd(mediaId)) {
            putLong(
                MediaConstants.METADATA_KEY_IS_ADVERTISEMENT,
                MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
        }
        // ...add any other properties you normally would.
        mediaSession.setMetadata(build())
    }
}

Java

import androidx.media.utils.MediaConstants;

@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
    MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
    if (isAd(mediaId)) {
        builder.putLong(
            MediaConstants.METADATA_KEY_IS_ADVERTISEMENT,
            MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT);
    }
    // ...add any other properties you normally would.
    mediaSession.setMetadata(builder.build());
}

معالجة الأخطاء العامة

عندما يواجه التطبيق خطأً، اضبط حالة التشغيل على STATE_ERROR وقدِّم رسالة خطأ باستخدام الطريقة setErrorMessage(). راجِع PlaybackStateCompat للحصول على قائمة برموز الخطأ التي يمكنك استخدامها عند ضبط رسالة الخطأ. يجب أن تكون رسائل الخطأ موجَّهة للمستخدم، وأن تكون مترجَمة حسب اللغة الحالية للمستخدم. يمكن بعد ذلك Android Auto ونظام التشغيل Android Automotive عرض رسالة الخطأ للمستخدم.

على سبيل المثال، إذا لم يكن المحتوى متاحًا في المنطقة الحالية للمستخدم، يمكنك استخدام رمز الخطأ ERROR_CODE_NOT_AVAILABLE_IN_REGION عند ضبط رسالة الخطأ.

Kotlin

mediaSession.setPlaybackState(
    PlaybackStateCompat.Builder()
        .setState(PlaybackStateCompat.STATE_ERROR)
        .setErrorMessage(PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION, getString(R.string.error_unsupported_region))
        // ...and any other setters.
        .build())

Java

mediaSession.setPlaybackState(
    new PlaybackStateCompat.Builder()
        .setState(PlaybackStateCompat.STATE_ERROR)
        .setErrorMessage(PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION, getString(R.string.error_unsupported_region))
        // ...and any other setters.
        .build());

لمزيد من المعلومات حول حالات الخطأ، راجع استخدام جلسة وسائط: الحالات والأخطاء.

إذا احتاج مستخدم Android Auto إلى فتح تطبيق الهاتف لإصلاح خطأ، عليك تقديم هذه المعلومات للمستخدم في رسالتك. على سبيل المثال، قد تظهر رسالة الخطأ "سجِّل الدخول إلى [اسم تطبيقك]" بدلاً من "يُرجى تسجيل الدخول".

مراجع أخرى