Google은 흑인 공동체를 위한 인종 간 평등을 진전시키기 위해 노력하고 있습니다. Google에서 어떤 노력을 하고 있는지 확인하세요.

Android Auto용 메시지 앱 빌드

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

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

이 과정에서는 채팅 앱처럼 사용자에게 메시지를 표시하고 사용자의 회신을 수신하는 앱을 빌드했다고 가정합니다. 또한 메시지를 Auto 기기로 전달하여 표시하고 회신하도록 앱을 확장하는 방법을 보여줍니다.

시작하기

앱이 Auto 기기에 메시지 서비스를 제공할 수 있게 하려면 앱에서 다음 작업을 할 수 있어야 합니다.

  1. 회신 및 읽음으로 표시 Action 객체가 포함된 NotificationCompat.MessagingStyle 객체를 빌드하고 전송합니다.
  2. 회신하고 대화를 읽음으로 표시하는 작업을 Service를 사용하여 처리합니다.
  3. 앱에서 Android Auto를 지원함을 표시하도록 매니페스트를 구성합니다.

개념 및 객체

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

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

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

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

앱은 Action 객체를 Notification에 추가하여 사용자가 알림 창에서 바로 메시지에 신속하게 회신하거나 읽음으로 표시하도록 할 수도 있습니다. 대화를 관리하려면 읽음으로 표시 및 회신 Action 객체가 Android Auto에 있어야 합니다.

요약하면 단일 대화는 단일 MessagingStyle 객체로 스타일이 지정된 단일 Notification 객체로 표현됩니다. MessagingStyle에는 하나 이상의 MessagingStyle.Message 객체와 함께 이 대화 내의 모든 메시지가 포함됩니다. 마지막으로 Android Auto와 완전히 호환되도록 하려면 회신 및 읽음으로 표시 ActionNotification에 연결해야 합니다.

메시지 흐름

이 섹션에서는 앱과 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>
    

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

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

앱에서 SMS, MMS 및 RCS 처리를 지원해야 하는 경우 다음 내용도 포함해야 합니다.

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

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

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

  1. 아래와 같이 최상위 build.gradle 파일에 Google Maven 저장소를 포함해야 합니다.

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

    dependencies {
            implementation 'androidx.core:core:1.0.0'
        }
        

사용자 작업 처리

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

인텐트 작업 정의

Intent 작업(알림 작업과 혼동하지 말 것)은 Intent의 목적이 무엇인지 식별할 수 있는 간단한 문자열입니다. 단일 서비스에서는 여러 유형의 인텐트를 처리할 수 있으므로 여러 IntentServices를 정의하는 것보다 여러 Intent.action 문자열을 정의하는 것이 더 쉽습니다.

예제 메시지 앱에는 다음 코드 샘플에 선언된 대로 회신 및 읽음으로 표시라는 두 가지 유형의 작업이 있습니다.

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?) {
            // Fetch our internal data
            val conversationId = intent!!.getIntExtra(EXTRA_CONVERSATION_ID_KEY, -1)

            // And search our database for that conversation
            val conversation = YourAppConversation.getById(conversationId)

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

또한 이 서비스를 앱과 연결하려면 아래와 같이 앱 manifiest에서 서비스를 등록해야 합니다.

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

인텐트 생성 및 처리

IntentPendingIntent를 통해 외부 앱으로 전달되므로 다른 앱에서 MessagingService를 트리거하는 Intent를 가져올 수 있는 방법은 없습니다. 이러한 제한이 있으므로 아래와 같이 다른 앱에서 '회신' 텍스트를 이 앱에 다시 제공할 수 있도록 RemoteInput 객체를 만들어야 합니다.

/**
     * Creates a [RemoteInput] which allows remote apps to 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 via
        // static methods in the RemoteInput class.
    }

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

        // This action string lets the MessagingService know this is a "reply" request.
        intent.action = ACTION_REPLY

        // Provide the conversation id so we know what conversation this applies to.
        intent.putExtra(EXTRA_CONVERSATION_ID_KEY, appConversation.id)

        return intent
    }
    

회신 Intent에 들어갈 내용을 알고 있으므로 아래와 같이 MessagingServiceACTION_REPLY 스위치 절의 TODO를 처리하고 이 정보를 추출합니다.

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

        // Note this conversation object came from above in the MessagingService
        conversation.reply(message)
    }
    

읽음으로 표시 Intent도 비슷한 방식으로 처리합니다(RemoteInput은 필요 없음).

/** Creates an [Intent] which 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 스위치 절의 TODO를 처리합니다.

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

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

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

Android Auto에서 회신 Action을 설정하려면 회신 Action에 관한 다음 세 가지 요구사항을 충족해야 합니다.

  • 시맨틱 작업이 Action.SEMANTIC_ACTION_REPLY로 설정되어야 합니다.
  • Action은 실행 시에 사용자 인터페이스를 표시하지 않음을 나타내야 합니다.
  • Action에 단일 RemoteInput이 포함되어야 합니다.

다음 코드 샘플은 위에 나열된 요구사항을 해결하는 동시에 회신 Action을 설정합니다.

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

            // The action doesn't show any UI (it's a requirement for apps to
            // not show UI for 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로 설정됩니다.
  • 이 작업은 실행될 때 사용자 인터페이스를 표시하지 않음을 나타냅니다.
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)
        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
    }
    

MessagingStyle 만들기

MessagingStyle은 메시지 정보 전달 수단으로, Android Auto가 대화에서 각 메시지를 소리내어 읽는 데 사용됩니다. 먼저 기기 사용자는 Person 객체의 형태로 지정되어야 합니다.

fun createMessagingStyle(
            context: Context, appConversation: YourAppConversation): MessagingStyle {
        // Method defined by our 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)

        // Set the conversation title. Note that if your app's target version is lower
        // than P, this will automatically mark this conversation as a group (to
        // maintain backwards compatibility). Simply 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 via a Person object
            val senderPerson = Person.Builder()
                .setName(appMessage.sender.name)
                .setIcon(appMessage.sender.icon)
                .setKey(appMessage.sender.id)
                .build()

            // And the message is added. Note that more complex messages (ie. images)
            // may 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) {
        // Create our Actions and MessagingStyle from the methods defined above.
        val replyAction = createReplyAction(context, appConversation)
        val markAsReadAction = createMarkAsReadAction(context, appConversation)
        val messagingStyle = createMessagingStyle(context, appConversation)

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

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

            // Add MessagingStyle.
            .setStyle(messagingStyle)

            // Add reply action.
            .addAction(replyAction)

            // Let's say we don't want to show our mark-as-read Action in the
            // notification shade. We can do that by adding it as invisible so it
            // won't appear in Android UI, but still satisfy Android Auto's
            // mark-as-read Action requirement. You're free to add both Actions as
            // visible or invisible. This is just a stylistic choice.
            .addInvisibleAction(markAsReadAction)

            .build()

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

참고 항목