Tạo ứng dụng nhắn tin cho Android Auto

Danh mục Truyền thông sắp ra mắt
Danh mục Nhắn tin đang được mở rộng để hỗ trợ các tính năng mới, bao gồm cả nhật ký tin nhắn và trải nghiệm gọi điện

Việc duy trì kết nối qua tin nhắn có ý nghĩa quan trọng với nhiều người lái xe. Các ứng dụng nhắn tin có thể cho người dùng biết có cần đến đón con hay không hoặc địa điểm ăn tối có thay đổi hay không. Khung Android cho phép ứng dụng nhắn tin mở rộng dịch vụ sang trải nghiệm lái xe bằng một giao diện người dùng tiêu chuẩn giúp người lái xe tập trung quan sát đường đi.

Các ứng dụng hỗ trợ tính năng nhắn tin có thể mở rộng thông báo nhắn tin để cho phép Android Auto sử dụng thông báo đó khi Auto đang chạy. Những thông báo này hiển thị trong Auto và cho phép người dùng đọc rồi trả lời tin nhắn trong một giao diện nhất quán và hạn chế sự mất tập trung. Đồng thời, khi sử dụng API MessagingStyle, bạn sẽ nhận được thông báo tin nhắn tối ưu hoá dành cho mọi thiết bị Android, bao gồm Android Auto. Những điểm tối ưu hoá bao gồm giao diện người dùng dành riêng cho thông báo tin nhắn, ảnh động được cải tiến và tính năng hỗ trợ hình ảnh trên cùng dòng.

Hướng dẫn này cho bạn biết cách mở rộng một ứng dụng hiển thị tin nhắn cho người dùng và nhận tin nhắn trả lời của người dùng (chẳng hạn như ứng dụng nhắn tin) để chuyển màn hình tin nhắn và thông báo xác nhận tin nhắn sang thiết bị Auto. Để biết hướng dẫn có liên quan về thiết kế, vui lòng xem phần Ứng dụng nhắn tin trên trang web Design for Driving (Design cho hoạt động lái xe).

Bắt đầu

Để cung cấp dịch vụ nhắn tin cho thiết bị Auto, ứng dụng của bạn phải khai báo tính năng hỗ trợ Android Auto trong tệp kê khai và có thể thực hiện những tác vụ sau:

  • Tạo và gửi đối tượng NotificationCompat.MessagingStyle chứa các đối tượng Action trả lời và đánh dấu là đã đọc.
  • Xử lý thao tác trả lời và đánh dấu cuộc trò chuyện là đã đọc bằng Service.

Khái niệm và đối tượng

Trước khi bắt đầu thiết kế ứng dụng, bạn cần nắm được cách Android Auto xử lý thao tác nhắn tin.

Một đoạn thông tin trao đổi riêng lẻ được gọi là tin nhắn và được biểu thị bằng lớp MessagingStyle.Message. Tin nhắn chứa người gửi, nội dung tin nhắn và thời gian gửi tin nhắn.

Hoạt động giao tiếp giữa những người dùng được gọi là cuộc trò chuyện và được biểu thị bằng đối tượng MessagingStyle. Một cuộc trò chuyện (MessagingStyle) chứa tiêu đề, tin nhắn và thông tin cho biết liệu đó có phải là cuộc trò chuyện giữa một nhóm người dùng hay không.

Để thông báo cho người dùng về nội dung cập nhật của cuộc trò chuyện (chẳng hạn như một tin nhắn mới), các ứng dụng sẽ đăng Notification lên hệ thống Android. Notification này sử dụng đối tượng MessagingStyle để hiển thị giao diện người dùng dành riêng cho tính năng nhắn tin trong ngăn thông báo. Nền tảng Android cũng truyền Notification này sang Android Auto, còn MessagingStyle được trích xuất và dùng để đăng thông báo qua màn hình của ô tô.

Android Auto cũng yêu cầu ứng dụng thêm các đối tượng Action vào Notification để cho phép người dùng nhanh chóng trả lời tin nhắn hoặc đánh dấu tin nhắn là đã đọc trực tiếp trong ngăn thông báo.

Tóm lại, một cuộc trò chuyện được biểu thị bằng đối tượng Notification. Đối tượng này được tạo kiểu bằng một đối tượng MessagingStyle. MessagingStyle chứa tất cả tin nhắn trong cuộc trò chuyện đó, trong một hoặc nhiều đối tượng MessagingStyle.Message. Ngoài ra, để tương thích với Android Auto, ứng dụng phải đính kèm đối tượng Action trả lời và đánh dấu là đã đọc vào Notification.

Quy trình nhắn tin

Phần này mô tả quy trình nhắn tin thông thường giữa ứng dụng của bạn và Android Auto.

  1. Ứng dụng của bạn nhận được một tin nhắn.
  2. Ứng dụng tạo một thông báo MessagingStyle có các đối tượng Action trả lời và đánh dấu là đã đọc.
  3. Android Auto nhận được sự kiện "thông báo mới" từ hệ thống Android và tìm thấy MessagingStyle, Action trả lời cũng như Action đánh dấu là đã đọc.
  4. Android Auto tạo và hiển thị thông báo trên ô tô.
  5. Nếu người dùng nhấn vào thông báo trên màn hình của ô tô, thì Android Auto sẽ kích hoạt Action đánh dấu là đã đọc.
    • Ở chế độ nền, ứng dụng của bạn phải xử lý sự kiện đánh dấu là đã đọc này.
  6. Nếu người dùng trả lời thông báo bằng giọng nói, thì Android Auto sẽ thêm bản chép lời phản hồi của người dùng vào Action trả lời rồi kích hoạt đối tượng đó.
    • Ở chế độ nền, ứng dụng của bạn phải xử lý sự kiện trả lời này.

Giả định sơ bộ

Trang này không hướng dẫn bạn cách tạo một ứng dụng nhắn tin hoàn chỉnh. Mã mẫu sau đây cho biết một số việc bạn cần thực hiện đối với ứng dụng của mình để có thể bắt đầu hỗ trợ tính năng nhắn tin qua 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)

Khai báo chức năng hỗ trợ Android Auto

Khi nhận được thông báo từ một ứng dụng nhắn tin, Android Auto sẽ kiểm tra để đảm bảo ứng dụng đó đã khai báo tính năng hỗ trợ Android Auto. Để bật tính năng hỗ trợ này, hãy thêm mục sau vào tệp kê khai của ứng dụng:

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

Mục kê khai này là một tệp XML khác mà bạn cần tạo bằng đường dẫn sau: YourAppProject/app/src/main/res/xml/automotive_app_desc.xml. Trong automotive_app_desc.xml, hãy khai báo các tính năng của Android Auto mà ứng dụng của bạn hỗ trợ. Ví dụ: để khai báo tính năng hỗ trợ cho thông báo, hãy thêm nội dung sau:

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

Nếu có thể đặt ứng dụng làm trình xử lý tin nhắn SMS mặc định, hãy nhớ thêm phần tử <uses> sau. Nếu không, trình xử lý mặc định tích hợp trong Android Auto sẽ xử lý tin nhắn SMS/MMS đến khi ứng dụng của bạn được đặt làm trình xử lý tin nhắn SMS mặc định. Điều này có thể dẫn đến thông báo trùng lặp.

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

Nhập thư viện AndroidX Core

Khi tạo thông báo để sử dụng với thiết bị Auto, bạn phải có thư viện AndroidX Core. Nhập thư viện này vào dự án của bạn theo cách dưới đây:

  1. Trong tệp build.gradle cấp cao nhất, hãy đưa phần phụ thuộc vào kho lưu trữ Maven của Google, như trong ví dụ sau:

Groovy

allprojects {
    repositories {
        google()
    }
}

Kotlin

allprojects {
    repositories {
        google()
    }
}
  1. Trong tệp build.gradle của mô-đun ứng dụng, hãy thêm phần phụ thuộc thư viện AndroidX Core, như trong ví dụ sau:

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")
}

Xử lý thao tác của người dùng

Ứng dụng nhắn tin của bạn cần có cách thức xử lý thao tác cập nhật cuộc trò chuyện thông qua Action. Đối với Android Auto, ứng dụng của bạn cần xử lý 2 loại đối tượng Action: trả lời và đánh dấu là đã đọc. Bạn nên xử lý các đối tượng này bằng IntentService, cho phép bạn xử lý linh hoạt các lệnh gọi có khả năng tốn nhiều tài nguyên ở chế độ nền, giúp giải phóng luồng chính của ứng dụng.

Xác định các thao tác theo ý định

Thao tác Intent là các chuỗi đơn giản dùng để xác định mục đích của Intent. Vì một dịch vụ có thể xử lý nhiều loại ý định nên việc xác định nhiều chuỗi thao tác sẽ dễ dàng hơn so với xác định nhiều thành phần IntentService.

Ứng dụng nhắn tin trong hướng dẫn này có 2 loại thao tác bắt buộc: trả lời và đánh dấu là đã đọc, như minh hoạ trong mã mẫu sau.

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

Tạo dịch vụ

Để tạo một dịch vụ xử lý những đối tượng Action này, bạn cần có mã nhận dạng cuộc trò chuyện – một cấu trúc dữ liệu tuỳ ý do ứng dụng của bạn xác định để nhận dạng cuộc trò chuyện. Bạn cũng cần có một khoá nhập từ xa. Điều này sẽ được thảo luận chi tiết sau trong phần này. Mã mẫu sau đây sẽ tạo một dịch vụ để xử lý các thao tác bắt buộc:

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()
        }
    }
}

Để liên kết dịch vụ này với ứng dụng của bạn, bạn cũng cần đăng ký dịch vụ trong tệp kê khai của ứng dụng, như trong ví dụ sau:

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

Tạo và xử lý ý định

Các ứng dụng khác, bao gồm cả Android Auto, không thể có được Intent kích hoạt MessagingService, vì Intent được truyền đến các ứng dụng khác thông qua PendingIntent. Do giới hạn này, bạn cần tạo đối tượng RemoteInput để cho phép các ứng dụng khác gửi văn bản trả lời về ứng dụng của bạn, như trong ví dụ sau:

/**
 * 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
}

Trong mệnh đề chuyển đổi ACTION_REPLY trong MessagingService, hãy trích xuất thông tin chuyển đến Intent trả lời, như trong ví dụ sau:

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)
}

Bạn sẽ xử lý Intent đánh dấu là đã đọc theo cách tương tự. Tuy nhiên, đối tượng này không yêu cầu RemoteInput, như trong ví dụ sau:

/** 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
}

Mệnh đề chuyển đổi ACTION_MARK_AS_READ trong MessagingService không cần thêm logic, như trong ví dụ sau:

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

Thông báo cho người dùng về tin nhắn

Sau khi xử lý xong tác vụ cuộc hội thoại, bước tiếp theo là tạo thông báo tương thích với Android Auto.

Tạo thao tác

Bạn có thể chuyển đối tượng Action đến các ứng dụng khác bằng Notification để kích hoạt các phương thức trong ứng dụng ban đầu. Đây là cách Android Auto có thể đánh dấu cuộc trò chuyện là đã đọc hoặc trả lời cuộc trò chuyện đó.

Để tạo Action, hãy bắt đầu bằng Intent. Ví dụ sau cho biết cách tạo một Intent "trả lời":

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

Sau đó, hãy gói Intent này trong một PendingIntent để chuẩn bị cho việc sử dụng ứng dụng bên ngoài. PendingIntent sẽ chặn mọi quyền truy cập vào Intent đã gói bằng cách chỉ hiển thị một nhóm phương thức chọn lọc cho phép ứng dụng nhận kích hoạt Intent hoặc lấy tên gói của ứng dụng gốc. Ứng dụng bên ngoài tuyệt đối không truy cập được vào Intent cơ bản hoặc dữ liệu có trong đó.

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

Trước khi bạn thiết lập Action trả lời, hãy lưu ý rằng Android Auto có 3 yêu cầu đối với Action trả lời:

  • Thao tác ngữ nghĩa phải được đặt thành Action.SEMANTIC_ACTION_REPLY.
  • Action phải cho biết rằng khi được kích hoạt, thao tác này sẽ không hiển thị giao diện người dùng nào.
  • Action phải chứa một RemoteInput duy nhất.

Mã mẫu sau đây thiết lập một Action trả lời đáp ứng các yêu cầu nêu trên:

    // ...
    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
}

Việc xử lý hành động đánh dấu là đã đọc cũng tương tự như vậy, ngoại trừ việc không có RemoteInput. Do đó, Android Auto có 2 yêu cầu đối với Action đánh dấu là đã đọc:

  • Thao tác ngữ nghĩa được đặt thành Action.SEMANTIC_ACTION_MARK_AS_READ.
  • Thao tác này cho biết rằng khi được kích hoạt, thao tác sẽ không hiển thị giao diện người dùng nào.

Mã mẫu sau đây thiết lập một Action đánh dấu là đã đọc, đáp ứng các yêu cầu sau:

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
}

Khi tạo ý định đang chờ xử lý, bạn có thể dùng 2 hai phương thức sau: createReplyId()createMarkAsReadId(). Các phương thức này đóng vai trò là mã yêu cầu cho từng PendingIntent (được Android sử dụng để kiểm soát các ý định đang chờ xử lý hiện có). Các phương thức create() phải trả về mã nhận dạng duy nhất cho mỗi cuộc trò chuyện, nhưng các lệnh gọi lặp lại cho cùng một cuộc trò chuyện phải trả về mã nhận dạng duy nhất đã tạo.

Hãy xem xét ví dụ với 2 cuộc trò chuyện, A và B: Mã trả lời của cuộc trò chuyện A là 100 và mã đánh dấu là đã đọc là 101. Mã trả lời của cuộc trò chuyện B là 102 và mã đánh dấu là đã đọc là 103. Nếu cuộc trò chuyện A được cập nhật, thì mã trả lời vẫn là 100 và mã đánh dấu là đã đọc vẫn là 101. Để biết thêm thông tin, hãy xem PendingIntent.FLAG_UPDATE_CURRENT.

Tạo MessagingStyle

MessagingStyle là phần tử mang thông tin nhắn tin. Android Auto sử dụng phần tử này để đọc to từng tin nhắn trong một cuộc trò chuyện.

Trước tiên, bạn phải chỉ định người dùng thiết bị ở dạng đối tượng Person, như trong ví dụ sau:

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()
    // ...

Sau đó, bạn có thể tạo đối tượng MessagingStyle và cung cấp một số thông tin chi tiết về cuộc trò chuyện.

    // ...
    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)
    // ...

Cuối cùng, hãy thêm các tin nhắn chưa đọc.

    // ...
    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
}

Đóng gói và gửi thông báo

Sau khi tạo các đối tượng ActionMessagingStyle, bạn có thể tạo và đăng 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)
}

Tài nguyên khác

Báo cáo sự cố Nhắn tin trên Android Auto

Nếu gặp sự cố trong khi phát triển ứng dụng nhắn tin cho Android Auto, bạn có thể báo cáo sự cố đó bằng Công cụ theo dõi lỗi của Google. Hãy nhớ điền tất cả thông tin được yêu cầu vào mẫu báo cáo lỗi.

Báo lỗi mới

Trước khi báo lỗi mới, hãy kiểm tra xem lỗi đó đã được báo cáo trong danh sách lỗi hay chưa. Bạn có thể đăng ký theo dõi và bình chọn cho các lỗi bằng cách nhấp vào dấu sao cho một lỗi trong công cụ theo dõi. Để biết thêm thông tin, hãy xem bài viết Đăng ký theo dõi lỗi.