자동차 앱 라이브러리 기본사항 알아보기

1. 시작하기 전에

이 Codelab에서는 자동차용 Android 앱 라이브러리를 사용하여 Android AutoAndroid Automotive OS용 주의 분산 행동 최적화 앱을 빌드하는 방법을 알아봅니다. 먼저 Android Auto 지원을 추가하고 최소한의 추가 작업으로 Android Automotive OS에서 실행할 수 있는 앱의 변형을 만듭니다. 앱이 두 플랫폼에서 모두 실행되면 추가 화면과 기본 상호작용을 빌드합니다.

이 Codelab의 의도하지 않은 용도

  • Android Auto 및 Android Automotive OS용 미디어(오디오) 앱을 만드는 방법에 관한 가이드. 이러한 앱을 빌드하는 방법에 관한 자세한 내용은 자동차용 미디어 앱 빌드를 참고하세요.
  • Android Auto용 메시지 앱을 만드는 방법에 관한 가이드. 이러한 앱을 빌드하는 방법에 관한 자세한 내용은 Android Auto용 메시지 앱 빌드를 참고하세요.

필요한 항목

빌드할 항목

Android Auto

Android Automotive OS

데스크톱 헤드 유닛을 사용하여 Android Auto에서 실행되는 앱을 보여주는 화면 녹화

Android Automotive OS 에뮬레이터에서 실행되는 앱을 보여주는 화면 녹화

학습할 내용

  • 자동차 앱 라이브러리의 클라이언트-호스트 아키텍처가 작동하는 방식
  • 자체 CarAppService, Session, Screen 클래스를 작성하는 방법
  • Android Auto와 Android Automotive OS에서 모두 구현을 공유하는 방법
  • 데스크톱 헤드 유닛을 사용하여 개발 머신에서 Android Auto를 실행하는 방법
  • Android Automotive OS 에뮬레이터를 실행하는 방법

2. 설정

코드 가져오기

  1. 이 Codelab의 코드는 car-codelabs GitHub 저장소의 car-app-library-fundamentals 디렉터리에서 찾을 수 있습니다. 코드를 클론하려면 다음 명령어를 실행합니다.
git clone https://github.com/android/car-codelabs.git
  1. 또는 저장소를 ZIP 파일로 다운로드할 수도 있습니다.

프로젝트 열기

  • Android 스튜디오를 시작한 후 car-app-library-fundamentals/start 디렉터리만 선택하여 프로젝트를 가져옵니다. car-app-library-fundamentals/end 디렉터리에는 솔루션 코드가 포함되어 있습니다. 솔루션 코드는 도움이 필요한 경우 또는 전체 프로젝트를 살펴보고 싶을 때 언제든지 참조할 수 있습니다.

코드 숙지하기

  • Android 스튜디오에서 프로젝트를 열고 시작 코드를 살펴봅니다.

앱의 시작 코드가 :app, :common:data라는 두 모듈로 나뉘어져 있습니다.

:app 모듈은 :common:data 모듈에 종속됩니다.

:app 모듈에는 모바일 앱의 UI와 로직이 포함되어 있고 :common:data 모듈에는 Place 모델 데이터 클래스Place 모델을 읽는 데 사용되는 저장소가 포함되어 있습니다. 편의상 저장소는 하드코딩된 목록에서 읽지만 실제 앱에서는 데이터베이스나 백엔드 서버에서 쉽게 읽을 수 있습니다.

:app 모듈에는 Place 모델의 목록을 읽고 표시할 수 있도록 :common:data 모듈의 종속 항목이 포함됩니다.

3. 자동차용 Android 앱 라이브러리 알아보기

자동차용 Android 앱 라이브러리는 개발자가 차량에서 사용할 앱을 빌드할 수 있도록 지원하는 Jetpack 라이브러리 세트입니다. 운전에 최적화된 사용자 인터페이스를 제공하면서 자동차에 있는 다양한 하드웨어 구성(예: 입력 방법, 화면 크기, 가로세로 비율)에도 적응할 수 있는 템플릿 프레임워크를 제공합니다. 이를 통해 개발자는 쉽게 앱을 빌드하고 Android Auto 및 Android Automotive OS를 모두 실행하는 다양한 차량에서 앱이 잘 작동할 것이라고 확신할 수 있습니다.

작동 방식 알아보기

자동차 앱 라이브러리를 사용하여 빌드된 앱은 Android Auto나 Android Automotive OS에서 직접 실행되지 않습니다. 대신 클라이언트 앱과 통신하고 클라이언트의 사용자 인터페이스를 대신 렌더링하는 호스트 앱을 사용합니다. Android Auto 자체가 호스트이고 Google Automotive 앱 호스트는 Google 탑재 Android Automotive OS 차량에서 사용되는 호스트입니다. 다음은 앱을 빌드할 때 확장해야 하는 자동차 앱 라이브러리의 주요 클래스입니다.

CarAppService

CarAppService는 Android Service 클래스의 서브클래스이며 호스트 애플리케이션이 클라이언트 앱(예: 이 Codelab에서 빌드하는 앱)과 통신하는 진입점 역할을 합니다. 기본 목적은 호스트 앱이 상호작용하는 Session 인스턴스를 만드는 것입니다.

Session

Session은 차량의 디스플레이에서 실행되는 클라이언트 앱의 인스턴스라고 보면 됩니다. 다른 Android 구성요소와 마찬가지로 자체 수명 주기가 있으며 이 수명 주기는 Session 인스턴스가 존재하는 동안 사용된 리소스를 초기화하고 해체하는 데 사용할 수 있습니다. CarAppServiceSession 간에는 일대다 관계가 있습니다. 예를 들어 하나의 CarAppService에는 Session 인스턴스 두 개가 있을 수 있습니다. 하나는 기본 디스플레이용이고 다른 하나는 계기판 디스플레이용이며 이는 계기판 화면을 지원하는 탐색 앱을 위한 것입니다.

Screen

Screen 인스턴스는 호스트 앱에서 렌더링된 사용자 인터페이스를 생성하는 역할을 담당합니다. 이러한 사용자 인터페이스는 각각 특정 유형의 레이아웃(예: 그리드 또는 목록)을 모델링하는 Template 클래스로 표현됩니다. 각 Session은 앱의 다양한 부분을 통한 사용자 플로우를 처리하는 Screen 인스턴스 스택을 관리합니다. Session과 마찬가지로 Screen에도 연결할 수 있는 자체 수명 주기가 있습니다.

자동차 앱 라이브러리의 작동 방식을 보여주는 다이어그램입니다. 왼쪽에는 Display 상자가 두 개 있습니다. 중앙에는 Host 상자가 있습니다. 오른쪽에는 CarAppService 상자가 있습니다. CarAppService 상자 안에는 Session 상자가 두 개 있습니다. 첫 번째 Session 안에는 Screen 상자 세 개가 서로 겹쳐 있습니다. 두 번째 Session 안에는 Screen 상자 두 개가 서로 겹쳐 있습니다. 각 Display와 Host 간, Host와 Session 간에 화살표가 있어 호스트가 다양한 모든 구성요소 간에 통신을 어떻게 관리하는지 알 수 있습니다.

이 Codelab의 CarAppService 작성 섹션에서 CarAppServiceSession, Screen을 작성해 보므로 지금은 잘 몰라도 됩니다.

4. 초기 구성 설정

시작하려면 CarAppService가 포함된 모듈을 설정하고 종속 항목을 선언합니다.

car-app-service 모듈 만들기

  1. Project 창에서 :common 모듈이 선택된 상태에서 마우스 오른쪽 버튼을 클릭하고 New > Module 옵션을 선택합니다.
  2. 모듈 마법사가 열리면 왼쪽 목록에서 Android Library 템플릿(다른 모듈에서 이 모듈을 종속 항목으로 사용할 수 있도록)을 선택한 후 다음 값을 사용합니다.
  • Module name: :common:car-app-service
  • Package name: com.example.places.carappservice
  • Minimum SDK: API 23: Android 6.0 (Marshmallow)

이 단계에 설명된 대로 값을 설정하여 New Module 마법사를 만듭니다.

종속 항목 설정

  1. 프로젝트 수준 build.gradle 파일에서 다음과 같이 자동차 앱 라이브러리 버전의 변수 선언을 추가합니다. 이렇게 하면 앱의 각 모듈에서 동일한 버전을 쉽게 사용할 수 있습니다.

build.gradle(Project: Places)

buildscript {
    ext {
        // All versions can be found at https://developer.android.com/jetpack/androidx/releases/car-app
        car_app_library_version = '1.3.0-rc01'
        ...
    }
}
  1. 이제 :common:car-app-service 모듈의 build.gradle 파일에 종속 항목을 두 개 추가합니다.
  • androidx.car.app:app. 자동차 앱 라이브러리의 기본 아티팩트이며 앱을 빌드할 때 사용되는 핵심 클래스가 모두 포함되어 있습니다. 라이브러리를 구성하는 다른 아티팩트가 세 개 있습니다. Android Auto 관련 기능용 androidx.car.app:app-projected, Android Automotive OS 기능 코드용 androidx.car.app:app-automotive, 단위 테스트에 유용한 도우미용 androidx.car.app:app-testing입니다. Codelab의 후반부에서 app-projectedapp-automotive를 사용합니다.
  • :common:data. 기존 모바일 앱에서 사용하는 동일한 데이터 모듈이며 모든 버전의 앱 환경에서 동일한 데이터 소스를 사용할 수 있도록 합니다.

build.gradle(Module :common:car-app-service)

dependencies {
    ...
    implementation "androidx.car.app:app:$car_app_library_version"
    implementation project(":common:data")
    ...
}

이 변경사항을 적용하면 앱 자체 모듈의 종속 항목 그래프는 다음과 같습니다.

:app 모듈과 :common:car-app-service 모듈은 모두 :common:data 모듈에 종속됩니다.

종속 항목을 설정했으므로 이제 CarAppService를 작성해 보겠습니다.

5. CarAppService 작성

  1. 먼저 :common:car-app-service 모듈 내의 carappservice 패키지에서 PlacesCarAppService.kt라는 파일을 만듭니다.
  2. 이 파일 내에서 CarAppService를 확장하는 PlacesCarAppService라는 클래스를 만듭니다.

PlacesCarAppService.kt

class PlacesCarAppService : CarAppService() {

    override fun createHostValidator(): HostValidator {
        return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
    }

    override fun onCreateSession(): Session {
        // PlacesSession will be an unresolved reference until the next step
        return PlacesSession()
    }
}

CarAppService 추상 클래스는 onBind, onUnbind와 같은 Service 메서드를 구현하고 이러한 메서드의 추가 재정의를 방지하여 호스트 애플리케이션과의 적절한 상호 운용성을 보장합니다. 개발자는 createHostValidatoronCreateSession만 구현하면 됩니다.

createHostValidator에서 반환하는 HostValidator는 호스트를 신뢰할 수 있는지 호스트가 정의된 매개변수와 일치하지 않는 경우 바인딩이 실패하는지 확인하기 위해 CarAppService가 바인딩될 때 참조됩니다. 이 Codelab(및 일반적인 테스트)에서는 ALLOW_ALL_HOSTS_VALIDATOR를 사용하면 앱이 연결되어 있는지 쉽게 확인할 수 있지만 프로덕션에서는 사용하면 안 됩니다. 프로덕션 앱에서 이를 구성하는 방법에 관한 자세한 내용은 createHostValidator 문서를 참고하세요.

이처럼 단순한 앱의 경우 onCreateSessionSession 인스턴스를 간단히 반환할 수 있습니다. 좀 더 복잡한 앱에서는 앱이 차량에서 실행되는 동안 사용되는 장기 지속 리소스(예: 측정항목, 로깅 클라이언트)를 여기에서 초기화하는 것이 좋습니다.

  1. 마지막으로 :common:car-app-service 모듈의 AndroidManifest.xml 파일에서 PlacesCarAppService에 상응하는 <service> 요소를 추가해야 합니다. 그러면 운영체제(및 더 나아가 호스트와 같은 다른 앱)에서 이것의 존재 사실을 알 수 있습니다.

AndroidManifest.xml(:common:car-app-service)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!--
        This AndroidManifest.xml will contain all of the elements that should be shared across the
        Android Auto and Automotive OS versions of the app, such as the CarAppService <service> element
    -->

    <application>
        <service
            android:name="com.example.places.carappservice.PlacesCarAppService"
            android:exported="true">
            <intent-filter>
                <action android:name="androidx.car.app.CarAppService" />
                <category android:name="androidx.car.app.category.POI" />
            </intent-filter>
        </service>
    </application>
</manifest>

여기서는 다음과 같은 두 가지 중요한 사항이 있습니다.

  • <action> 요소를 사용하면 호스트(및 런처) 애플리케이션이 앱을 찾을 수 있습니다.
  • <category> 요소는 앱의 카테고리를 선언하고 이는 앱이 충족해야 하는 앱 품질 기준을 결정합니다(나중에 자세히 설명함). 다른 가능한 값은 androidx.car.app.category.NAVIGATIONandroidx.car.app.category.IOT입니다.

PlacesSession 클래스 만들기

  • PlacesCarAppService.kt 파일을 만들고 다음 코드를 추가합니다.

PlacesCarAppService.kt

class PlacesSession : Session() {
    override fun onCreateScreen(intent: Intent): Screen {
        // MainScreen will be an unresolved reference until the next step
        return MainScreen(carContext)
    }
}

이와 같은 단순한 앱에서는 onCreateScreen에서 기본 화면을 반환하면 됩니다. 하지만 이 메서드는 Intent를 매개변수로 사용하므로 좀 더 기능이 많은 앱은 Intent에서 읽고 화면의 백 스택을 채우거나 다른 조건부 로직을 사용할 수도 있습니다.

MainScreen 클래스 만들기

이제 screen.이라는 새로운 패키지를 만듭니다.

  1. com.example.places.carappservice 패키지를 마우스 오른쪽 버튼으로 클릭하고 New > Package(전체 패키지 이름은 com.example.places.carappservice.screen임)를 선택합니다. 여기에 앱의 Screen 서브클래스를 모두 배치합니다.
  2. screen 패키지에서 MainScreen.kt라는 파일을 만들어 Screen을 확장하는 MainScreen 클래스를 포함합니다. 지금은 PaneTemplate을 사용하여 Hello, world!라는 단순한 메시지만 표시됩니다.

MainScreen.kt

class MainScreen(carContext: CarContext) : Screen(carContext) {
    override fun onGetTemplate(): Template {
        val row = Row.Builder()
            .setTitle("Hello, world!")
            .build()
        
        val pane = Pane.Builder()
            .addRow(row)
            .build()

        return PaneTemplate.Builder(pane)
            .setHeaderAction(Action.APP_ICON)
            .build()
    }
}

6. Android Auto 지원 추가

이제 앱이 실행되도록 하는 데 필요한 로직을 모두 구현했지만 Android Auto에서 앱을 실행하려면 두 가지 구성을 더 설정해야 합니다.

car-app-service 모듈의 종속 항목 추가

:app 모듈의 build.gradle 파일에서 다음을 추가합니다.

build.gradle(모듈 :app)

dependencies {
    ...
    implementation project(path: ':common:car-app-service')
    ...
}

이 변경사항을 적용하면 앱 자체 모듈의 종속 항목 그래프는 다음과 같습니다.

:app 모듈과 :common:car-app-service 모듈은 모두 :common:data 모듈에 종속됩니다. :app 모듈은 :common:car-app-service 모듈에도 종속됩니다.

이는 :common:car-app-service 모듈에서 방금 작성한 코드를 자동차 앱 라이브러리에 포함된 다른 구성요소(예: 제공된 권한 부여 활동)와 함께 번들로 묶습니다.

com.google.android.gms.car.application meta-data 선언

  1. :common:car-app-service 모듈을 마우스 오른쪽 버튼으로 클릭하고 New > Android Resource File 옵션을 선택한 후 다음 값을 재정의합니다.
  • File name: automotive_app_desc.xml
  • Resource type: XML
  • Root element: automotiveApp

이 단계에 설명된 대로 값이 설정된 New Resource File 마법사

  1. 이 파일 내에서 다음 <uses> 요소를 추가하여 앱이 자동차 앱 라이브러리에서 제공한 템플릿을 사용한다고 선언합니다.

automotive_app_desc.xml

<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
    <uses name="template"/>
</automotiveApp>
  1. :app 모듈의 AndroidManifest.xml 파일에서 방금 만든 automotive_app_desc.xml 파일을 참조하는 다음 <meta-data> 요소를 추가합니다.

AndroidManifest.xml(:app)

<application ...>

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

    ...

</application>

이 파일은 Android Auto에서 읽으므로 앱에서 지원하는 기능을 알릴 수 있습니다. 여기서는 자동차 앱 라이브러리의 템플릿 시스템을 사용함을 알릴 수 있습니다. 그러면 이 정보는 Android Auto 런처에 앱을 추가하거나 알림에서 앱을 여는 등의 동작을 처리하는 데 사용됩니다.

선택사항: 프로젝션 변경사항 수신 대기

사용자의 기기가 자동차에 연결되어 있는지 알고 싶을 때가 있습니다. 이럴 때는 CarConnection API를 사용하면 됩니다. 이 API는 연결 상태를 확인할 수 있는 LiveData를 제공합니다.

  1. CarConnection API를 사용하려면 먼저 androidx.car.app:app 아티팩트의 :app 모듈에 종속 항목을 추가합니다.

build.gradle(모듈 :app)

dependencies {
    ...
    implementation "androidx.car.app:app:$car_app_library_version"
    ...
}
  1. 예시용으로 현재 연결 상태를 표시하는 다음과 같은 간단한 Composable을 만들 수 있습니다. 실제 앱에서는 이 상태가 일부 로깅에서 캡처되거나 프로젝션하는 동안 휴대전화 화면의 일부 기능을 사용 중지하는 데 사용될 수도 있습니다.

MainActivity.kt

@Composable
fun ProjectionState(carConnectionType: Int, modifier: Modifier = Modifier) {
    val text = when (carConnectionType) {
        CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not projecting"
        CarConnection.CONNECTION_TYPE_NATIVE -> "Running on Android Automotive OS"
        CarConnection.CONNECTION_TYPE_PROJECTION -> "Projecting"
        else -> "Unknown connection type"
    }

    Text(
        text = text,
        style = MaterialTheme.typography.bodyMedium,
        modifier = modifier
    )
}
  1. 이제 데이터를 표시하고 읽고 Composable에 전달하는 방법이 있습니다(다음 스니펫 참고).

MainActivity.kt

setContent {
    val carConnectionType by CarConnection(this).type.observeAsState(initial = -1)
    PlacesTheme {
        // A surface container using the 'background' color from the theme
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background
        ) {
            Column {
                Text(
                    text = "Places",
                    style = MaterialTheme.typography.displayLarge,
                    modifier = Modifier.padding(8.dp)
                )
                ProjectionState(
                    carConnectionType = carConnectionType,
                    modifier = Modifier.padding(8.dp)
                )
                PlaceList(places = PlacesRepository().getPlaces())
            }
        }
    }
}
  1. 앱을 실행하면 Not projecting이 표시됩니다.

이제 &#39;Not projecting&#39;이라는 프로젝션 상태에 관한 추가적인 텍스트 줄이 화면에 표시됩니다.

7. 데스크톱 헤드 유닛(DHU)으로 테스트

CarAppService를 구현하고 Android Auto 구성을 완료했으므로 이제 앱을 실행하여 어떻게 표시되는지 살펴보겠습니다.

  1. 휴대전화에 앱을 설치하고 안내를 따라 DHU를 설치하고 실행합니다.

DHU가 실행되면 런처에 앱 아이콘이 표시됩니다(표시되지 않으면 이전 섹션의 모든 단계를 따랐는지 다시 한번 확인하고 터미널에서 DHU를 종료했다가 다시 시작합니다).

  1. 런처에서 앱을 엽니다.

Places 앱을 포함하여 앱 그리드를 보여주는 Android Auto 런처

앱이 비정상 종료되었습니다.

&#39;Android Auto에 예상치 못한 오류가 발생했습니다&#39;라는 메시지와 함께 오류 화면이 표시됩니다. 화면 오른쪽 상단에 디버그 전환 버튼이 있습니다.

  1. 앱이 비정상 종료된 이유를 확인하려면 오른쪽 상단의 디버그 아이콘(DHU에서 실행할 때만 표시됨)을 전환하거나 Android 스튜디오의 Logcat을 확인합니다.

이전 그림과 같은 오류 화면이지만 지금은 디버그 전환 버튼이 사용 설정되어 있습니다. 스택 트레이스가 화면에 표시됩니다.

Error: [type: null, cause: null, debug msg: java.lang.IllegalArgumentException: Min API level not declared in manifest (androidx.car.app.minCarApiLevel)
        at androidx.car.app.AppInfo.retrieveMinCarAppApiLevel(AppInfo.java:143)
        at androidx.car.app.AppInfo.create(AppInfo.java:91)
        at androidx.car.app.CarAppService.getAppInfo(CarAppService.java:380)
        at androidx.car.app.CarAppBinder.getAppInfo(CarAppBinder.java:255)
        at androidx.car.app.ICarApp$Stub.onTransact(ICarApp.java:182)
        at android.os.Binder.execTransactInternal(Binder.java:1285)
        at android.os.Binder.execTransact(Binder.java:1244)
]

로그를 보면 앱에서 지원하는 최소 API 수준에 관한 선언이 매니페스트에 누락된 것을 알 수 있습니다. 해당 항목을 추가하기 전에 이 선언이 필요한 이유를 이해하는 것이 좋습니다.

Android 자체와 마찬가지로, 자동차 앱 라이브러리에도 API 수준이라는 개념이 있습니다. 서로 통신하려면 호스트 애플리케이션과 클라이언트 애플리케이션 간에 계약이 있어야 하기 때문입니다. 호스트 애플리케이션은 특정 API 수준과 관련 기능을 지원합니다(이전 버전과의 호환성을 위해 이전 수준의 기능도 지원). 예를 들어 SignInTemplate은 API 수준 2 이상을 실행하는 호스트에서 사용할 수 있습니다. 하지만 API 수준 1만 지원하는 호스트에서 사용하려고 하면 호스트는 템플릿 유형을 알 수 없고 이를 사용하여 유의미한 작업도 할 수 없습니다.

호스트를 클라이언트에 바인딩하는 과정에서 지원되는 API 수준이 일부 중복되어야 바인딩이 성공합니다. 예를 들어 호스트가 API 수준 1만 지원하지만 클라이언트 앱이 API 수준 2의 기능 없이는 실행될 수 없는 경우(이 매니페스트 선언에서 표시됨) 클라이언트가 호스트에서 실행될 수 없으므로 앱은 연결되지 않아야 합니다. 따라서 클라이언트는 필요한 최소 API 수준을 매니페스트에서 선언하여 이를 지원할 수 있는 호스트만 바인딩되도록 해야 합니다.

  1. 지원되는 최소 API 수준을 설정하려면 :common:car-app-service 모듈의 AndroidManfiest.xml 파일에 다음 <meta-data> 요소를 추가합니다.

AndroidManifest.xml(:common:car-app-service)

<application>
    <meta-data
        android:name="androidx.car.app.minCarApiLevel"
        android:value="1" />
    <service android:name="com.example.places.carappservice.PlacesCarAppService" ...>
        ...
    </service>
</application>
  1. 앱을 다시 설치하고 DHU에서 실행하면 다음과 같이 표시됩니다.

기본 &#39;Hello, world&#39; 화면을 보여주는 앱

완전하게 하기 위해 minCarApiLevel을 큰 값(예: 100)으로 설정하여 호스트와 클라이언트가 호환되지 않는 경우 앱을 시작하려고 하면 어떤 일이 발생하는지 확인해 봐도 됩니다(힌트: 값을 설정하지 않은 때와 유사하게 비정상 종료됨).

Android 자체와 마찬가지로 호스트에서 필수 수준을 지원하는지 런타임에 확인하는 경우 선언된 최소 수준보다 높은 API의 기능을 사용할 수 있다는 점에 유의하세요.

선택사항: 프로젝션 변경사항 수신 대기

  • 이전 단계에서 CarConnection 리스너를 추가한 경우 DHU가 실행 중일 때 휴대전화에 상태 업데이트가 표시됩니다(아래 참고).

휴대전화가 DHU에 연결되어 있으므로 프로젝션 상태를 표시하는 텍스트 줄이 이제 &#39;Projecting&#39;으로 표시됩니다.

8. Android Automotive OS 지원 추가

Android Auto가 실행 중이므로 이제 추가로 Android Automotive OS도 지원해 보겠습니다.

:automotive 모듈 만들기

  1. Android Automotive OS 버전의 앱과 관련된 코드를 포함하는 모듈을 만들려면 Android 스튜디오에서 File > New > New Module...을 열고 왼쪽의 템플릿 유형 목록에서 Automotive 옵션을 선택한 후 다음 값을 사용합니다.
  • Application/Library name: Places(기본 앱과 같지만 원하는 경우 다른 이름을 선택해도 됨)
  • Module name: automotive
  • Package name: com.example.places.automotive
  • Language: Kotlin
  • Minimum SDK: API 29: Android 10.0 (Q)(앞서 :common:car-app-service 모듈을 만들 때 언급했듯이 자동차 앱 라이브러리 앱을 지원하는 모든 Android Automotive OS 차량은 API 29 이상을 실행함)

이 단계에서 나열된 값을 보여주는 Android Automotive OS 모듈의 Create New Module 마법사

  1. Next를 클릭하고 다음 화면에서 No Activity를 선택한 후 Finish를 클릭합니다.

Create New Module 마법사의 두 번째 페이지 &#39;No Activity&#39;, &#39;Media Service&#39;, &#39;Messaging Service&#39;라는 세 가지 옵션이 표시됩니다. &#39;No Activity&#39; 옵션이 선택되어 있습니다.

종속 항목 추가

Android Auto와 마찬가지로 :common:car-app-service 모듈의 종속 항목을 선언해야 합니다. 이렇게 하면 두 플랫폼에서 구현을 공유하게 됩니다.

또한 androidx.car.app:app-automotive 아티팩트의 종속 항목을 추가해야 합니다. Android Auto에 선택사항인 androidx.car.app:app-projected 아티팩트와 달리 이 종속 항목은 Android Automotive OS에 필수입니다. 앱을 실행하는 데 사용되는 CarAppActivity를 포함하고 있기 때문입니다.

  1. 종속 항목을 추가하려면 build.gradle 파일을 열고 다음 코드를 삽입합니다.

build.gradle(모듈 :automotive)

dependencies {
    ...
    implementation project(':common:car-app-service')
    implementation "androidx.car.app:app-automotive:$car_app_library_version"
    ...
}

이 변경사항을 적용하면 앱 자체 모듈의 종속 항목 그래프는 다음과 같습니다.

:app 모듈과 :common:car-app-service 모듈은 모두 :common:data 모듈에 종속됩니다. :app 모듈과 :automotive 모듈은 :common:car-app-service 모듈에 종속됩니다.

매니페스트 설정

  1. 먼저 두 가지 기능인 android.hardware.type.automotiveandroid.software.car.templates_host필수인 것으로 선언해야 합니다.

android.hardware.type.automotive는 기기 자체가 차량임을 나타내는 시스템 기능입니다(자세한 내용은 FEATURE_AUTOMOTIVE 참고). 이 기능을 필수로 표시한 앱만 Play Console의 Automotive OS 트랙에 제출할 수 있습니다(다른 트랙에 제출된 앱에는 이 기능이 필수일 수 없음). android.software.car.templates_host는 템플릿 앱을 실행하는 데 필요한 템플릿 호스트를 보유한 차량에만 있는 시스템 기능입니다.

AndroidManifest.xml(:automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-feature
        android:name="android.hardware.type.automotive"
        android:required="true" />
    <uses-feature
        android:name="android.software.car.templates_host"
        android:required="true" />
    ...
</manifest>
  1. 이제 일부 기능을 필수가 아닌 것으로 선언해야 합니다.

이는 Google이 탑재된 자동차에서 사용할 수 있는 다양한 하드웨어와 앱이 호환되도록 하기 위함입니다. 예를 들어 앱에 android.hardware.screen.portrait 기능이 필요한 경우 가로 모드 화면이 있는 차량과는 호환되지 않습니다. 대부분의 차량에서 방향은 고정되어 있기 때문입니다. 따라서 이러한 기능의 경우 android:required 속성이 false로 설정됩니다.

AndroidManifest.xml(:automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
    <uses-feature
        android:name="android.hardware.wifi"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.screen.portrait"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.screen.landscape"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />
    ...
</manifest>
  1. 이제 Android Auto와 마찬가지로 automotive_app_desc.xml 파일 참조를 추가해야 합니다.

이번에는 android:name 속성이 이전과 달리 com.google.android.gms.car.application이 아닌 com.android.automotive입니다. 이전과 마찬가지로 이는 :common:car-app-service 모듈의 automotive_app_desc.xml 파일을 참조합니다. 즉, Android Auto와 Android Automotive OS에서 모두 동일한 리소스가 사용됩니다. <meta-data> 요소는 <application> 요소 내에 있습니다(따라서 자동으로 닫히지 않도록 application 태그를 변경해야 함).

AndroidManifest.xml(:automotive)

<application>
    ...
    <meta-data android:name="com.android.automotive"
        android:resource="@xml/automotive_app_desc"/>
    ...
</application>
  1. 이제 라이브러리에 포함된 CarAppActivity<activity> 요소를 추가해야 합니다.

AndroidManifest.xml(:automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
    <application ...>
        ...
        <activity
            android:name="androidx.car.app.activity.CarAppActivity"
            android:exported="true"
            android:launchMode="singleTask"
            android:theme="@android:style/Theme.DeviceDefault.NoActionBar">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="distractionOptimized"
                android:value="true" />
        </activity>
    </application>
</manifest>

다음은 각각 실행하는 작업입니다.

  • android:nameapp-automotive 패키지에 있는 CarAppActivity 클래스의 정규화된 클래스 이름을 나열합니다.
  • android:exportedtrue로 설정됩니다. 이 Activity를 그 자체가 아닌 다른 앱(런처)에서 실행할 수 있어야 하기 때문입니다.
  • android:launchModesingleTask로 설정되므로 한 번에 CarAppActivity 인스턴스가 하나만 있을 수 있습니다.
  • android:theme@android:style/Theme.DeviceDefault.NoActionBar로 설정되므로 앱이 사용할 수 있는 전체 화면 공간을 차지할 수 있습니다.
  • 인텐트 필터는 이것이 앱의 런처 Activity임을 나타냅니다.
  • 차량이 움직이는 경우와 같이 UX 제한이 있는 동안 앱을 사용할 수 있음을 시스템에 나타내는 <meta-data> 요소가 있습니다.

선택사항: :app 모듈에서 런처 아이콘 복사

방금 :automotive 모듈을 만들었으므로 기본 녹색 Android 로고 아이콘이 있습니다.

  • 원하는 경우 :app 모듈에서 mipmap 리소스 디렉터리를 복사하여 :automotive 모듈로 붙여넣어 모바일 앱과 동일한 런처 아이콘을 사용합니다.

9. Android Automotive OS 에뮬레이터를 사용하여 테스트

Play 스토어 시스템 이미지로 Automotive 설치

  1. 먼저 Android 스튜디오에서 SDK Manager를 열고, 아직 선택하지 않았다면 SDK Platforms 탭을 선택합니다. SDK Manager 창의 오른쪽 하단에서 Show package details 체크박스가 선택되어 있는지 확인합니다.
  2. 다음 에뮬레이터 이미지를 하나 이상 설치합니다. 이미지는 자신과 동일한 아키텍처(x86/ARM)를 사용하는 컴퓨터에서만 실행됩니다.
  • Android 12L > Play 스토어 Intel x86 Atom_64 시스템 이미지를 사용한 Automotive
  • Android 12L > Play 스토어 ARM 64 v8a 시스템 이미지를 사용한 Automotive
  • Android 11 > Play 스토어 Intel x86 Atom_64 시스템 이미지를 사용한 Automotive
  • Android 10 > Play 스토어 Intel x86 Atom_64 시스템 이미지를 사용한 Automotive

Android Automotive OS Android Virtual Device 만들기

  1. 기기 관리도구를 열고 창 왼쪽의 Category 열에서 Automotive를 선택합니다. 그런 다음 목록에서 Automotive(1024p landscape) 기기 정의를 선택하고 Next를 클릭합니다.

Virtual Device Configuration 마법사에서 &#39;Automotive(1024p landscape)&#39; 하드웨어 프로필이 선택되어 있습니다.

  1. 다음 페이지에서 이전 단계의 시스템 이미지를 선택합니다(Android 11/API 30 이미지를 선택한 경우 기본 Recommended 탭이 아닌 x86 Images 탭에 있을 수 있음). Next를 클릭하고 원하는 고급 옵션을 선택한 다음 Finish를 클릭하여 AVD를 만듭니다.

앱 실행

  1. automotive 실행 구성을 사용하여 방금 만든 에뮬레이터에서 앱을 실행합니다.

실행

앱을 처음 실행하면 다음과 같은 화면이 표시될 수 있습니다.

앱에 &#39;System update required&#39;라는 화면과 그 아래에 &#39;Check for updates&#39;라는 버튼이 표시됩니다.

이 경우 Check for updates 버튼을 클릭하면 Google Automotive App Host 앱의 Play 스토어 페이지로 이동하며 여기서 Install 버튼을 클릭해야 합니다. 로그인하지 않은 상태에서 Check for updates 버튼을 클릭하면 로그인 흐름을 거치게 됩니다. 로그인한 후에는 앱을 다시 열어 버튼을 클릭하고 Play 스토어 페이지로 다시 이동하면 됩니다.

Google Automotive App Host Play 스토어 페이지의 오른쪽 상단에 &#39;Install&#39; 버튼이 있습니다.

  1. 마지막으로 호스트가 설치된 상태에서 런처(하단 행의 점 9개 그리드 아이콘)에서 앱을 다시 열면 다음과 같이 표시됩니다.

기본 &#39;Hello, world&#39; 화면을 보여주는 앱

다음 단계에서는 :common:car-app-service 모듈을 변경하여 장소 목록을 표시하고 사용자가 다른 앱에서 선택된 위치로 탐색을 시작할 수 있도록 합니다.

10. 지도 및 세부정보 화면 추가

기본 화면에 지도 추가

  1. 시작하려면 MainScreen 클래스의 onGetTemplate 메서드에 있는 코드를 다음 코드로 바꿉니다.

MainScreen.kt

override fun onGetTemplate(): Template {
    val placesRepository = PlacesRepository()
    val itemListBuilder = ItemList.Builder()
        .setNoItemsMessage("No places to show")

    placesRepository.getPlaces()
        .forEach {
            itemListBuilder.addItem(
                Row.Builder()
                    .setTitle(it.name)
                    // Each item in the list *must* have a DistanceSpan applied to either the title
                    // or one of the its lines of text (to help drivers make decisions)
                    .addText(SpannableString(" ").apply {
                        setSpan(
                            DistanceSpan.create(
                                Distance.create(Math.random() * 100, Distance.UNIT_KILOMETERS)
                            ), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE
                        )
                    })
                    .setOnClickListener { TODO() }
                    // Setting Metadata is optional, but is required to automatically show the
                    // item's location on the provided map
                    .setMetadata(
                        Metadata.Builder()
                            .setPlace(Place.Builder(CarLocation.create(it.latitude, it.longitude))
                                // Using the default PlaceMarker indicates that the host should
                                // decide how to style the pins it shows on the map/in the list
                                .setMarker(PlaceMarker.Builder().build())
                                .build())
                            .build()
                    ).build()
            )
        }

    return PlaceListMapTemplate.Builder()
        .setTitle("Places")
        .setItemList(itemListBuilder.build())
        .build()
}

이 코드는 PlacesRepository에서 Place 인스턴스 목록을 읽고 PlaceListMapTemplate에서 표시한 ItemList에 추가할 Row로 각 인스턴스를 변환합니다.

  1. 앱을 다시 실행하여(각 플랫폼 또는 두 플랫폼에서 모두) 결과를 확인합니다.

Android Auto

Android Automotive OS

오류로 인해 다른 스택 트레이스가 표시됩니다.

앱이 비정상 종료되고 사용자는 앱을 연 후 다시 런처로 이동합니다.

또 다른 오류가 발생했습니다. 권한이 누락된 것 같습니다.

java.lang.SecurityException: The car app does not have a required permission: androidx.car.app.MAP_TEMPLATES
        at android.os.Parcel.createExceptionOrNull(Parcel.java:2373)
        at android.os.Parcel.createException(Parcel.java:2357)
        at android.os.Parcel.readException(Parcel.java:2340)
        at android.os.Parcel.readException(Parcel.java:2282)
        ...
  1. 오류를 수정하려면 :common:car-app-service 모듈의 매니페스트에 다음 <uses-permission> 요소를 추가합니다.

이 권한은 PlaceListMapTemplate을 사용하는 앱에서 선언해야 하며 선언하지 않으면 방금과 같이 앱이 비정상 종료됩니다. androidx.car.app.category.POI카테고리를 선언하는 앱만 이 템플릿을 사용할 수 있으며 결과적으로 이 권한도 사용할 수 있습니다.

AndroidManifest.xml(:common:car-app-service)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
    ...
</manifest>

권한을 추가한 후 앱을 실행하면 각 플랫폼에서 다음과 같이 표시됩니다.

Android Auto

Android Automotive OS

화면 왼쪽에 위치 목록이 표시되고 위치에 상응하는 핀이 있는 지도가 그 뒤에 표시되어 나머지 화면을 채웁니다.

화면 왼쪽에 위치 목록이 표시되고 위치에 상응하는 핀이 있는 지도가 그 뒤에 표시되어 나머지 화면을 채웁니다.

지도 렌더링은 필요한 Metadata를 제공하면 애플리케이션 호스트에서 처리됩니다.

세부정보 화면 추가

이제 세부정보 화면을 추가하여 사용자가 특정 위치에 관한 자세한 정보를 확인하고 선호하는 내비게이션 앱을 사용해 해당 위치로 이동하거나 다른 장소 목록으로 돌아갈 수 있도록 해 보겠습니다. 이 작업은 선택적 작업 버튼 옆에 최대 4행의 정보를 표시할 수 있는 PaneTemplate을 사용하면 됩니다.

  1. 먼저 :common:car-app-service 모듈의 res 디렉터리를 마우스 오른쪽 버튼으로 클릭하고 New > Vector Asset을 클릭한 후 다음 구성을 사용하여 탐색 아이콘을 만듭니다.
  • Asset type: Clip art
  • Clip art: navigation
  • Name: baseline_navigation_24
  • Size: 24dp x 24dp
  • Color: #000000
  • Opacity: 100%

이 단계에서 언급된 입력을 보여주는 Asset Studio 마법사

  1. 그런 다음 screen 패키지에서 DetailScreen.kt라는 파일을 만들고(기존 MainScreen.kt 파일 옆에) 다음 코드를 추가합니다.

DetailScreen.kt

class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {

    override fun onGetTemplate(): Template {
        val place = PlacesRepository().getPlace(placeId)
            ?: return MessageTemplate.Builder("Place not found")
                .setHeaderAction(Action.BACK)
                .build()

        val navigateAction = Action.Builder()
            .setTitle("Navigate")
            .setIcon(
                CarIcon.Builder(
                    IconCompat.createWithResource(
                        carContext,
                        R.drawable.baseline_navigation_24
                    )
                ).build()
            )
            // Only certain intent actions are supported by `startCarApp`. Check its documentation
            // for all of the details. To open another app that can handle navigating to a location
            // you must use the CarContext.ACTION_NAVIGATE action and not Intent.ACTION_VIEW like
            // you might on a phone.
            .setOnClickListener {  carContext.startCarApp(place.toIntent(CarContext.ACTION_NAVIGATE)) }
            .build()

        return PaneTemplate.Builder(
            Pane.Builder()
                .addAction(navigateAction)
                .addRow(
                    Row.Builder()
                        .setTitle("Coordinates")
                        .addText("${place.latitude}, ${place.longitude}")
                        .build()
                ).addRow(
                    Row.Builder()
                        .setTitle("Description")
                        .addText(place.description)
                        .build()
                ).build()
        )
            .setTitle(place.name)
            .setHeaderAction(Action.BACK)
            .build()
    }
}

navigateAction이 빌드되는 방식에 특히 주의하세요. OnClickListener에서 startCarApp을 호출하는 것이 Android Auto 및 Android Automotive OS에서 다른 앱과 상호작용하는 데 가장 중요합니다.

이제 두 가지 유형의 화면이 있으므로 화면 간에 탐색을 추가해 보겠습니다. 자동차 앱 라이브러리의 탐색은 운전 중 완료하기에 적합한 간단한 작업 흐름에 이상적인 푸시 및 팝의 스택 모델을 사용합니다.

인앱 탐색이 자동차 앱 라이브러리에서 작동하는 방식을 표현하는 다이어그램 왼쪽에는 MainScreen만 있는 스택이 있습니다. 이 스택과 중앙 스택 사이에는 &#39;Push DetailScreen&#39; 라벨이 지정된 화살표가 있습니다. 중앙 스택에는 기존 MainScreen 위에 DetailScreen이 있습니다. 중앙 스택과 오른쪽 스택 사이에는 &#39;Pop&#39; 라벨이 지정된 화살표가 있습니다. 오른쪽 스택은 왼쪽 스택과 동일하게 MainScreen만 있습니다.

  1. MainScreen의 목록 항목 중 하나에서 해당 항목의 DetailScreen으로 이동하려면 다음 코드를 추가합니다.

MainScreen.kt

Row.Builder()
    ...
    .setOnClickListener { screenManager.push(DetailScreen(carContext, it.id)) }
    ...

DetailScreen에서 MainScreen으로 다시 이동하는 것은 이미 처리되었습니다. DetailScreen에 표시된 PaneTemplate을 빌드할 때 setHeaderAction(Action.BACK)이 호출되기 때문입니다. 헤더 작업을 사용자가 클릭하면 호스트는 스택에서 현재 화면을 삭제하는 작업을 처리하지만 이 동작은 원하는 경우 앱에서 재정의할 수 있습니다.

  1. 이제 앱을 실행하면 DetailScreen과 인앱 탐색이 작동합니다.

11. 화면의 콘텐츠 업데이트

개발자는 사용자가 화면과 상호작용하고 화면의 요소 상태를 변경할 수 있도록 하고 싶은 경우가 많습니다. 이렇게 하는 방법을 보여주기 위해 사용자가 DetailScreen에서 장소를 즐겨찾기에 추가하거나 즐겨찾기에서 해제하는 것을 전환할 수 있는 기능을 빌드합니다.

  1. 먼저 상태를 보유하는 로컬 변수 isFavorite을 추가합니다. 실제 앱에서는 데이터 레이어의 일부로 저장해야 하지만 예시용으로는 로컬 변수로도 충분합니다.

DetailScreen.kt

class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {
    private var isFavorite = false
    ...
}
  1. 이제 :common:car-app-service 모듈의 res 디렉터리를 마우스 오른쪽 버튼으로 클릭하고 New > Vector Asset을 클릭한 후 다음 구성을 사용하여 즐겨찾기 아이콘을 만듭니다.
  • Asset type: Clip art
  • Name: baseline_favorite_24
  • Clip art: favorite
  • Size: 24dp x 24dp
  • Color: #000000
  • Opacity: 100%

이 단계에서 언급된 입력을 보여주는 Asset Studio 마법사

  1. 그런 다음, DetailsScreen.kt에서 PaneTemplateActionStrip을 만듭니다.

ActionStrip UI 구성요소는 제목 반대편의 헤더 행에 배치되며 2차 및 3차 작업에 적합합니다. 탐색은 DetailScreen에서 실행될 기본 작업이므로 즐겨찾기 추가 또는 즐겨찾기 해제를 위한 ActionActionStrip에 배치하는 것이 화면을 구조화하는 좋은 방법입니다.

DetailScreen.kt

val navigateAction = ...

val actionStrip = ActionStrip.Builder()
    .addAction(
        Action.Builder()
            .setIcon(
                CarIcon.Builder(
                    IconCompat.createWithResource(
                        carContext,
                        R.drawable.baseline_favorite_24
                    )
                ).setTint(
                    if (isFavorite) CarColor.RED else CarColor.createCustom(
                        Color.LTGRAY,
                        Color.DKGRAY
                    )
                ).build()
            )
            .setOnClickListener {
                isFavorite = !isFavorite
            }.build()
    )
    .build()

...

두 가지 사항에 유의해야 합니다.

  • CarIcon은 항목의 상태에 따라 색조가 조정됩니다.
  • setOnClickListener는 사용자의 입력에 반응하여 즐겨찾기 상태를 전환하는 데 사용됩니다.
  1. PaneTemplate.Builder에서 setActionStrip을 호출해야 이를 사용할 수 있습니다.

DetailScreen.kt

return PaneTemplate.Builder(...)
    ...
    .setActionStrip(actionStrip)
    .build()
  1. 이제 앱을 실행하여 어떤 일이 일어나는지 확인합니다.

DetailScreen이 표시됩니다. 사용자가 즐겨찾기 아이콘을 탭하지만 예상대로 색상이 변경되지 않습니다.

클릭은 발생하지만 UI는 업데이트되지 않는 것 같습니다.

이는 자동차 앱 라이브러리에 새로고침이라는 개념이 있기 때문입니다. 운전자 주의 분산 행동을 제한하기 위해 화면의 콘텐츠를 새로고침하는 데는 특정 제한사항(표시되는 템플릿에 따라 다름)이 있고 각 새로고침은 Screen 클래스의 invalidate 메서드를 호출하여 자체 코드를 통해 명시적으로 요청해야 합니다. onGetTemplate에서 참조되는 일부 상태를 업데이트하는 것으로는 UI를 업데이트하기에 충분하지 않습니다.

  1. 이 문제를 수정하려면 OnClickListener를 다음과 같이 업데이트합니다.

DetailScreen.kt

.setOnClickListener {
    isFavorite = !isFavorite
    // Request that `onGetTemplate` be called again so that updates to the
    // screen's state can be picked up
    invalidate()
}
  1. 앱을 다시 실행하여 하트 아이콘의 색상이 클릭할 때마다 업데이트되는지 확인합니다.

DetailScreen이 표시됩니다. 사용자가 즐겨찾기 아이콘을 탭하면 이제 예상대로 색상이 변경됩니다.

이것으로 Android Auto 및 Android Automotive OS와 모두 잘 통합된 기본 앱을 완성했습니다.

12. 축하합니다

첫 번째 자동차 앱 라이브러리 앱을 성공적으로 빌드했습니다. 이제 배운 내용을 자체 앱에 적용해 보세요.

앞서 언급했듯이 현재 자동차 앱 라이브러리 앱을 사용하여 빌드된 특정 카테고리만 Play 스토어에 제출할 수 있습니다. 앱이 탐색 앱이거나 관심 장소(POI) 앱(예: 이 Codelab에서 작업한 앱), 사물 인터넷(IOT) 앱이라면 지금 빌드를 시작하여 두 플랫폼에서 모두 프로덕션까지 앱을 출시할 수 있습니다.

새 앱 카테고리가 매년 추가되고 있으므로 배운 내용을 바로 적용할 수 없더라도 나중에 다시 확인하면 그때는 앱을 자동차에 확장할 수도 있습니다.

시도해 볼 만한 작업

  • OEM의 에뮬레이터(예: Polestar 2 에뮬레이터)를 설치하고 Android Automotive OS에서 자동차 앱 라이브러리 앱의 디자인과 분위기가 OEM 맞춤설정으로 어떻게 변하는지 확인합니다. 일부 OEM 에뮬레이터는 자동차 앱 라이브러리 앱을 지원하지 않습니다.
  • 자동차 앱 라이브러리의 전체 기능을 보여주는 Showcase 샘플 애플리케이션을 확인합니다.

추가 자료

참조 문서