פיתוח אפליקציות מדיה לרכבים

Android Auto ו-Android Automotive OS עוזרים לכם להציג את התוכן של אפליקציית המדיה שלכם למשתמשים ברכב. אפליקציית מדיה למכוניות חייבת לספק שירות של דפדפן מדיה כדי ש-Android Auto ו-Android Automotive OS, או אפליקציה אחרת עם דפדפן מדיה, יוכלו למצוא את התוכן שלכם ולהציג אותו.

במדריך הזה אנו מתייחסים לאפליקציית מדיה שכבר מותקנת בטלפון ומשמשת להפעלת אודיו, ושעומדת בדרישות של ארכיטקטורת אפליקציות המדיה ב-Android.

במדריך הזה מתוארים הרכיבים הנדרשים של MediaBrowserService ו-MediaSession שנחוצים לאפליקציה כדי לפעול ב-Android Auto או ב-Android Automotive OS. אחרי שמשלימים את תשתית המדיה המרכזית, אפשר להוסיף תמיכה ב-Android Auto ולהוסיף לאפליקציית המדיה תמיכה ב-Android Automotive OS.

לפני שמתחילים

  1. כאן אפשר לקרוא את המסמכים של Android media API.
  2. במאמר יצירת אפליקציות מדיה מפורטות הנחיות לעיצוב.
  3. כדאי לעיין במונחים ובמושגים המרכזיים שמפורטים בקטע הזה.

מונחים ומושגים מרכזיים

שירות דפדפן מדיה
שירות Android שמוטמע באפליקציית המדיה שלכם ועונה על דרישות ה-API של MediaBrowserServiceCompat. האפליקציה שלכם משתמשת בשירות הזה כדי לחשוף את התוכן שלה.
דפדפן מדיה
ממשק API שמשמש אפליקציות מדיה כדי למצוא שירותי דפדפני מדיה ולהציג את התוכן שלהם. Android Auto ו-Android Automotive OS משתמשים בדפדפן מדיה כדי למצוא את שירות דפדפן המדיה של האפליקציה.
פריט מדיה

דפדפן המדיה מארגן את התוכן שלו בעץ של אובייקטים מסוג MediaItem. פריט מדיה יכול לכלול אחד מהסימונים הבאים, או את שניהם:

  • FLAG_PLAYABLE: מציין שהפריט הוא עלה בעץ התוכן. הפריט מייצג מקור אודיו יחיד, כמו שיר באלבום, פרק בספר אודיו או פרק בפודקאסט.
  • FLAG_BROWSABLE: מציין שהפריט הוא צומת בעץ התוכן ויש לו צאצאים. לדוגמה, הפריט מייצג אלבום, והצאצאים שלו הם השירים באלבום.

פריט מדיה שאפשר לעיין בו ולהפעיל אותו פועל כמו פלייליסט. אפשר לבחור את הפריט עצמו כדי להפעיל את כל הצאצאים שלו, או לעיין בצאצאים שלו.

מותאם לרכב

פעילות לאפליקציה ל-Android Automotive OS שתואמת להנחיות העיצוב של Android Automotive OS. ממשק הפעילות האלה לא נוצר על ידי Android Automotive OS, לכן עליכם לוודא שהאפליקציה שלכם עומדת בהנחיות העיצוב. בדרך כלל, התכונות האלה כוללות יעדי הקשה וגדלי גופן גדולים יותר, תמיכה במצב יום ובמצב לילה ויחסי ניגודיות גבוהים יותר.

אפשר להציג ממשקי משתמש שמותאמות לרכב רק כשהגבלות על חוויית המשתמש ברכב (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 כדי לייצג את האפליקציה בממשק המשתמש של המערכת. יש צורך בשני סוגים של סמלים:

  • סמל מרכז האפליקציות
  • סמל השיוך (Attribution)

סמל מרכז האפליקציות

סמל מרכז האפליקציות מייצג את האפליקציה בממשק המשתמש של המערכת, למשל במרכז האפליקציות ובמגש הסמלים. כדי לציין שאתם רוצים להשתמש בסמל של האפליקציה לנייד כדי לייצג את אפליקציית המדיה ברכב, תוכלו להשתמש בהצהרת המניפסט הבאה:

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

כדי להשתמש בסמל שונה מהסמל של האפליקציה לנייד, מגדירים את המאפיין android:icon ברכיב <service> של שירות דפדפן המדיה במניפסט:

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

סמל השיוך (Attribution)

איור 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 OS ו-Android Auto פועלים יחד עם שירות דפדפן המדיה במהלך תהליך עבודה אופייני של משתמש.

  1. המשתמש מפעיל את האפליקציה ב-Android Automotive OS או ב-Android Auto.
  2. Android Automotive OS או Android Auto יוצרים קשר עם שירות דפדפן המדיה באפליקציה באמצעות השיטה onCreate(). בהטמעה של method‏ onCreate(), צריך ליצור ולרשום אובייקט MediaSessionCompat ואת אובייקט ה-callback שלו.
  3. Android Automotive OS או Android Auto מפעילים את השיטה onGetRoot() של השירות כדי לקבל את פריט המדיה הבסיסי בהיררכיית התוכן. פריט המדיה ברמה הבסיסית לא מוצג, אלא משמש לאחזור תוכן נוסף מהאפליקציה.
  4. Android Automotive OS או Android Auto קוראים לשיטה onLoadChildren() של השירות כדי לקבל את הצאצאים של פריט המדיה ברמה הבסיסית. Android Automotive OS ו-Android Auto מציגים את פריטי המדיה האלה ברמה העליונה של פריטי תוכן. בקטע מבנה התפריט ברמה הבסיסית בדף הזה מוסבר מה המערכת מצפה לראות ברמה הזו.
  5. אם המשתמש בוחר פריט מדיה שאפשר לעיין בו, המערכת קוראת שוב ל-method‏ onLoadChildren() של השירות כדי לאחזר את הפריטים הצאצאים של פריט התפריט שנבחר.
  6. אם המשתמש בוחר פריט מדיה שאפשר להפעיל, מערכת Android Automotive OS או Android Auto מפעילה את השיטה המתאימה לקריאה חוזרת (callback) של סשן במדיה כדי לבצע את הפעולה הזו.
  7. אם האפליקציה תומכת בכך, המשתמש יוכל גם לחפש את התוכן שלכם. במקרה כזה, Android Automotive OS או Android Auto קוראים לשיטה onSearch() של השירות.

יצירת היררכיית התוכן

Android Auto ו-Android Automotive OS קוראים לשירות דפדפן המדיה של האפליקציה כדי לברר איזה תוכן זמין. כדי לתמוך בכך, צריך להטמיע שתי שיטות בשירות דפדפן המדיה: onGetRoot() ו-onLoadChildren()

יישום ב-GetRoot

השיטה 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);
}

לדוגמה מפורטת יותר של השיטה הזו, אפשר לעיין ב-method onGetRoot() באפליקציה לדוגמה של Universal Android Music Player ב-GitHub.

הוספת אימות חבילה ל-onGetRoot()

כשמתבצעת קריאה ל-method‏ onGetRoot() של השירות, החבילה של מבצע הקריאה מעבירה מידע מזהה לשירות. השירות יכול להשתמש במידע הזה כדי להחליט אם לחבילה תהיה גישה לתוכן שלכם. לדוגמה, אפשר להגביל את הגישה לתוכן של האפליקציה לרשימה של חבילות שאושרו על ידי השוואה בין clientPackageName לרשימת ההיתרים ואימות האישור שמשמש לחתימה על ה-APK של החבילה. אם אי אפשר לאמת את החבילה, צריך להחזיר את הערך null כדי לדחות את הגישה לתוכן.

כדי לספק אפליקציות מערכת, כמו Android Auto ו-Android Automotive OS, עם גישה לתוכן, השירות תמיד צריך להחזיר BrowserRoot שהוא לא null כשאפליקציות המערכת האלה מבצעות קריאה ל-method onGetRoot(). החתימה באפליקציית המערכת של Android Automotive OS עשויה להשתנות בהתאם ליצרן ולדגם של הרכב, לכן צריך לאפשר חיבורים מכל אפליקציות המערכת כדי לתמוך ב-Android Automotive OS בצורה יציבה.

בקטע הקוד הבא מוצג איך השירות יכול לאמת שהחבילת הקריאה היא אפליקציית מערכת:

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 Assistant להתחבר ל-MediaBrowserService. לתשומת ליבכם: ל-Google Assistant יש שמות חבילות נפרדים בטלפון, שכוללים את Android Auto, וב-Android Automotive OS.

הטמעה של onLoadChildren()

אחרי שמקבלים את אובייקט הצומת של הרמה הבסיסית (root), מערכת Android Auto ו-Android Automotive OS יוצרים תפריט ברמה עליונה על ידי קריאה ל-onLoadChildren() באובייקט של צומת הרמה הבסיסית (root) כדי לקבל את הצאצאים שלו. אפליקציות לקוח יוצרות תפריטי משנה על ידי קריאה לאותה שיטה באמצעות אובייקטים של צמתים צאצאים.

כל צומת בהיררכיית התוכן מיוצג באובייקט 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);
}

דוגמה מלאה לשימוש בשיטה הזו מופיעה ב-method‏ onLoadChildren() באפליקציית Universal Android Music Player לדוגמה ב-GitHub.

מבנה התפריט ברמה הבסיסית

איור 2. תוכן הבסיס מוצג ככרטיסיות ניווט.

ל-Android Auto ול-Android Automotive OS יש אילוצים ספציפיים לגבי המבנה של תפריט הבסיס. הם מועברים ל-MediaBrowserService דרך רמזים ברמה הבסיסית, שאפשר לקרוא דרך הארגומנט Bundle שמוענק ל-onGetRoot(). מעקב אחר הרמזים האלה מאפשר למערכת להציג את תוכן הבסיס באופן אופטימלי ככרטיסיות ניווט. אם לא תפעלו לפי ההנחיות האלה, יכול להיות שחלק מהתוכן ברמה הבסיסית יוסר או שהמערכת תתקשה למצוא אותו. נשלחות שתי רמזים:

אפשר להשתמש בקוד הבא כדי לקרוא את הטיפים הרלוונטיים ברמה הבסיסית (root):

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 המקומי הזה חייב להפנות לקובץ bitmap או לקובץ וקטור שניתן לציור במשאבי האפליקציה. לאובייקטים 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. פריט מדיה א' עם extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  2. פריט מדיה ב' עם extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")
  3. פריט מדיה ג' עם extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  4. פריט מדיה ד' עם extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  5. פריט מדיה E עם extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")

מכיוון שפריטי המדיה בקבוצה 'שירים' ובקבוצה 'אלבומים' לא נשמרים יחד בבלוק רציף, מערכת Android Auto ומערכת Android Automotive OS מפרשות אותם כארבעת הקבוצות הבאות:

  • קבוצה 1 שנקראת 'שירים' ומכילה את פריט המדיה א'
  • קבוצה 2 שנקראת 'אלבומים' ומכילה את פריט המדיה ב'
  • קבוצה 3 שנקראת 'שירים' ומכילה את פריטי המדיה C ו-D
  • קבוצה 4 שנקראת 'אלבומים' ומכילה את פריט המדיה E

כדי להציג את הפריטים האלה בשתי קבוצות, האפליקציה צריכה להעביר את פריטי המדיה במקום זאת בסדר הבא:

  1. פריט מדיה א' עם extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  2. פריט מדיה C עם 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. פריט מדיה ב' עם 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:

אפשר להשתמש בערכי הקבועים הבאים רק בתוספים של תיאור MediaItem:

כדי להציג אינדיקטורים שמופיעים בזמן שהמשתמש גולש בעץ העיון של המדיה, יוצרים חבילה של פריטים נוספים שכוללת אחת או יותר מהקבועות האלה ומעבירים את החבילה הזו ל-method‏ MediaDescription.Builder.setExtras().

קטע הקוד הבא מראה איך להציג אינדיקטורים לפריט מדיה בוטה בשלמותו:

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() של השירות, ולמפות אותו ל-boolean true.

בקטע הקוד הבא מוסבר איך להפעיל תמיכה ב-method 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. Overflow של פעולת גלישה בהתאמה אישית

אם יש יותר פעולות בהתאמה אישית ממה ש-OEM מאפשר להציג, יוצג למשתמש תפריט Overflow.

איך הם פועלים?

כל פעולת גלישה בהתאמה אישית מוגדרת באמצעות:

  • מזהה פעולה (מזהה מחרוזת ייחודי)
  • תווית פעולה (הטקסט שמוצג למשתמש)
  • URI של סמל פעולה (קובץ וקטורי שניתן להזזה עם גוון)

מגדירים רשימה של פעולות גלישה בהתאמה אישית באופן גלובלי כחלק מ-BrowseRoot. לאחר מכן תוכלו לצרף קבוצת משנה של הפעולות האלה לMediaItem. ספציפיים.

כשמשתמש יוצר אינטראקציה עם פעולת גלישה מותאמת אישית, האפליקציה מקבלת קריאה חוזרת (callback) ב-onCustomAction(). לאחר מכן תוכלו לטפל בפעולה ולעדכן את רשימת הפעולות של MediaItem לפי הצורך. כדאי להשתמש באפשרות הזו לפעולות עם מצב (stateful), כמו 'הוספה למועדפים' ו'הורדה'. לגבי פעולות שלא צריך לעדכן, כמו 'הפעלת רדיו', אין צורך לעדכן את רשימת הפעולות.

פעולות גלישה בהתאמה אישית ברמה הבסיסית של צומת גלישה.

איור 8. סרגל הכלים של פעולות 'עיון' בהתאמה אישית

אפשר גם לצרף פעולות גלישה בהתאמה אישית לשורש של צומת גלישה. הפעולות האלה יוצגו בסרגל כלים משני מתחת לסרגל הכלים הראשי.

איך מטמיעים פעולות גלישה בהתאמה אישית

כך מוסיפים לפרויקט פעולות גלישה בהתאמה אישית:

  1. משנים את ברירת המחדל של שתי שיטות בהטמעה של MediaBrowserServiceCompat:
  2. לנתח את מגבלות הפעולות בזמן הריצה:
    • ב-onGetRoot(), מקבלים את המספר המקסימלי של הפעולות שמותר לבצע לכל MediaItem באמצעות המפתח BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT ב-rootHints Bundle. אם הערך של המגבלה הוא 0, המשמעות היא שהמערכת לא תומכת בתכונה.
  3. יוצרים את הרשימה הגלובלית של פעולות גלישה בהתאמה אישית:
    • לכל פעולה, יוצרים אובייקט Bundle עם המפתחות הבאים: * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID: מזהה הפעולה * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL: תווית הפעולה * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI: סמל הפעולה URI * מוסיפים את כל האובייקטים של הפעולה Bundle לרשימה.
  4. מוסיפים את הרשימה הגלובלית אל BrowseRoot:
  5. מוסיפים פעולות לאובייקטים של MediaItem:
    • כדי להוסיף פעולות לאובייקטים ספציפיים מסוג MediaItem, צריך לכלול את רשימת מזהי הפעולות בפרטים הנוספים של MediaDescriptionCompat באמצעות המפתח DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST. הרשימה הזו חייבת להיות קבוצת משנה של הרשימה הגלובלית של הפעולות שהגדרתם ב-BrowseRoot.
  6. טיפול בפעולות והחזרת התקדמות או תוצאות:
    • ב-onCustomAction, מטפלים בפעולה על סמך מזהה הפעולה וכל נתון אחר שנחוץ לכם. אפשר לקבל את המזהה של ה-MediaItem שהפעיל את הפעולה מהתכונות הנוספות באמצעות המפתח EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID.
    • כדי לעדכן את רשימת הפעולות של MediaItem, צריך לכלול את המפתח EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM בחבילת ההתקדמות או התוצאה.

ריכזנו כאן כמה שינויים שאפשר לבצע ב-BrowserServiceCompat כדי להתחיל להשתמש בפעולות גלישה בהתאמה אישית.

שינוי של BrowserServiceCompat

צריך לשנות את השיטות הבאות ב-MediaBrowserServiceCompat.

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

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

מגבלה על פעולות ניתוח

כדאי לבדוק כמה פעולות גלישה בהתאמה אישית נתמכות.

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

יצירה של פעולת גלישה בהתאמה אישית

כל פעולה צריכה להיות ארוזת ב-Bundle נפרד.

  • מזהה פעולה
    bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID,
                    "<ACTION_ID>")
  • תווית הפעולה
    bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL,
                    "<ACTION_LABEL>")
  • URI של סמל פעולה
    bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI,
                    "<ACTION_ICON_URI>")

הוספת פעולות גלישה בהתאמה אישית אל Parceable ArrayList

מוסיפים את כל האובייקטים מסוג Bundle של פעולות גלישה בהתאמה אישית ל-ArrayList.

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

הוספת רשימת פעולות גלישה בהתאמה אישית לשורש הגלישה

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

הוספת פעולות ל-MediaItem

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

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

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

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

תוצאת build 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()
  • חבילת תוצאות Build
    • שליחת הודעה למשתמש
      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. בוצעה הורדה: מצב זה מציין שההורדה הושלמה. כשההורדה תושלם, תוכלו להחליף את 'Downloading' (הורדה) ב-'Downloaded' (הורדנו) ולקרוא ל-sendResult באמצעות המקש EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM כדי לציין שצריך לרענן את הפריט. בנוסף, אפשר להשתמש במפתח EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE כדי להציג למשתמש הודעת אישור.

כך תוכלו לספק למשתמשים משוב ברור על תהליך ההורדה והמצב הנוכחי שלו. אפשר להוסיף עוד פרטים באמצעות סמלים שמייצגים את שלבי ההורדה: 25%, 50% ו-75%.

דוגמה: פעולה מועדפת

דוגמה נוספת היא פעולה מועדפת עם שני מצבים:

  1. מועדפים: הפעולה הזו מוצגת לפריטים שלא נמצאים ברשימת המועדפים של המשתמש. כשהמשתמש בוחר בפעולה הזו, אפשר להחליף אותה ב-Favorited [מועדפים] ולקרוא ל-sendResult במפתח EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM כדי לעדכן את ממשק המשתמש.
  2. הוספה למועדפים: הפעולה הזו מוצגת לפריטים שנמצאים ברשימת המועדפים של המשתמש. כשהמשתמש בוחר בפעולה הזו, אפשר להחליף אותה ב'מועדף' ולקרוא ל-sendResult באמצעות המקש EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM כדי לעדכן את ממשק המשתמש.

הגישה הזו מספקת למשתמשים דרך ברורה ועקבית לנהל את הפריטים המועדפים עליהם.

הדוגמאות האלה מדגימות את הגמישות של פעולות גלישה בהתאמה אישית ואת האופן שבו אפשר להשתמש בהן כדי להטמיע מגוון פונקציות עם משוב בזמן אמת, לשיפור חוויית המשתמש באפליקציית המדיה ברכב.

דוגמה מלאה להטמעה של התכונה הזו מופיעה בפרויקט TestMediaApp.

הפעלת פקד ההפעלה

Android Auto ו-Android Automotive OS שולחים פקודות לניהול ההפעלה דרך MediaSessionCompat של השירות. צריך לרשום סשן ולהטמיע את שיטות ה-callback המשויכות.

רישום של סשן מדיה

בשיטה 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 מפעילים אחת מהשיטות של אובייקט ה-callback.

כדי לטפל בהפעלת תוכן, האפליקציה צריכה להרחיב את הכיתה המופשטת MediaSessionCompat.Callback ולהטמיע את השיטות שהאפליקציה תומכת בהן.

כדאי להטמיע את כל השיטות הבאות להתקשרות חזרה שמתאימות לסוג התוכן שהאפליקציה מציעה:

onPrepare()
הקריאה מתבצעת כשמקור המדיה משתנה. מערכת Android Automotive OS מפעילה את השיטה הזו גם מיד אחרי האתחול. אפליקציית המדיה שלכם חייבת להטמיע את השיטה הזו.
onPlay()
האירוע הזה מופעל אם המשתמש בוחר להפעיל את הסרטון בלי לבחור פריט ספציפי. חובה להפעיל באפליקציה את תוכן ברירת המחדל. לחלופין, אם ההפעלה הושהתה עם onPause(), האפליקציה תמשיך לפעול.

הערה: האפליקציה לא אמורה להתחיל להשמיע מוזיקה באופן אוטומטי כשמערכת Android Automotive OS או Android Auto מתחברות לשירות דפדפן המדיה. מידע נוסף זמין בקטע הגדרת מצב ההפעלה הראשוני.

onPlayFromMediaId()
האירוע מופעל כשהמשתמש בוחר להפעיל פריט ספציפי. השיטה מועברת המזהה שהוקצה על ידי שירות דפדפן המדיה לפריט המדיה בהיררכיית התוכן.
onPlayFromSearch()
האירוע מופעל כשהמשתמש בוחר להפעיל תוכן משאילתת חיפוש. האפליקציה צריכה לבחור את האפשרות המתאימה על סמך מחרוזת החיפוש שהועברה.
onPause()
האירוע מופעל כשהמשתמש בוחר להשהות את ההפעלה.
onSkipToNext()
מופעלת כשהמשתמש בוחר לדלג לפריט הבא.
onSkipToPrevious()
מופעלת כשהמשתמש בוחר לדלג לפריט הקודם.
onStop()
הקריאה מתבצעת כשהמשתמש בוחר להפסיק את ההפעלה.

אפשר לשנות את השיטות האלה באפליקציה כדי לספק את הפונקציונליות הרצויה. אין צורך להטמיע שיטה אם הפונקציונליות שלה לא נתמכת באפליקציה. לדוגמה, אם האפליקציה מפעילה שידור חי, כמו שידור ספורט, אין צורך להטמיע את השיטה onSkipToNext(). במקום זאת, אפשר להשתמש בהטמעה שמוגדרת כברירת מחדל של onSkipToNext().

לא נדרשת שום היגיון מיוחד כדי להפעיל תוכן דרך הרמקולים של הרכב. כשהאפליקציה מקבלת בקשה להפעיל תוכן, היא יכולה להפעיל אודיו באותו אופן שבו היא מפעילה תוכן דרך הרמקולים או האוזניות של הטלפון של המשתמש. Android Auto ו-Android Automotive OS שולחים באופן אוטומטי את תוכן האודיו למערכת של הרכב כדי להשמיע אותו דרך הרמקולים של הרכב.

למידע נוסף על הפעלת תוכן אודיו, ראו סקירה כללית על MediaPlayer, סקירה כללית על אפליקציות אודיו וסקירה כללית על ExoPlayer.

הגדרת פעולות הפעלה רגילות

ב-Android Auto וב-Android Automotive OS מוצגים פקדי ההפעלה שמבוססים על הפעולות שמופעלות באובייקט PlaybackStateCompat.

כברירת מחדל, האפליקציה צריכה לתמוך בפעולות הבאות:

בנוסף, האפליקציה יכולה לתמוך בפעולות הבאות אם הן רלוונטיות לתוכן של האפליקציה:

בנוסף, יש אפשרות ליצור תור הפעלה שיוצג למשתמשים, אבל זו לא חובה. לשם כך, צריך לקרוא ל-methods‏ setQueue() ו-setQueueTitle(), להפעיל את הפעולה ACTION_SKIP_TO_QUEUE_ITEM ולהגדיר את פונקציית ה-callback‏ onSkipToQueueItem().

בנוסף, מוסיפים תמיכה בסמל מה שומעים עכשיו?, שמציין את התוכן שמושמע כרגע. כדי לעשות זאת, צריך לבצע קריאה ל-method‏ setActiveQueueItemId() ולהעביר את המזהה של הפריט שמופעל כרגע בתור. צריך לעדכן את setActiveQueueItemId() בכל פעם שיש שינוי בתור.

ב-Android Auto וב-Android Automotive OS מוצגים לחצנים לכל פעולה מופעלת, וגם תור ההפעלה. כשלוחצים על הלחצנים, המערכת מפעילה את פונקציית ה-callback המתאימה מ-MediaSessionCompat.Callback.

הזמנת נפח אחסון שלא מנוצל

המוצרים Android Auto ו-Android Automotive OS שומרים מקום בממשק המשתמש לפעולות ACTION_SKIP_TO_PREVIOUS ו-ACTION_SKIP_TO_NEXT. אם האפליקציה לא תומכת באחת מהפונקציות האלה, Android Auto ו-Android Automotive OS ינצלו את השטח כדי להציג פעולות מותאמות אישית שיוצרים.

אם אתם לא רוצים למלא את המרחבים האלה בפעולות בהתאמה אישית, תוכלו להקצות אותם כך שמערכת Android Auto ומערכת Android Automotive OS ישאירו את המרחב ריק בכל פעם שהאפליקציה שלכם לא תומכת בפונקציה המתאימה. כדי לעשות את זה, קוראים ל-method 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 OS או Android Auto מתחברות לשירות דפדפן המדיה. במקום זאת, עדיף להשתמש ב-Android Auto וב-Android Automotive OS כדי להמשיך או להתחיל את ההפעלה בהתאם למצב הרכב או לפעולות המשתמש.

כדי לעשות זאת, צריך להגדיר את ה-PlaybackStateCompat הראשוני של סשן המדיה כ-STATE_STOPPED, STATE_PAUSED, STATE_NONE, או STATE_ERROR.

סשנים של מדיה ב-Android Auto וב-Android Automotive OS נמשכים רק למשך הנסיעה, ולכן המשתמשים מתחילים ומפסיקים את הסשנים האלה לעיתים קרובות. כדי להבטיח חוויה חלקה בין נסיעות, כדאי לעקוב אחרי מצב הסשן הקודם של המשתמש, כך שכאשר אפליקציית המדיה תקבל בקשה להמשך, המשתמש יוכל להמשיך באופן אוטומטי מהמקום שבו הוא הפסיק. לדוגמה, פריט המדיה האחרון שהופעל, PlaybackStateCompat ותור.

הוספת פעולות מותאמות אישית להפעלה

אתם יכולים להוסיף פעולות הפעלה בהתאמה אישית כדי להציג פעולות נוספות שאפליקציית המדיה תומכת בהן. אם יש מספיק מקום (והוא לא שמור), Android מוסיפה את הפעולות בהתאמה אישית לפקדי התחבורה. אחרת, הפעולות בהתאמה אישית יוצגו בתפריט האפשרויות הנוספות. הפעולות בהתאמה אישית מוצגות לפי הסדר שבו הן נוספו ל-PlaybackStateCompat.

אפשר להשתמש בפעולות בהתאמה אישית כדי ליצור התנהגות שונה מפעולות רגילות. אל תשתמשו בהן כדי להחליף או לשכפל פעולות רגילות.

אפשר להוסיף פעולות מותאמות אישית באמצעות method addCustomAction() במחלקה PlaybackStateCompat.Builder.

קטע הקוד הבא מראה איך להוסיף פעולה מותאמת אישית של 'הפעלת ערוץ רדיו':

Kotlin

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

Java

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

דוגמה מפורטת יותר לשיטה הזו מופיעה ב-method‏ 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)) {
        ...
    }
}

דוגמה מפורטת יותר לשיטה הזו מופיעה ב-method‏ onCustomAction באפליקציית Universal Android Music Player לדוגמה ב-GitHub.

סמלים של פעולות בהתאמה אישית

לכל פעולה בהתאמה אישית שיוצרים נדרש משאב סמל. אפליקציות במכוניות יכולות לפעול במגוון גדלים ודחיסות של מסכים, ולכן הסמלים שאתם מספקים צריכים להיות פריטים שניתן לצייר בצורה וקטורית. כלי גרפי וקטורי מאפשר להתאים את גודל הנכסים בלי לאבד את הפרטים. אפשר גם ליישר בקלות קצוות ופינות לגבולות של פיקסלים ברזולוציות קטנות יותר, באמצעות שרטוט בווקטור.

אם פעולה מותאמת אישית היא עם שמירת מצב, למשל, היא מפעילה או משביתה הגדרות הפעלה – צריך לספק סמלים שונים למצבים השונים, כדי שהמשתמשים יוכלו לראות שינוי כשהם בוחרים בפעולה.

לספק סגנונות סמלים חלופיים לפעולות מושבתות

כשאין פעולה מותאמת אישית זמינה בהקשר הנוכחי, מחליפים את סמל הפעולה המותאמת אישית בסמל חלופי שמראה שהפעולה מושבתת.

איור 6. דוגמאות לסמלי פעולה מותאמים אישית שאינם בסגנון.

ציון פורמט האודיו

כדי לציין שמדיה שמופעלת כרגע משתמשת בפורמט אודיו מיוחד, אפשר לציין סמלים שמעובדים במכוניות שתומכות בתכונה הזו. אפשר להגדיר את הערכים של KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI ושל KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI בחבילת התוספים של פריט המדיה שפועל כרגע (שמועברים אל MediaSession.setMetadata()). חשוב להגדיר את שני התוספים האלה כדי להתאים לעיצובים שונים.

בנוסף, אפשר להגדיר את האפשרות הנוספת KEY_IMMERSIVE_AUDIO כדי להודיע ליצרני ציוד מקורי לכלי רכב שמדובר באודיו סוחף, ויש להיזהר מאוד כשמחליטים אם להחיל אפקטים אודיו שעשויים להפריע לתוכן הסוחף.

אתם יכולים להגדיר את פריט המדיה שמופעלת כרגע כך שהכתוביות או התיאור שלו, או שניהם, יהיו קישורים לפריטי מדיה אחרים. כך המשתמשים יכולים לדלג במהירות לפריטים קשורים. לדוגמה, הם יכולים לדלג לשירים אחרים של אותו אומן, לפרקים אחרים של אותו פודקאסט וכו'. אם הרכב תומך בתכונה הזו, המשתמשים יכולים להקיש על הקישור כדי לעיין בתוכן הזה.

כדי להוסיף קישורים, צריך להגדיר את המטא-נתונים KEY_SUBTITLE_LINK_MEDIA_ID (לקישור מהכותרת משנה) או את KEY_DESCRIPTION_LINK_MEDIA_ID (לקישור מהתיאור). פרטים נוספים זמינים במסמכי העזרה של שדות המטא-נתונים האלה.

תמיכה בפעולות קוליות

אפליקציית המדיה חייבת לתמוך בפעולות קוליות כדי לספק לנהגים חוויה בטוחה ונוחה שמפחיתה את הסחות הדעת. לדוגמה, אם האפליקציה מפעילה פריט מדיה אחד, המשתמש יכול לומר"Play [song title]" כדי להורות לאפליקציה להפעיל שיר אחר, בלי להסתכל על המסך של הרכב או לגעת בו. המשתמשים יכולים להתחיל שאילתות בלחיצה על הלחצנים המתאימים בהגה או בלחיצה על מילות המפתח "OK Google".

כש-Android Auto או Android Automotive OS מזהים ומנתחים פעולה קולית, הפעולה הקולית נשלחת לאפליקציה באמצעות onPlayFromSearch(). כשהאפליקציה מקבלת את הקריאה החוזרת, היא מחפשת תוכן שמתאים למחרוזת query ומתחילה את ההפעלה.

המשתמשים יכולים לציין קטגוריות שונות של מונחים בשאילתה שלהם: ז'אנר, אומן, אלבום, שם שיר, תחנת רדיו או פלייליסט, בין היתר. כשאתם מפתחים תמיכה בחיפוש, חשוב להביא בחשבון את כל הקטגוריות שרלוונטיות לאפליקציה שלכם. אם מערכת Android Auto או Android Automotive OS מזהה ששאילתה מסוימת מתאימה לקטגוריות מסוימות, היא מוסיפה פרטים נוספים לפרמטר extras. אפשר לשלוח את התוספים הבאים:

חשבון עם מחרוזת query ריקה, שאותה אפשר לשלוח באמצעות Android Auto או Android Automotive OS אם המשתמש לא מציין מונחי חיפוש. לדוגמה, אם המשתמש אומר "Play some music". במקרה כזה, יכול להיות שהאפליקציה תתחיל נגינה של טראק שהושמעו לאחרונה או של טראק חדש שהוצעה.

אם אי אפשר לעבד חיפוש במהירות, אין לחסום אותו ב-onPlayFromSearch(). במקום זאת, מגדירים את מצב ההפעלה ל-STATE_CONNECTING ומבצעים את החיפוש בשרשור אסינכררוני.

לאחר שההפעלה מתחילה, כדאי לאכלס את התור של סשן המדיה בתוכן קשור. לדוגמה, אם משתמש מבקש להשמיע אלבום, האפליקציה עשויה למלא את התור ברשימת הטראקים של האלבום. מומלץ גם להטמיע תמיכה בתוצאות חיפוש שניתן לעיין בהן כדי שהמשתמשים יוכלו לבחור טראק אחר שתואם לשאילתה שלהם.

בנוסף לשאילתות הפעלה, מערכות Android Auto ו-Android Automotive OS גם מזהות שאילתות קוליות כדי לשלוט בהפעלה כמו pause music ו-Next song, ולהתאים את הפקודות האלה לקריאות החוזרות (callback) המתאימות של סשן המדיה, כמו onPause() ו-onSkipToNext().

דוגמה מפורטת להטמעת פעולות הפעלה מבוססות-קול באפליקציה מופיעה במאמר Google Assistant ואפליקציות מדיה.

הטמעת אמצעי הגנה מפני הסחות דעת

הטלפון של המשתמש מחובר לרמקולים של הרכב בזמן השימוש ב-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 צריך לפתוח את האפליקציה בטלפון כדי לפתור שגיאה, צריך לציין את המידע הזה בהודעה למשתמש. לדוגמה, הודעת השגיאה עשויה להופיע כ "כניסה אל [שם האפליקציה]" במקום כ "יש להיכנס לחשבון".

מקורות מידע נוספים