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

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(). בהטמעה של השיטה 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()

הטמעה של 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);
}

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

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

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

כדי לתת לאפליקציות מערכת, כמו Android Auto ו-Android Automotive OS, גישה לתוכן שלכם, השירות שלכם תמיד צריך להחזיר ערך BrowserRoot שאינו null כשאפליקציות המערכת האלה קוראות לשיטה 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()‎

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

דוגמה מלאה לשימוש בשיטה הזו מופיעה ב-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. פריט מדיה C עם 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 שנקראת 'שירים' ומכילה את פריט המדיה א'
  • קבוצה 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().

בקטע הקוד הבא מוסבר איך להציג אינדיקטורים לגבי פריט מדיה בוטה שהושלם ב-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() של השירות, ולמפות אותו ל-boolean 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. 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()
  • יצירת חבילה של תוצאות פיתוח
    • שליחת הודעה למשתמש
      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. מועדף: הפעולה הזו מוצגת לפריטים שלא נמצאים ברשימת המועדפים של המשתמש. כשהמשתמש בוחר בפעולה הזו, אפשר להחליף אותה באפשרות 'הוספה למועדפים' ולהפעיל את 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());
    ...
}

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

הטמעת פקודות הפעלה

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

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

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

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

אפשר להוסיף פעולות בהתאמה אישית באמצעות השיטה 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());

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

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

לכל פעולה בהתאמה אישית שיוצרים צריך סמל.

אם התיאור של הסמל הזה תואם לאחד מהקבועים CommandButton.ICON_, צריך להגדיר את ערך המספר השלם הזה למפתח EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT של התוספים של הפעולה בהתאמה אישית. במערכות נתמכות, הפעולה הזו תגביל את המשאב של הסמל שמוענק ל-CustomAction.Builder, וכך רכיבי המערכת יוכלו ליצור את הפעולה שלכם ואת פעולות ההפעלה האחרות בסגנון עקבי.

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

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

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

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

איור 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 ומבצעים את החיפוש בשרשור אסינכררוני.

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

בנוסף לשאילתות מסוג play, Android Auto ו-Android Automotive OS מזהים שאילתות קוליות לצורך בקרה על ההפעלה, כמו pause music ו-next song, ומתאימים את הפקודות האלה להחזרות קריאה מתאימות של סשן המדיה, כמו 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 צריך לפתוח את האפליקציה בטלפון כדי לפתור שגיאה, צריך לציין את המידע הזה בהודעה. לדוגמה, הודעת השגיאה עשויה להופיע כ "כניסה אל [שם האפליקציה]" במקום כ "יש להיכנס לחשבון".

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