הרחבת ההתראות על הודעות ל-Android Auto

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

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

הנחיות עיצוב קשורות זמינות במאמר אפליקציות להעברת הודעות באתר Design for Driving.

שנתחיל?

כדי לספק שירות הודעות למכשירי Auto, האפליקציה צריכה להצהיר על התמיכה שלה ב-Android Auto במניפסט, ולבצע את הפעולות הבאות:

  • יצירה ושליחה של אובייקטים NotificationCompat.MessagingStyle שמכילים אובייקטים של תשובה וסימון כנקרא Action.
  • איך משיבים ומסמנים ששיחה נקראה באמצעות Service.

מושגים ואובייקטים

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

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

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

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

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

לסיכום, שיחה אחת מיוצגת על ידי אובייקט Notification שעוצב באמצעות אובייקט MessagingStyle. האובייקט MessagingStyle מכיל את כל ההודעות בשיחה באובייקט MessagingStyle.Message אחד או יותר. בנוסף, כדי שאפליקציה תהיה תואמת ל-Android Auto, היא צריכה לצרף אובייקטים של Action ושל Notification ל-Notification.

תהליך העבודה של העברת הודעות

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

  1. האפליקציה מקבלת הודעה.
  2. האפליקציה יוצרת התראה MessagingStyle עם אובייקטים של תגובה וסימון כנקרא Action.
  3. ‫Android Auto מקבל את האירוע 'התראה חדשה' ממערכת Android ומוצא את MessagingStyle, את התשובה Action ואת הסימון כנקרא Action.
  4. מערכת Android Auto יוצרת התראה ומציגה אותה ברכב.
  5. אם המשתמש מקיש על ההתראה במסך הרכב, Android Auto מפעיל את הפעולה 'סימון כפריט שנקרא' Action.
    • ברקע, האפליקציה צריכה לטפל באירוע הזה של סימון כפריט שנקרא.
  6. אם המשתמש מגיב להודעה באמצעות קול, Android Auto מציב תמלול של התגובה של המשתמש בתשובה Action ואז מפעיל אותה.
    • ברקע, האפליקציה צריכה לטפל באירוע התשובה הזה.

הנחות ראשוניות

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

data class YourAppConversation(
        val id: Int,
        val title: String,
        val recipients: MutableList<YourAppUser>,
        val icon: Bitmap) {
    companion object {
        /** Fetches [YourAppConversation] by its [id]. */
        fun getById(id: Int): YourAppConversation = // ...
    }

    /** Replies to this conversation with the given [message]. */
    fun reply(message: String) {}

    /** Marks this conversation as read. */
    fun markAsRead() {}

    /** Retrieves all unread messages from this conversation. */
    fun getUnreadMessages(): List<YourAppMessage> { return /* ... */ }
}
data class YourAppUser(val id: Int, val name: String, val icon: Uri)
data class YourAppMessage(
    val id: Int,
    val sender: YourAppUser,
    val body: String,
    val timeReceived: Long)

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

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

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

רשומה זו במניפסט מתייחסת לקובץ XML אחר שצריך ליצור בנתיב הבא: YourAppProject/app/src/main/res/xml/automotive_app_desc.xml. ב-automotive_app_desc.xml מציינים את היכולות של Android Auto שהאפליקציה תומכת בהן. לדוגמה, כדי להצהיר על תמיכה בהתראות, צריך לכלול את השורה הבאה:

<automotiveApp>
    <uses name="notification" />
</automotiveApp>

אם אפשר להגדיר את האפליקציה כמטפל ברירת המחדל ב-SMS, צריך לוודא שכוללים את רכיב <uses> הבא. אם לא תעשו זאת, נעשה שימוש ב-handler שמוגדר כברירת מחדל ומובנה ב-Android Auto כדי לטפל בהודעות SMS/MMS נכנסות כשהאפליקציה שלכם מוגדרת כ-handler ל-SMS שמוגדר כברירת מחדל, מה שעלול להוביל להתרעות כפולות.

<automotiveApp>
    ...
    <uses name="sms" />
</automotiveApp>

ייבוא ספריית הליבה של AndroidX

כדי ליצור התראות לשימוש במכשירי Auto, צריך להשתמש בספריית הליבה של AndroidX. מייבאים את הספרייה לפרויקט באופן הבא:

  1. בקובץ build.gradle ברמה העליונה, כוללים תלות במאגר Maven של Google, כמו בדוגמה הבאה:

גרוב

allprojects {
    repositories {
        google()
    }
}

Kotlin

allprojects {
    repositories {
        google()
    }
}
  1. בקובץ build.gradle של מודול האפליקציה, כוללים את תלות הספרייה AndroidX Core, כמו בדוגמה הבאה:

גרוב

dependencies {
    // If your app is written in Java
    implementation 'androidx.core:core:1.17.0'

    // If your app is written in Kotlin
    implementation 'androidx.core:core-ktx:1.17.0'
}

Kotlin

dependencies {
    // If your app is written in Java
    implementation("androidx.core:core:1.17.0")

    // If your app is written in Kotlin
    implementation("androidx.core:core-ktx:1.17.0")
}

טיפול בפעולות משתמש

אפליקציית הצ'אט צריכה דרך לעדכן שיחה באמצעות Action. ב-Android Auto, יש שני סוגים של Action אובייקטים שהאפליקציה צריכה לטפל בהם: תשובה וסימון כנקרא. מומלץ לטפל בהם באמצעות IntentService, שמאפשר לטפל בקריאות יקרות פוטנציאליות ברקע, וכך לאפשר לשרשור הראשי של האפליקציה לפעול באופן חופשי.

הגדרת פעולות של כוונות

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

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

private const val ACTION_REPLY = "com.example.REPLY"
private const val ACTION_MARK_AS_READ = "com.example.MARK_AS_READ"

יצירת השירות

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

private const val EXTRA_CONVERSATION_ID_KEY = "conversation_id"
private const val REMOTE_INPUT_RESULT_KEY = "reply_input"

/**
 * An [IntentService] that handles reply and mark-as-read actions for
 * [YourAppConversation]s.
 */
class MessagingService : IntentService("MessagingService") {
    override fun onHandleIntent(intent: Intent?) {
        // Fetches internal data.
        val conversationId = intent!!.getIntExtra(EXTRA_CONVERSATION_ID_KEY, -1)

        // Searches the database for that conversation.
        val conversation = YourAppConversation.getById(conversationId)

        // Handles the action that was requested in the intent. The TODOs
        // are addressed in a later section.
        when (intent.action) {
            ACTION_REPLY -> TODO()
            ACTION_MARK_AS_READ -> TODO()
        }
    }
}

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

<application>
    <service android:name="com.example.MessagingService" />
    ...
</application>

יצירה וטיפול בכוונות

אין דרך שאפליקציות אחרות, כולל Android Auto, יקבלו את Intent שמופעל על ידי MessagingService, כי Intent מועברים לאפליקציות אחרות דרך PendingIntent. בגלל המגבלה הזו, צריך ליצור אובייקט RemoteInput כדי לאפשר לאפליקציות אחרות לספק את הטקסט של התשובה בחזרה לאפליקציה שלכם, כמו בדוגמה הבאה:

/**
 * Creates a [RemoteInput] that lets remote apps provide a response string
 * to the underlying [Intent] within a [PendingIntent].
 */
fun createReplyRemoteInput(context: Context): RemoteInput {
    // RemoteInput.Builder accepts a single parameter: the key to use to store
    // the response in.
    return RemoteInput.Builder(REMOTE_INPUT_RESULT_KEY).build()
    // Note that the RemoteInput has no knowledge of the conversation. This is
    // because the data for the RemoteInput is bound to the reply Intent using
    // static methods in the RemoteInput class.
}

/** Creates an [Intent] that handles replying to the given [appConversation]. */
fun createReplyIntent(
        context: Context, appConversation: YourAppConversation): Intent {
    // Creates the intent backed by the MessagingService.
    val intent = Intent(context, MessagingService::class.java)

    // Lets the MessagingService know this is a reply request.
    intent.action = ACTION_REPLY

    // Provides the ID of the conversation that the reply applies to.
    intent.putExtra(EXTRA_CONVERSATION_ID_KEY, appConversation.id)

    return intent
}

במשפט התנאי ACTION_REPLY switch בתוך MessagingService, תחלץ את המידע שייכלל בתגובה Intent, כמו בדוגמה הבאה:

ACTION_REPLY -> {
    // Extracts reply response from the intent using the same key that the
    // RemoteInput uses.
    val results: Bundle = RemoteInput.getResultsFromIntent(intent)
    val message = results.getString(REMOTE_INPUT_RESULT_KEY)

    // This conversation object comes from the MessagingService.
    conversation.reply(message)
}

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

/** Creates an [Intent] that handles marking the [appConversation] as read. */
fun createMarkAsReadIntent(
        context: Context, appConversation: YourAppConversation): Intent {
    val intent = Intent(context, MessagingService::class.java)
    intent.action = ACTION_MARK_AS_READ
    intent.putExtra(EXTRA_CONVERSATION_ID_KEY, appConversation.id)
    return intent
}

המשפט ACTION_MARK_AS_READ switch בתוך MessagingService לא דורש לוגיקה נוספת, כמו בדוגמה הבאה:

// Marking as read has no other logic.
ACTION_MARK_AS_READ -> conversation.markAsRead()

הודעה למשתמשים על הודעות

אחרי שהטיפול בפעולות בשיחה מסתיים, השלב הבא הוא ליצור התראות שתואמות ל-Android Auto.

יצירת פעולות

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

כדי ליצור Action, מתחילים עם Intent. בדוגמה הבאה מוצג אופן יצירת Intent 'תשובה':

fun createReplyAction(
        context: Context, appConversation: YourAppConversation): Action {
    val replyIntent: Intent = createReplyIntent(context, appConversation)
    // ...

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

    // ...
    val replyPendingIntent = PendingIntent.getService(
        context,
        createReplyId(appConversation), // Method explained later.
        replyIntent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
    // ...

לפני שמגדירים את התשובה Action, חשוב לדעת שיש שלוש דרישות של Android Auto לתשובה Action:

  • הפעולה הסמנטית צריכה להיות Action.SEMANTIC_ACTION_REPLY.
  • התג Action צריך לציין שהוא לא יציג ממשק משתמש כשהוא יופעל.
  • התג Action חייב להכיל תג RemoteInput אחד.

בדוגמה הבאה של קוד מוגדרת תשובה Action שעומדת בדרישות שצוינו למעלה:

    // ...
    val replyAction = Action.Builder(R.drawable.reply, "Reply", replyPendingIntent)
        // Provides context to what firing the Action does.
        .setSemanticAction(Action.SEMANTIC_ACTION_REPLY)

        // The action doesn't show any UI, as required by Android Auto.
        .setShowsUserInterface(false)

        // Don't forget the reply RemoteInput. Android Auto will use this to
        // make a system call that will add the response string into
        // the reply intent so it can be extracted by the messaging app.
        .addRemoteInput(createReplyRemoteInput(context))
        .build()

    return replyAction
}

הטיפול בפעולה 'סימון ש'כבר קראתי'' דומה, אבל אין RemoteInput. לכן, ל-Android Auto יש שתי דרישות לסימון הודעות כנקראו Action:

  • הפעולה הסמנטית מוגדרת ל-Action.SEMANTIC_ACTION_MARK_AS_READ.
  • הפעולה מציינת שלא יוצג ממשק משתמש כשהיא תופעל.

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

fun createMarkAsReadAction(
        context: Context, appConversation: YourAppConversation): Action {
    val markAsReadIntent = createMarkAsReadIntent(context, appConversation)
    val markAsReadPendingIntent = PendingIntent.getService(
            context,
            createMarkAsReadId(appConversation), // Method explained below.
            markAsReadIntent,
            PendingIntent.FLAG_UPDATE_CURRENT  or PendingIntent.FLAG_IMMUTABLE)
    val markAsReadAction = Action.Builder(
            R.drawable.mark_as_read, "Mark as Read", markAsReadPendingIntent)
        .setSemanticAction(Action.SEMANTIC_ACTION_MARK_AS_READ)
        .setShowsUserInterface(false)
        .build()
    return markAsReadAction
}

כשיוצרים את הכוונות בהמתנה, משתמשים בשתי שיטות: createReplyId() ו-createMarkAsReadId(). ה-methods האלה משמשות כקודי בקשה לכל PendingIntent, שמשמשים את Android כדי לשלוט ב-intents ממתינים קיימים. השיטות create() צריכות להחזיר מזהים ייחודיים לכל שיחה, אבל קריאות חוזרות לאותה שיחה צריכות להחזיר את המזהה הייחודי שכבר נוצר.

נניח שיש שתי שיחות, א' וב': מזהה התשובה של שיחה א' הוא 100, ומזהה הסימון כשיחה שנקראה הוא 101. מזהה התשובה בשיחה ב' הוא 102, ומזהה הסימון כנקרא הוא 103. אם שיחה א' מתעדכנת, המזהים של התשובה ושל הסימון כנקרא עדיין יהיו 100 ו-101. מידע נוסף זמין במאמר PendingIntent.FLAG_UPDATE_CURRENT.

יצירת MessagingStyle

MessagingStyle הוא הספק של פרטי ההודעות, ו-Android Auto משתמש בו כדי להקריא כל הודעה בשיחה.

קודם צריך לציין את המשתמש במכשיר באמצעות אובייקט Person, כמו בדוגמה הבאה:

fun createMessagingStyle(
        context: Context, appConversation: YourAppConversation): MessagingStyle {
    // Method defined by the messaging app.
    val appDeviceUser: YourAppUser = getAppDeviceUser()

    val devicePerson = Person.Builder()
        // The display name (also the name that's read aloud in Android auto).
        .setName(appDeviceUser.name)

        // The icon to show in the notification shade in the system UI (outside
        // of Android Auto).
        .setIcon(appDeviceUser.icon)

        // A unique key in case there are multiple people in this conversation with
        // the same name.
        .setKey(appDeviceUser.id)
        .build()
    // ...

אחר כך אפשר ליצור את אובייקט MessagingStyle ולספק פרטים על השיחה.

    // ...
    val messagingStyle = MessagingStyle(devicePerson)

    // Sets the conversation title. If the app's target version is lower
    // than P, this will automatically mark the conversation as a group (to
    // maintain backward compatibility). Use `setGroupConversation` after
    // setting the conversation title to explicitly override this behavior. See
    // the documentation for more information.
    messagingStyle.setConversationTitle(appConversation.title)

    // Group conversation means there is more than 1 recipient, so set it as such.
    messagingStyle.setGroupConversation(appConversation.recipients.size > 1)
    // ...

לבסוף, מוסיפים את ההודעות שלא נקראו.

    // ...
    for (appMessage in appConversation.getUnreadMessages()) {
        // The sender is also represented using a Person object.
        val senderPerson = Person.Builder()
            .setName(appMessage.sender.name)
            .setIcon(appMessage.sender.icon)
            .setKey(appMessage.sender.id)
            .build()

        // Adds the message. More complex messages, like images,
        // can be created and added by instantiating the MessagingStyle.Message
        // class directly. See documentation for details.
        messagingStyle.addMessage(
                appMessage.body, appMessage.timeReceived, senderPerson)
    }

    return messagingStyle
}

אריזה ושליחה של ההתראה

אחרי יצירת האובייקטים Action ו-MessagingStyle, אפשר ליצור ולפרסם את Notification.

fun notify(context: Context, appConversation: YourAppConversation) {
    // Creates the actions and MessagingStyle.
    val replyAction = createReplyAction(context, appConversation)
    val markAsReadAction = createMarkAsReadAction(context, appConversation)
    val messagingStyle = createMessagingStyle(context, appConversation)

    // Creates the notification.
    val notification = NotificationCompat.Builder(context, channel)
        // A required field for the Android UI.
        .setSmallIcon(R.drawable.notification_icon)

        // Shows in Android Auto as the conversation image.
        .setLargeIcon(appConversation.icon)

        // Adds MessagingStyle.
        .setStyle(messagingStyle)

        // Adds reply action.
        .addAction(replyAction)

        // Makes the mark-as-read action invisible, so it doesn't appear
        // in the Android UI but the app satisfies Android Auto's
        // mark-as-read Action requirement. Both required actions can be made
        // visible or invisible; it is a stylistic choice.
        .addInvisibleAction(markAsReadAction)

        .build()

    // Posts the notification for the user to see.
    val notificationManagerCompat = NotificationManagerCompat.from(context)
    notificationManagerCompat.notify(appConversation.id, notification)
}

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

דיווח על בעיה בהודעות ב-Android Auto

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

יצירת בעיה חדשה

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