Android Auto용 메시지 앱 빌드

많은 운전자에게는 메시지를 끊김 없이 주고받는 것이 중요합니다. 채팅 앱을 통해 사용자는 자녀를 데리러 가야 하는지, 저녁 식사 장소가 변경되었는지 등을 알 수 있습니다. Android 프레임워크를 통해 메시지 앱은 운전자가 도로를 주시할 수 있게 해주는 일반 사용자 인터페이스를 사용하여 서비스를 운전 환경으로 확장할 수 있습니다.

메시지를 지원하는 앱에서는 메시지 알림을 확장하여 Android Auto가 실행될 때 Auto에서 메시지 알림을 사용하도록 허용할 수 있습니다. 이러한 알림은 Auto에 표시되므로 사용자는 일관성 있고 주의 분산이 적은 인터페이스에서 메시지를 읽고 응답할 수 있습니다. 또한 MessagingStyle API를 사용하면 Android Auto를 비롯한 모든 Android 기기에 최적화된 메시지 알림을 받게 됩니다. 최적화에는 메시지 알림에 특화된 UI, 향상된 애니메이션, 인라인 이미지 지원이 포함됩니다.

이 가이드에서는 사용자에게 메시지를 표시하고 사용자의 회신을 수신하는 앱(예: 채팅 앱)을 확장하여 메시지 표시 및 회신 수신을 Auto 기기에 전달하는 방법을 보여줍니다. 관련 디자인 안내는 운전을 위한 디자인 사이트의 메시지 앱을 참고하세요.

시작하기

Auto 기기에 메시지 서비스를 제공하려면 앱은 매니페스트에서 Android Auto 지원을 선언하고 다음 작업을 할 수 있어야 합니다.

  • 회신 및 읽음으로 표시 Action 객체가 포함된 NotificationCompat.MessagingStyle 객체를 빌드하고 전송합니다.
  • 회신하고 대화를 읽음으로 표시하는 작업을 Service를 사용하여 처리합니다.

개념 및 객체

앱 설계를 시작하기 전에 Android Auto에서 메시지를 처리하는 방법을 이해하면 도움이 됩니다.

개별 통신 청크는 메시지라고 하며 MessagingStyle.Message 클래스로 표현됩니다. 메시지에는 발신자, 메시지 콘텐츠, 메시지가 전송된 시간이 포함됩니다.

사용자 간의 통신은 대화라고 하며 MessagingStyle 객체로 표현됩니다. 대화 또는 MessagingStyle에는 제목, 메시지, 대화가 사용자 그룹에 속하는지 여부가 포함됩니다.

대화 업데이트(예: 새 메시지)에 관해 사용자에게 알리기 위해 앱은 Notification을 Android 시스템에 게시합니다. 이 NotificationMessagingStyle 객체를 사용하여 알림 창에 메시지별 UI를 표시합니다. Android 플랫폼에서도 이 Notification을 Android Auto에 전달하며 MessagingStyle은 추출되어 자동차의 화면에 알림을 게시하는 데 사용됩니다.

또한 Android Auto에서는 사용자가 메시지에 빠르게 회신하거나 알림 창에서 직접 읽음으로 표시할 수 있도록 앱이 Action 객체를 Notification에 추가해야 합니다.

요약하면 단일 대화는 MessagingStyle 객체로 스타일이 지정된 Notification 객체로 표현됩니다. MessagingStyle에는 하나 이상의 MessagingStyle.Message 객체에 있는 해당 대화 내의 모든 메시지가 포함되어 있습니다. 또한 Android Auto와 호환되려면 앱에서 회신 및 읽음으로 표시 Action 객체를 Notification에 연결해야 합니다.

메시지 흐름

이 섹션에서는 앱과 Android Auto 간의 일반적인 메시지 흐름에 관해 설명합니다.

  1. 앱에서 메시지를 수신합니다.
  2. 앱은 회신 및 읽음으로 표시 Action 객체로 MessagingStyle 알림을 생성합니다.
  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>

이 매니페스트 항목은 YourAppProject/app/src/main/res/xml/automotive_app_desc.xml 경로를 사용하여 만들어야 하는 다른 XML 파일을 참조합니다. automotive_app_desc.xml에서 앱이 지원하는 Android Auto 기능을 선언합니다. 예를 들어 알림 지원을 선언하려면 다음을 포함하세요.

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

앱을 기본 SMS 핸들러로 설정할 수 있다면 다음 <uses> 요소를 포함해야 합니다. 포함하지 않으면 Android Auto에 내장된 기본 핸들러가 앱이 기본 SMS 핸들러로 설정되어 있을 때 수신 SMS/MMS 메시지를 처리하는 데 사용되며 이로 인해 중복 알림이 발생할 수 있습니다.

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

AndroidX 핵심 라이브러리 가져오기

Auto 기기에서 사용할 알림을 빌드하려면 AndroidX 핵심 라이브러리가 필요합니다. 다음과 같이 라이브러리를 프로젝트로 가져옵니다.

  1. 다음 예와 같이 최상위 build.gradle 파일에서 Google Maven 저장소의 종속 항목을 포함합니다.

Groovy

allprojects {
    repositories {
        google()
    }
}

Kotlin

allprojects {
    repositories {
        google()
    }
}
  1. 앱 모듈의 build.gradle 파일에서 다음 예와 같이 AndroidX 핵심 라이브러리 종속 항목을 포함합니다.

Groovy

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

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

Kotlin

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

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

사용자 작업 처리

메시지 앱은 Action을 통해 대화 업데이트를 처리할 방법이 있어야 합니다. Android Auto의 경우 앱에서 처리해야 할 두 가지 유형의 Action 객체로 회신 및 읽음으로 표시가 있습니다. IntentService를 사용하여 처리하는 것이 좋습니다. 그러면 비용이 많이 들 수 있는 호출을 유연하게 백그라운드에서 처리할 수 있으므로 앱의 기본 스레드가 해제됩니다.

인텐트 작업 정의

Intent 작업은 Intent의 용도를 식별하는 간단한 문자열입니다. 단일 서비스에서는 여러 유형의 인텐트를 처리할 수 있으므로 여러 IntentService 구성요소를 정의하는 대신 여러 작업 문자열을 정의하는 것이 더 쉽습니다.

이 가이드의 메시지 앱 예에는 다음 코드 샘플과 같이 2가지 필수 작업 유형인 회신 및 읽음으로 표시가 있습니다.

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

서비스 만들기

이러한 Action 객체를 처리하는 서비스를 만들려면 대화를 식별하는, 앱에서 정의한 임의의 데이터 구조인 대화 ID가 필요합니다. 또한 원격 입력 키가 필요합니다. 원격 입력 키는 이 섹션의 뒷부분에서 자세히 설명합니다. 다음 코드 샘플은 필수 작업을 처리하는 서비스를 만듭니다.

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를 비롯한 다른 앱에서는 MessagingService를 트리거하는 Intent를 가져올 방법이 없습니다. IntentPendingIntent를 통해 다른 앱에 전달되기 때문입니다. 이러한 제한으로 인해, 다음 예와 같이 다른 앱이 내 앱에 회신 텍스트를 다시 제공할 수 있도록 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
}

MessagingServiceACTION_REPLY 스위치 절에서 다음 예와 같이 회신 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
}

MessagingServiceACTION_MARK_AS_READ 스위치 절에는 다음 예와 같이 추가 로직이 필요하지 않습니다.

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

그러면 이 IntentPendingIntent에 래핑하여 외부 앱에서 사용할 수 있도록 준비합니다. PendingIntent는 수신 앱이 Intent를 실행하거나 발신 앱의 패키지 이름을 가져오도록 허용하는 일련의 선택된 메서드만 노출함으로써 래핑된 Intent에 대한 모든 액세스를 차단합니다. 외부 앱은 기본 Intent 또는 그 내부의 데이터에 액세스할 수 없습니다.

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

Android Auto에서 회신 Action을 설정하려면 회신 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() 메서드는 각 대화의 고유 ID를 반환해야 하지만 동일한 대화를 반복적으로 호출하면 이미 생성된 고유 ID가 반환되어야 합니다.

대화 A와 B라는 두 가지 대화가 있다고 가정해 보겠습니다. 대화 A의 회신 ID는 100이고 읽음으로 표시 ID는 101입니다. 대화 B의 회신 ID는 102이고 읽음으로 표시 ID는 103입니다. 대화 A가 업데이트되어도 회신 및 읽음으로 표시 ID는 여전히 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
}

알림 패키징 및 푸시

ActionMessagingStyle 객체를 생성한 후에는 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를 사용하여 신고할 수 있습니다. 문제 템플릿에 요청된 모든 정보를 작성해야 합니다.

새로운 문제 제출하기

새로운 문제를 신고하기 전에 그 문제가 문제 목록에 이미 보고되었는지 확인합니다. Tracker에서 문제에 있는 별표를 클릭하여 문제를 구독하고 투표를 할 수 있습니다. 자세한 내용은 문제 구독을 참고하세요.