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

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

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

  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. ملف الوسائط "هـ" الذي يتضمّن 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>")
    
  • عنوان URL لرمز الإجراء
    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

val customActionExtras = Bundle()
customActionExtras.putInt(
  androidx.media3.session.MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT,
  androidx.media3.session.CommandButton.ICON_RADIO)

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

Java

Bundle customActionExtras = new Bundle();
customActionExtras.putInt(
  androidx.media3.session.MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT,
  androidx.media3.session.CommandButton.ICON_RADIO);

stateBuilder.addCustomAction(
    new PlaybackStateCompat.CustomAction.Builder(
        CUSTOM_ACTION_START_RADIO_FROM_MEDIA,
        resources.getString(R.string.start_radio_from_media),
        startRadioFromMediaIcon) // or R.drawable.media3_icon_radio
    .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.

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

يتطلّب كل إجراء مخصّص تنشئه رمزًا.

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

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

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

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

عندما لا يكون الإجراء المخصّص متاحًا للسياق الحالي، استبدِل رمز الإجراء المخصّص برمز بديل يشير إلى أنّ الإجراء مُعطل.

الشكل 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 وأدخِل طلب البحث في سلسلة محادثات غير متزامنة.

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

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

مراجع أخرى