פיתוח אפליקציית מדיה לפי תבנית

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

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

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

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

הגדרת המניפסט של האפליקציה

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

הצהרה על תמיכה בקטגוריה במניפסט

צריך להצהיר על קטגוריית האפליקציה לרכב של androidx.car.app.category.MEDIA במסנן ה-Intent של CarAppService.

<application>
    ...
   <service
       ...
        android:name=".MyCarAppService"
        android:exported="true">
      <intent-filter>
        <action android:name="androidx.car.app.CarAppService" />
        <category android:name="androidx.car.app.category.MEDIA"/>
      </intent-filter>
    </service>
    ...
<application>

כדי לקבל גישה אל MediaPlaybackTemplate, האפליקציה צריכה גם להצהיר על ההרשאה androidx.car.app.MEDIA_TEMPLATES בקובץ המניפסט שלה:

<manifest ...>
  ...
  <uses-permission android:name="androidx.car.app.MEDIA_TEMPLATES"/>
  ...
</manifest>

הגדרת רמת ה-API המינימלית של אפליקציית הרכב

אפליקציות מדיה שמשתמשות ב-MediaPlaybackTemplate נתמכות רק ב-CAL API 8 ואילך. חשוב לוודא שרמת ה-Car App API level המינימלית מוגדרת ל-8.

<application ...>
  ...
  <meta-data
    android:name="androidx.car.app.minCarApiLevel"
    android:value="8"/>
  ...
</application>

לספק סמל שיוך (Attribution)

חשוב להוסיף סמל שיוך (Attribution) לאפליקציות מדיה שנוצרו באמצעות ספריית האפליקציות לרכב.

הצהרה על תמיכה ב-Android Auto

חשוב לוודא שהמניפסט של האפליקציה כולל את הפרטים הבאים:

<application>
  ...
  <meta-data android:name="com.google.android.gms.car.application"
      android:resource="@xml/automotive_app_desc"/>
  ...
</application>

לאחר מכן, מוסיפים את ההצהרה template ל-automotive_app_desc.xml במשאבי ה-XML. הוא אמור להיראות כך:

<automotiveApp xmlns:android="http://schemas.android.com/apk/res/android">
 <uses name="media"/>
 <uses name="template"/>
</automotiveApp>

הצהרה על תמיכה ב-Android Automotive OS

יש שתי דרכים שונות להפיץ אפליקציית מדיה שמופעלת בה ספריית האפליקציות לרכב ב-Android Automotive OS: כקובץ APK יחיד או כשני קובצי APK נפרדים. אם אתם מפיצים קובץ APK אחד, הוא יתמוך בכלי רכב שמותאמים ל-Android Automotive OS עם המארח של ספריית האפליקציות לרכב, ויחזור לאפליקציית MediaBrowserService או MediaLibraryService אם לא, גם בגרסאות Android ישנות יותר (Android 10 עד Android 13). אם תבחרו להפיץ שני קובצי APK נפרדים, תוכלו לעדכן את התוספות החדשות לגרסה של ספריית האפליקציות לרכב בלי לחשוש מהשפעה על גרסת MediaBrowserService או MediaLibraryService של האפליקציה.

הפצה של קובץ APK אחד

כשמפיצים קובץ APK יחיד לספריית האפליקציות לרכב ולגרסאות MediaBrowserService או MediaLibraryService של האפליקציה, חשוב מאוד להגדיר את android.software.car.templates_host.media לערך android:required="false".

<uses-feature android:name="android.software.car.templates_host.media" android:required="false"/>

אחר כך פועלים לפי ההנחיות של ספריית האפליקציות לרכב ל-AAOS ומוסיפים CarAppActivity שניתן להפעיל (או פעילות טרמפולינה). צריך להגדיר את הפעילות כ-android:enabled="false" במניפסט. בשלב הבא, מוסיפים תג מטא-נתונים להצהרה על MediaBrowserService שמציין את הרכיב CarAppActivity כתחליף. דוגמה למניפסט:

<service android:name=".media.MyMediaService"
    android:exported="true"
    android:label="@string/app_name">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaLibraryService"/>
    </intent-filter>

    <!-- Link to Car App Library Activity -->
    <meta-data
        android:name="androidx.car.app.media.CalMediaActivityComponent"
        android:value="com.example.mediaapp.LaunchableTrampoline"/>
</service>

<activity
    android:name=".LaunchableTrampoline"
    android:exported="true"
    android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
    android:launchMode="singleTask"
    android:label="@string/app_name_cal"
    android:enabled="false"> <!-- Set to false -->

    <meta-data android:name="distractionOptimized" android:value="true" />

    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <action android:name="androidx.car.app.media.action.SHOW_MEDIA_PLAYBACK"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>

ההפצה של Play

צריך להפעיל את ה-APK עם ספריית האפליקציות לרכב ו-MediaBrowserService או MediaLibraryService עם קוד גרסה גבוה יותר ו-minSdk שמטרגט ל-Android 14 ‏ (34).

הפצה באמצעות שני קובצי APK

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

כשיוצרים קובץ APK נפרד לגרסה של האפליקציה בספריית האפליקציות לרכב, צריך להגדיר את android.software.car.templates_host.media לערך android:required=true. כך האפליקציה תופץ רק בגרסאות build של Android Automotive OS שמאושרות לתמיכה במארח של ספריית האפליקציות לרכב.

<uses-feature android:name="android.software.car.templates_host.media" android:required="true"/>

בנוסף לשימוש ב-android.software.car.templates_host.media והגדרתו ל-android:required=true, צריך לפעול לפי השלבים הבאים כדי להפעיל את Android Automotive OS לפעילות של ספריית האפליקציות לרכב שניתן להפעיל.

הפצה ב-Play

צריך להפיץ את ה-APK שמשתמש בספריית האפליקציות לרכב במסלול הייעודי ל-Automotive OS.

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

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

יצירת תבנית Playback

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

בנגן מוזיקה מוצג השיר Sounds of Spring של Summer Fielding עם תמונת דיוקן מרובעת של אישה שמנגנת בגיטרה.

איור 1: MediaPlaybackTemplate עם פעולה בכותרת לפתיחת התור בחלק העליון.

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

val playbackTemplate = MediaPlaybackTemplate.Builder()
      .setHeader(
        Header.Builder()
          .setStartHeaderAction(Action.BACK)
          .addEndHeaderAction(
                Action.Builder()
                  .setTitle(model.context.getString(R.string.queue_button_title))
                  .setIcon(
                    CarIcon.Builder(
                        IconCompat.createWithResource(
                          model.context,
                          R.drawable.gs_queue_music_vd_theme_24,
                        ))
                      .build())
                  .setOnClickListener(showQueueScreen())
                  .build())
          .setTitle(model.context.getString(R.string.media_playback_view_title))
          .build())
      .build()

כשמשתמשים ב-MediaPlaybackTemplate, צריך לרשום טוקן MediaSession באמצעות MediaPlaybackManager ב-CarAppService. אם לא תעשו את זה, תוצג שגיאה כשתישלח בקשת MediaPlaybackTemplate למארח.

import androidx.car.app.media.MediaPlaybackManager
…

override fun onCreateSession(sessionInfo: SessionInfo): Session {
    return object : Session() {
        …

        init {
          lifecycle.addObserver(
            LifecycleEventObserver { _, event ->
              if (event == ON_CREATE) {
                val token = ... // MediaSessionCompat.Token
                (carContext.getCarService(CarContext.MEDIA_PLAYBACK_SERVICE) as MediaPlaybackManager)
                  .registerMediaPlaybackToken(token)
              }
              ...
            }
          )
        }
    }
}

צריך את .registerMediaPlaybackToken כדי לחשוף את המידע ואת אמצעי הבקרה של הפעלת המדיה ל-Android Auto. זה חשוב גם כדי שהמארח יוכל ליצור התראות ספציפיות למדיה.

באפליקציות שמשתמשות בספריית Media3, שמשתמשת ב-PlatformToken במקום ב-MediaSessionCompat.Token רגיל, צריך להטמיע SessionCommand בהתאמה אישית ב-MediaLibrarySession.Callback שמחזיר את אסימון הפלטפורמה הבסיסי של הסשן: session.platformToken. ב-CarAppService, שולחים את הפקודה המותאמת אישית הזו לסשן. אחרי שמקבלים את הטוקן של הפלטפורמה, ממירים אותו באמצעות MediaSessionCompat.Token.fromToken(platformToken) ומעבירים את הטוקן התואם הזה לספריית האפליקציות לרכב ב-.registerMediaPlaybackToken().

ארגון המדיה באמצעות תבניות

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

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

איור 2: SectionedItemTemplate שמכיל RowSection ואחריו GridSection

שימוש ב-SectionedItemTemplate בתוך TabTemplate

דרך נוחה אחת לסווג מדיה באפליקציה היא להשתמש ב-SectionedItemTemplate בתוך TabTemplate.

val template =
      SectionedItemTemplate.Builder()...build();
val tabTemplate =
      TabTemplate.Builder(tabCallback)
          .setTabContents(TabContents.Builder(template).build)
          .setHeaderAction(Action.APP_ICON)
          …
          .build();

רכיבים ותכונות בגרסה 1.9 של ספריית האפליקציות לרכב

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

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

איור 3: SectionedItemTemplate שמכיל Chips, Condensed Items, Interactive Header, Grid Items ו-Minimized Control Panel

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

איור 4: שני מסכים של דפדפן מדיה עם Expanded Header, Spotlight Sections ו-Progress Bars

במאמר אפליקציות מדיה מוסבר איך לעצב את ממשק המשתמש של אפליקציית המדיה באמצעות התבניות האלה.

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

אם אתם משתמשים ב-SectionedItemTemplate, אתם יכולים להוסיף כפתור פעולה שמעביר אתכם למסך הפעלת המדיה. משתמשים בפעולה Action.MEDIA_PLAYBACK הרגילה של ספריית האפליקציות לרכב. אפליקציית מדיה תציג את הפעולה הזו בתור לוח בקרה ממוזער, שנדרש כדי לעמוד בדרישת האיכות MFT-1 אם משתמשים ב-Car App Library API מגרסה 1.9 ואילך. בתבניות אחרות, אפשר להשתמש בפעולה בכותרת כדי להשיג את אותה מטרה.

טיפול ב-intent של הפעלת מדיה במערכת

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

מוסיפים את הפעולה androidx.car.app.media.action.SHOW_MEDIA_PLAYBACK למסנן ה-Intent של רכיב ספריית האפליקציות לרכב (CarAppActivity או הטרמפולינה Activity).

מוודאים שהפעילות משתמשת ב-launchMode של singleTask או singleTop כדי ש-onNewIntent() יופעל.

<activity
    android:name=".LaunchableTrampoline"
    android:exported="true"
    android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
    android:launchMode="singleTask"
    android:label="@string/app_name_cal"
    android:enabled="false">

    <meta-data android:name="distractionOptimized" android:value="true" />

    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <action android:name="androidx.car.app.media.action.SHOW_MEDIA_PLAYBACK"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>

במחלקה Session, מבטלים את onNewIntent() כדי לנתח את ה-Intent הנכנס. אם פעולת ה-Intent הנכנסת תואמת ל-SHOW_MEDIA_PLAYBACK, המשתמש יועבר למסך "מה שומעים עכשיו?".

@Override
public void onNewIntent(@NonNull Intent intent) {
    super.onNewIntent(intent);
    if (SHOW_MEDIA_PLAYBACK.equals(intent.getAction())) {
        ScreenManager screenManager = getCarContext().getCarService(ScreenManager.class);
        // Avoid redundant navigation if already on the playing screen
        if (screenManager.getTop() instanceof MyMediaPlayScreen) {
            return;
        }
        screenManager.push(MyMediaPlayScreen.createScreenFromPlaying(
                getCarContext(), mMediaSessionController));
    }
}

אם אתם משתמשים בפעילות טרמפולינה, חפשו את פעולת ה-Intent בתוך onCreate(). מעבירים את הפעולה הזו ל-Intent של יצירת CarAppActivity לפני שקוראים ל-finish().

public class LaunchableTrampoline extends AppCompatActivity {
    private static final String SHOW_MEDIA_PLAYBACK = "androidx.car.app.media.action.SHOW_MEDIA_PLAYBACK";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent receivedIntent = getIntent();
        String action;

        if (SHOW_MEDIA_PLAYBACK.equals(receivedIntent.getAction())) {
            action = SHOW_MEDIA_PLAYBACK;
        } else {
            action = Intent.ACTION_MAIN;
        }

        Intent intent = new Intent(action);
        intent.setClassName(getPackageName(), "androidx.car.app.activity.CarAppActivity");
        startActivity(intent);
        finish();
    }
}