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

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

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

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

שנתחיל?

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

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

לפני שמתחילים לעצב את האפליקציה, כדאי להבין איך 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 אחר, automotive_app_desc.xml, שצריך ליצור בספרייה res/xml של מודול האפליקציה. ב-automotive_app_desc.xml, מציינים את היכולות של Android Auto שהאפליקציה תומכת בהן. כדי להצהיר על תמיכה בהתראות, צריך לכלול את הפרטים הבאים:

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

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

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

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

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

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

Groovy

allprojects {
    repositories {
        google()
    }
}

Kotlin

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

Groovy

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

באפליקציית ההודעות לדוגמה שבמדריך הזה יש שני סוגים נדרשים של פעולות: reply (תגובה) ו-mark-as-read (סימון כהודעה שנקראה), כמו שמוצג בדוגמת הקוד הבאה:

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 כי הכוונות מועברות לאפליקציות אחרות דרך 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 clause בתוך 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 'תגובה' באמצעות המתודה createReplyIntent() מהקטע הקודם:

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, חשוב לדעת שיש 3 דרישות ב-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 של סימון כנקרא, שעונה על הדרישות האלה:

fun createMarkAsReadAction(
        context: Context, appConversation: YourAppConversation): Action {
    val markAsReadIntent = createMarkAsReadIntent(context, appConversation)
    val markAsReadPendingIntent = PendingIntent.getService(
            context,
            createMarkAsReadId(appConversation), // Method explained later.
            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 Issue Tracker. חשוב למלא את כל הפרטים הנדרשים בתבנית הבעיה.

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

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