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

يساعدك 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 يستخدم تطبيقك هذه الخدمة لعرض محتواه.
متصفح الوسائط
واجهة برمجة تطبيقات تستخدمها تطبيقات الوسائط لاكتشاف خدمات متصفّحات الوسائط وعرض المحتوى. يستخدم نظاما التشغيل Android Auto وAndroid Automotive متصفّح وسائط للعثور على خدمة متصفّح الوسائط في تطبيقك.
ملف وسائط

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

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

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

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

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

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

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

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

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

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

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

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

<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. على سبيل المثال، إذا كنت تعرض عادةً عنصرًا قابلاً للتشغيل في الجذر، قد تحتاج إلى تداخله ضمن عنصر قابل للتصفّح في الجذر بدلاً من ذلك بسبب قيمة التلميح للرموز المتوافقة.

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

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

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

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

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

  1. أنشئ معرّف موارد منتظم (URI) content:// يتوافق مع معرّف الموارد المنتظم للويب. تُرسِل خدمة متصفّح الوسائط وجلسة الوسائط عنوان URL للمحتوى هذا إلى 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()، تحقّق مما إذا كان هناك ملف موجودًا لمعرّف الموارد المنتظم المقابل. إذا لم يكن الأمر كذلك، نزِّل ملف الصورة واحفظه في ذاكرة التخزين المؤقت. يستخدم مقتطف الرمز البرمجي التالي 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: يتم عرض العناصر المقابلة كعناصر قائمة "category". هذه العناصر هي مثل عناصر القائمة العادية باستثناء أنّه يتم تطبيق الهوامش حول رموز العناصر، لأنّ الرموز تبدو أفضل عندما تكون صغيرة. يجب أن تكون الأيقونات قابلة للرسم متجهات قابلة للرسم. من المتوقّع أن يتم توفير هذا التلميح فقط للعناصر القابلة للتصفّح.
  • 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 يتضمّن عنوانًا ل subgroup "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. عنصر الوسائط D مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  5. ملف الوسائط "هـ" الذي يتضمّن 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 الإضافات:

  • EXTRA_DOWNLOAD_STATUS: يشير إلى حالة تنزيل المحتوى. استخدِم هذا الثابت كأحد المفاتيح، وتكون الثوابت الطويلة التالية هي القيم المحتمَلة:
  • METADATA_KEY_IS_EXPLICIT: تشير إلى ما إذا كان العنصر يتضمّن محتوًى فاضحًا. للإشارة إلى أنّ العنصر واضح، استخدِم هذا الثابت كمفتاح والقيمة الطويلة METADATA_VALUE_ATTRIBUTE_PRESENT.

يمكن استخدام الثوابت التالية فقط في إضافات الوصف 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: عنصر زائد في "الإجراء المخصّص لتصفّح"

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

كيف تعمل هذه الميزة؟

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

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

يمكنك تحديد قائمة بإجراءات التصفّح المخصّصة على مستوى الموقع الإلكتروني كجزء من 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: عنوان URL لرمز الإجراء * أضِف جميع عناصر 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 لتعديل واجهة المستخدم.

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

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

للاطّلاع على مثال كامل لتنفيذ هذه الميزة، يمكنك الرجوع إلى project 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 إحدى طُرق عنصر callback.

لتشغيل المحتوى، يجب أن ينشئ تطبيقك فئة 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 لقيم هذه الثوابت.

ضبط PlaybackState الأولي

عندما يتواصل 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 (لإنشاء رابط من الوصف). للاطّلاع على التفاصيل، يُرجى الاطّلاع على المستندات المرجعية لحقول metadata هذه.

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

يجب أن يتيح تطبيق الوسائط الإجراءات الصوتية للمساعدة في توفير تجربة آمنة ومريحة للسائقين تقلّل من مصادر التشتيت. على سبيل المثال، إذا كان تطبيقك يشغّل ملف وسائط واحدًا، يمكن للمستخدم قول"تشغيل [عنوان الأغنية]" لطلب تشغيل أغنية مختلفة من تطبيقك بدون النظر إلى شاشة السيارة أو لمسها. يمكن للمستخدمين بدء طلبات البحث من خلال النقر على الأزرار المناسبة في عجلة القيادة أو قول الكلمات الرئيسية "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 بحاجة إلى فتح تطبيق هاتفك لحلّ خطأ، قدِّم هذه المعلومات للمستخدم في رسالتك. على سبيل المثال، قد تظهر رسالة الخطأ التالية: "تسجيل الدخول إلى [اسم تطبيقك]" بدلاً من "يُرجى تسجيل الدخول".

مراجع أخرى