בניית אפליקציות להעברת הודעות ל-Android Auto

בקרוב תהיה אפשרות להשתמש בקטגוריה 'תקשורת'
הקטגוריה 'הודעות' מורחבת כדי לכלול תמיכה ביכולות חדשות, כולל היסטוריית הודעות וחוויית שיחות

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

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

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

תהליך שליחת ההודעות

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

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

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

כדי ליצור התראות לשימוש במכשירי 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.15.0'

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

Kotlin

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

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

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

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

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

פעולות Intent הן מחרוזות פשוטות שמזהות את המטרה של 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 בתוך 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 בתוך 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(). השיטות האלה משמשות כקודי הבקשה של כל PendingIntent, שבהם Android משתמשת כדי לשלוט בכוונות קיימות בהמתנה. השיטות של 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 Messaging

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

דיווח על בעיה חדשה

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