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

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

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

  • يوضّح هذا الدليل كيفية استخدام MediaBrowserService وMediaSession لإنشاء تطبيق يمكن أن يتصل به نظاما التشغيل Android Auto وAndroid Automotive لعرض طرق تصفّح الوسائط وتشغيلها بشكل محسّن للاستخدام داخل السيارة.

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

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

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

قبل البدء

  1. راجِع مستندات واجهة برمجة تطبيقات الوسائط في Android.
  2. راجِع مقالة إنشاء تطبيقات وسائط للحصول على إرشادات حول التصميم.
  3. راجِع المصطلحات والمفاهيم الرئيسية المدرَجة في هذا القسم.

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

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

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

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

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

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

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

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

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

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

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

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

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

يوضّح مقتطف الرمز البرمجي التالي كيفية تعريف خدمة متصفّح الوسائط في ملف البيان. أدرِج هذا الرمز في ملف البيان الخاص بوحدة 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 OS لتمثيل تطبيقك في واجهة مستخدم النظام. يجب توفير نوعَين من الرموز:

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

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

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

<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 OS استخدام خدمتك لإجراء ما يلي:

  • تصفَّح التسلسل الهرمي لمحتوى تطبيقك لعرض قائمة للمستخدم.
  • احصل على الرمز المميّز 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 OS أو Android Auto طريقة onLoadChildren() في خدمتك للحصول على العناصر الفرعية لعنصر الوسائط الجذر. يعرض نظام التشغيل Android Automotive وAndroid Auto عناصر الوسائط هذه على أنّها المستوى الأعلى من عناصر المحتوى. يمكنك الاطّلاع على هيكلة القائمة الجذر في هذه الصفحة للحصول على مزيد من المعلومات حول ما يتوقّعه النظام على هذا المستوى.
  5. إذا اختار المستخدم عنصر وسائط يمكن تصفّحه، سيتم استدعاء طريقة onLoadChildren() في خدمتك مرة أخرى لاسترداد العناصر الفرعية لعنصر القائمة المحدّد.
  6. إذا اختار المستخدم عنصر وسائط قابلاً للتشغيل، يستدعي نظام التشغيل Android Automotive أو Android Auto طريقة معاودة الاتصال المناسبة لجلسة الوسائط لتنفيذ هذا الإجراء.
  7. وإذا كان تطبيقك يتيح ذلك، يمكن للمستخدم أيضًا البحث في المحتوى. في هذه الحالة، سيطلب نظام التشغيل Android Automotive أو Android Auto تنفيذ طريقة onSearch() في خدمتك.

إنشاء التدرّج الهرمي للمحتوى

يستدعي كلّ من Android Auto وAndroid Automotive OS خدمة &quot;متصفّح الوسائط&quot; في تطبيقك لمعرفة المحتوى المتاح. يجب تنفيذ طريقتَين في خدمة متصفّح الوسائط لتوفير هذه الميزة، وهما onGetRoot() وonLoadChildren().

تنفيذ onGetRoot

تعرض طريقة onGetRoot() في خدمتك معلومات حول العقدة الجذرية لتسلسل المحتوى الهرمي. يستخدم Android Auto وAndroid Automotive OS عقدة الجذر هذه لطلب بقية المحتوى باستخدام طريقة 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 OS، مع إمكانية الوصول إلى المحتوى الخاص بك، يجب أن تعرض خدمتك دائمًا قيمة غير فارغة 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() في خدمتك.

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

.

تنفيذ طريقة onLoadChildren()

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

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

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

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

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

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

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

    Kotlin

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

    Java

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

    Kotlin

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

    Java

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

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

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

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

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

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

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

عناصر الشبكة

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

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

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

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

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

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

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

Kotlin

import androidx.media.utils.MediaConstants

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

Java

import androidx.media.utils.MediaConstants;

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

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

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

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

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

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

Kotlin

import androidx.media.utils.MediaConstants

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

Java

import androidx.media.utils.MediaConstants;

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

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

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

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

Kotlin

import androidx.media.utils.MediaConstants

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

Java

import androidx.media.utils.MediaConstants;

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

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

  1. ملف الوسائط A الذي يتضمّن extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  2. ملف الوسائط B مع 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. ملف الوسائط E مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")

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

  • المجموعة 1 المسماة "أغانٍ" والتي تحتوي على عنصر الوسائط A
  • المجموعة 2 المسماة "الألبومات" والتي تحتوي على عنصر الوسائط B
  • المجموعة 3 المسماة "أغانٍ" والتي تحتوي على وسائط C وD
  • المجموعة 4 المسماة "الألبومات" التي تحتوي على عنصر الوسائط E

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

  1. ملف الوسائط A الذي يتضمّن extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  2. ملف الوسائط "ج" مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  3. ملف الوسائط D مع extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  4. ملف الوسائط B مع 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 OS الإضافات المرتبطة بأحد العناصر ويبحثان عن ثوابت معيّنة لتحديد المؤشرات التي سيتم عرضها. أثناء تشغيل الوسائط، يقرأ كلّ من Android Auto وAndroid Automotive OS البيانات الوصفية لجلسة الوسائط ويبحثان عن ثوابت معيّنة لتحديد المؤشرات التي سيتم عرضها.

الشكل 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 OS، سيصبح هذا المؤشر غير دقيق بمرور الوقت.

لكي يتمكّن Android Auto وAndroid Automotive OS من إبقاء شريط التقدّم محدّثًا، يمكنك تقديم معلومات إضافية في 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 OS هذه النتائج من خلال واجهات طلبات البحث أو من خلال عناصر واجهة المستخدم التي تستند إلى طلبات البحث التي تم إجراؤها في وقت سابق من الجلسة. لمزيد من المعلومات، يُرجى الاطّلاع على القسم إتاحة الإجراءات الصوتية في هذا الدليل.

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

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

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

Kotlin

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

Java

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. تجاوز طريقتَين في تنفيذ MediaBrowserServiceCompat:
  2. تحليل حدود الإجراء في وقت التشغيل:
    • في onGetRoot()، احصل على الحد الأقصى لعدد الإجراءات المسموح بها لكل MediaItem باستخدام المفتاح BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT في rootHints Bundle. يشير الحدّ 0 إلى أنّ النظام لا يتيح استخدام الميزة.
  3. إنشاء القائمة العامة لإجراءات التصفّح المخصّصة:
    • لكل إجراء، أنشئ عنصر Bundle يتضمّن المفاتيح التالية: * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID: معرّف الإجراء * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL: تصنيف الإجراء * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI: معرّف الموارد المنتظم لرمز الإجراء * أضِف جميع عناصر الإجراء 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 للبدء باستخدام "إجراءات التصفّح المخصّصة".

Override 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>")
  • معرّف الموارد المنتظم لرمز الإجراء
    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 لتعديل واجهة المستخدم.

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

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

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

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

يرسل كلّ من Android Auto وAndroid Automotive OS أوامر التحكّم في التشغيل من خلال 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 OS إحدى طرق عنصر معاودة الاتصال.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

تحجز كلّ من Android Auto وAndroid Automotive OS مساحة في واجهة المستخدم لإجراءَي 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 OS مع خدمة متصفّح الوسائط، تتواصل جلسة الوسائط بشأن حالة تشغيل المحتوى باستخدام PlaybackStateCompat. يجب ألا يبدأ تطبيقك تشغيل الموسيقى تلقائيًا عند اتصال نظام التشغيل Android Automotive أو Android Auto بخدمة متصفّح الوسائط. بدلاً من ذلك، اعتمد على Android Auto وAndroid Automotive OS لاستئناف التشغيل أو بدئه استنادًا إلى حالة السيارة أو إجراءات المستخدم.

لإجراء ذلك، اضبط القيمة الأولية PlaybackStateCompat لجلسة الوسائط على STATE_STOPPED أو STATE_PAUSED أو STATE_NONE أو STATE_ERROR.

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

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

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

تفعيل الإجراءات الصوتية

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

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

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

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

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

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

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

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

تنفيذ إجراءات وقائية لتجنُّب التشتيت

بما أنّ هاتف المستخدم يكون متصلاً بمكبّرات صوت السيارة أثناء استخدام 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 OS رسالة الخطأ للمستخدم.

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

مراجع أخرى