1. 시작하기 전에
이 Codelab의 의도하지 않은 용도
- Android Auto 및 Android Automotive OS용 미디어(오디오 - 예: 음악, 라디오, 팟캐스트) 앱을 만드는 방법에 관한 가이드. 이러한 앱을 빌드하는 방법에 관한 자세한 내용은 자동차용 미디어 앱 빌드를 참고하세요.
필요한 항목
- Android 스튜디오 미리보기. 이 Codelab에서 사용하는 일부 Android Automotive OS 에뮬레이터는 Android 스튜디오 미리보기를 통해서만 사용할 수 있습니다. 아직 Android 스튜디오 미리보기를 설치하지 않은 경우, 미리보기 버전을 다운로드하는 동안 안정화 버전으로 Codelab을 시작할 수 있습니다.
- 기본적인 Kotlin 사용 경험.
- Android Virtual Device를 생성하고 Android Emulator에서 실행한 경험.
- Jetpack Compose에 관한 기본 지식.
- 부작용에 대한 이해.
- 창 인셋에 관한 기본 지식.
빌드할 항목
이 Codelab에서는 기존 동영상 스트리밍 모바일 앱인 Road Reels를 Android Automotive OS로 이전하는 방법을 알아봅니다.
휴대전화에서 실행 중인 앱의 시작 버전 | 디스플레이 컷아웃이 있는 Android Automotive OS 에뮬레이터에서 실행 중인 앱의 완성된 버전 |
학습할 내용
- Android Automotive OS 에뮬레이터 사용 방법
- Android Automotive OS 빌드를 만드는 데 필요한 변경사항을 적용하는 방법
- 모바일용 앱을 개발할 때 Android Automotive OS에서 실행 시 문제가 발생할 수 있는 지점에 관한 일반적인 가정
- 자동차 앱의 다양한 품질 등급
- 미디어 세션을 사용하여 다른 앱으로 앱의 재생을 제어할 수 있도록 사용 설정하는 방법
- Android Automotive OS 기기와 모바일 기기의 시스템 UI 및 창 인셋 차이점
2. 설정
코드 가져오기
- 이 Codelab의 코드는
car-codelabs
GitHub 저장소의build-a-parked-app
디렉터리에서 찾을 수 있습니다. 코드를 클론하려면 다음 명령어를 실행합니다.
git clone https://github.com/android/car-codelabs.git
- 또는 저장소를 ZIP 파일로 다운로드할 수도 있습니다.
프로젝트 열기
- Android 스튜디오를 시작한 후
build-a-parked-app/start
디렉터리만 선택하여 프로젝트를 가져옵니다.build-a-parked-app/end
디렉터리에는 솔루션 코드가 포함되어 있습니다. 솔루션 코드는 도움이 필요한 경우 또는 전체 프로젝트를 살펴보고 싶을 때 언제든지 참조할 수 있습니다.
코드 숙지하기
- Android 스튜디오에서 프로젝트를 열고 시작 코드를 살펴봅니다.
3. Android Automotive OS용 주차 앱 알아보기
주차 앱은 Android Automotive OS에서 지원하는 앱 카테고리의 하위 집합을 구성합니다. 주차 앱은 본 Codelab 작성 시점을 기준으로 동영상 스트리밍 앱, 웹브라우저, 게임으로 구성되어 있습니다. 이들 앱은 Google이 내장된 차량에 사용되는 하드웨어와 전기자동차의 높아진 보급률이라는 조건에 매우 적합합니다. 전기자동차의 충전 시간은 운전자와 승객이 이러한 유형의 앱을 활용할 좋은 기회가 됩니다.
자동차는 여러 가치 측면에서 태블릿이나 폴더블과 같은 다른 대형 화면 기기와 유사합니다. 자동차의 터치스크린은 태블릿 및 폴더블과 크기, 해상도, 가로세로 비율이 비슷하며 세로 또는 가로 방향일 수 있습니다(단, 태블릿과 달리 방향은 고정되어 있음). 자동차는 네트워크에 연결되거나 연결이 끊어질 수 있는 연결된 기기이기도 합니다. 이러한 모든 점을 고려했을 때, 대형 화면에 최적화된 앱에 적은 양의 변경사항만 적용하면 자동차에서 우수한 사용자 경험을 제공할 수 있습니다.
자동차용 앱에도 대형 화면과 마찬가지로 앱 품질 등급이 있습니다.
- 3등급 - 자동차 지원: 앱이 대형 화면과 호환되며 자동차가 주차되어 있는 동안 사용할 수 있습니다. 자동차에 최적화된 기능은 없지만 사용자는 다른 대형 화면 Android 기기에서와 마찬가지로 앱을 경험할 수 있습니다. 이러한 요구사항을 충족하는 모바일 앱은 자동차 지원 모바일 앱 프로그램을 통해 있는 그대로 자동차에 배포할 수 있습니다.
- 2등급 - 자동차 최적화: 앱이 자동차의 센터 스택 디스플레이에서 우수한 경험을 제공합니다. 이를 위해 앱의 카테고리에 따라 운전 모드 또는 주차 모드에서 사용할 수 있는 기능을 포함하도록 앱에 자동차 전용 엔지니어링이 적용됩니다.
- 1등급 - 자동차 차별화: 앱이 자동차의 다양한 하드웨어에서 작동하도록 빌드되었으며 운전 모드와 주차 모드에 맞게 환경을 조정할 수 있습니다. 1등급은 자동차의 여러 화면(중앙 콘솔, 계기판, 여러 프리미엄 자동차에서 볼 수 있는 파노라마 디스플레이와 같은 추가 화면)에 맞게 설계된 최상의 사용자 경험을 제공합니다.
4. Android Automotive OS 에뮬레이터에서 앱 실행
Play 스토어 시스템 이미지로 Automotive 설치
- 먼저 Android 스튜디오에서 SDK Manager를 열고, 아직 선택하지 않았다면 SDK Platforms 탭을 선택합니다. SDK Manager 창의 오른쪽 하단에서 Show package details 체크박스가 선택되어 있는지 확인합니다.
- Add generic system images에 나열된 Automotive with Play Store 에뮬레이터 이미지 중 하나를 설치합니다. 이미지는 자신과 동일한 아키텍처(x86/ARM)를 사용하는 컴퓨터에서만 실행됩니다.
Android Automotive OS Android Virtual Device 만들기
- 기기 관리도구를 열고 창 왼쪽의 Category 열에서 Automotive를 선택합니다. 그런 다음 목록에서 Automotive (1024p landscape) 번들 하드웨어 프로필을 선택하고 Next를 클릭합니다.
- 다음 페이지에서 이전 단계의 시스템 이미지를 선택합니다. Next를 클릭하고 원하는 고급 옵션을 선택한 다음 Finish를 클릭하여 AVD를 만듭니다. 참고: API 30 이미지를 선택한 경우 Recommended 탭이 아닌 다른 탭에 있을 수 있습니다.
앱 실행
기존 app
실행 구성을 사용하여 방금 만든 에뮬레이터에서 앱을 실행합니다. 앱의 여러 화면을 살펴보고 앱의 동작을 휴대전화 또는 태블릿 에뮬레이터에서 앱을 실행하는 경우와 비교해 봅니다.
5. Android Automotive OS 빌드 만들기
앱이 일단 '작동'하긴 하지만, Android Automotive OS에서 매끄럽게 작동하고 Play 스토어에 게시하기 위한 요구사항을 충족하려면 몇 가지 사소한 변경사항을 적용해야 합니다. 이러한 변경사항 중 일부는 앱의 모바일 버전에 포함하는 것이 적절하지 않으므로 먼저 Android Automotive OS 빌드 변형을 만듭니다.
폼 팩터 버전 차원 추가
먼저 build.gradle.kts
파일에서 flavorDimensions
를 수정하여 빌드가 타겟팅하는 폼 팩터의 버전 차원을 추가합니다. 그런 다음 각 폼 팩터(mobile
및 automotive
)에 productFlavors
블록과 버전을 추가합니다.
자세한 내용은 제품 버전 구성을 참고하세요.
build.gradle.kts (Module :app)
android {
...
flavorDimensions += "formFactor"
productFlavors {
create("mobile") {
// Inform Android Studio to use this flavor as the default (e.g. in the Build Variants tool window)
isDefault = true
// Since there is only one flavor dimension, this is optional
dimension = "formFactor"
}
create("automotive") {
// Since there is only one flavor dimension, this is optional
dimension = "formFactor"
// Adding a suffix makes it easier to differentiate builds (e.g. in the Play Console)
versionNameSuffix = "-automotive"
}
}
...
}
build.gradle.kts
파일을 업데이트하면 파일 상단 배너에 'Gradle files have changed since last project sync. A project sync may be necessary for the IDE to work properly"라는 메시지가 표시됩니다. Android 스튜디오가 이러한 빌드 구성 변경사항을 가져올 수 있도록 배너에서 Sync Now 버튼을 클릭합니다.
다음으로, Build > Select Build Variant... 메뉴 항목에서 Build Variants 도구 창을 열고 automotiveDebug
변형을 선택합니다. 이렇게 하면 Project 창에 automotive
소스 세트의 파일이 표시되고 Android 스튜디오를 통해 앱을 실행할 때 이 빌드 변형이 사용됩니다.
Android Automotive OS 매니페스트 만들기
다음으로, automotive
소스 세트의 AndroidManifest.xml
파일을 만듭니다. 이 파일은 Android Automotive OS 앱에 필요한 필수 요소를 포함합니다.
- Project 창에서
app
모듈을 마우스 오른쪽 버튼으로 클릭합니다. 표시되는 드롭다운에서 New > Other > Android Manifest File을 선택합니다. - New Android Component 창이 열리면 새 파일의 Target Source Set로
automotive
를 선택합니다. Finish를 클릭하여 파일을 만듭니다.
- 방금 만든
AndroidManifest.xml
파일의app/src/automotive/AndroidManifest.xml
경로 아래에 다음을 추가합니다.
AndroidManifest.xml (automotive)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- https://developer.android.com/training/cars/parked#required-features -->
<uses-feature
android:name="android.hardware.type.automotive"
android:required="true" />
<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" />
</manifest>
첫 번째 선언은 Play Console의 Android Automotive OS 트랙에 빌드 아티팩트를 업로드하는 데 필요합니다. 이 기능은 Google Play가 android.hardware.type.automotive
기능이 있는 기기(자동차)에만 앱을 배포하는 데 사용됩니다.
나머지 선언은 앱이 자동차에 있는 여러 하드웨어 구성에 설치되도록 하는 데 필요합니다. 자세한 내용은 Android Automotive OS 필수 기능을 참고하세요.
앱을 동영상 앱으로 표시
마지막으로 추가해야 하는 메타데이터는 automotive_app_desc.xml
파일입니다. 이 파일은 자동차용 Android 컨텍스트에서 앱의 카테고리를 선언하는 데 사용되며 Play Console에서 앱에 대해 선택한 카테고리와는 무관합니다.
app
모듈을 마우스 오른쪽 버튼으로 클릭하고 New > Android Resource File 옵션을 선택한 후 다음 값을 입력하고 OK를 클릭합니다.
- File name:
automotive_app_desc.xml
- Resource type:
XML
- Root element:
automotiveApp
- Source set:
automotive
- Directory name:
xml
- 이 파일에 다음
<uses>
요소를 추가하여 앱이 동영상 앱임을 선언합니다.
automotive_app_desc.xml
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses
name="video"
tools:ignore="InvalidUsesTagAttribute" />
</automotiveApp>
automotive
소스 세트의AndroidManifest.xml
파일(방금<uses-feature>
요소를 추가한 파일)에 빈<application>
요소를 추가합니다. 파일에 방금 만든automotive_app_desc.xml
파일을 참조하는 다음<meta-data>
요소를 추가합니다.
AndroidManifest.xml (automotive)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
...
<application>
<meta-data
android:name="com.android.automotive"
android:resource="@xml/automotive_app_desc" />
</application>
</manifest>
이렇게 해서 앱의 Android Automotive OS 빌드를 만드는 데 필요한 모든 변경사항을 적용했습니다.
6. Android Automotive OS 품질 요구사항 충족: 탐색 용이성
Android Automotive OS 빌드 변형을 만드는 것은 앱을 자동차에 적용하기 위한 작업의 일부분이긴 하나, 앱이 사용 가능하고 안전하게 사용할 수 있는지도 확인해야 합니다.
탐색 어포던스 추가
Android Automotive OS 에뮬레이터에서 앱을 실행하는 동안 세부정보 화면에서 기본 화면으로 또는 플레이어 화면에서 세부정보 화면으로 돌아갈 수 없다는 사실을 알 수 있었을 것입니다. 뒤로 탐색을 사용 설정하려면 뒤로 버튼이나 터치 동작이 필요한 다른 폼 팩터와 달리 Android Automotive OS 기기에는 이러한 요구사항이 없습니다. 따라서 앱은 사용자가 앱의 특정 화면에 강제로 머물러 있지 않고 자유롭게 탐색할 수 있도록 UI에 탐색 어포던스를 제공해야 합니다. 이 요구사항은 AN-1 품질 가이드라인으로 규정되어 있습니다.
세부정보 화면에서 기본 화면으로의 뒤로 탐색을 지원하려면 다음과 같이 세부정보 화면의 CenterAlignedTopAppBar
에 대한 navigationIcon
매개변수를 추가합니다.
RoadReelsApp.kt
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
...
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null
)
}
}
플레이어 화면에서 기본 화면으로의 뒤로 탐색을 지원하려면 다음 단계를 따르세요.
onClose
라는 콜백 매개변수를 받고 클릭되었을 때 이를 호출하는IconButton
이 추가되도록TopControls
컴포저블을 업데이트합니다.
PlayerControls.kt
@Composable
fun TopControls(
title: String?,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Box(modifier) {
IconButton(
modifier = Modifier
.align(Alignment.TopStart),
onClick = onClose
) {
Icon(
Icons.TwoTone.Close,
contentDescription = "Close player",
tint = Color.White
)
}
if (title != null) { ... }
}
}
onClose
콜백 매개변수를 받아서TopControls
에 전달하도록PlayerControls
컴포저블을 업데이트합니다.
PlayerControls.kt
fun PlayerControls(
visible: Boolean,
playerState: PlayerState,
onClose: () -> Unit,
onPlayPause: () -> Unit,
onSeek: (seekToMillis: Long) -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
Box(modifier = modifier.background(Color.Black.copy(alpha = .5f))) {
TopControls(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.screen_edge_padding))
.align(Alignment.TopCenter),
title = playerState.mediaMetadata.title?.toString(),
onClose = onClose
)
...
}
}
}
- 다음으로, 동일한 매개변수를 받아서
PlayerControls
에 전달하도록PlayerScreen
컴포저블을 업데이트합니다.
PlayerScreen.kt
@Composable
fun PlayerScreen(
onClose: () -> Unit,
modifier: Modifier = Modifier,
) {
...
PlayerControls(
modifier = Modifier
.fillMaxSize(),
visible = isShowingControls,
playerState = playerState,
onClose = onClose,
onPlayPause = { if (playerState.isPlaying) player.pause() else player.play() },
onSeek = { player.seekTo(it) }
)
}
- 마지막으로,
RoadReelsNavHost
에PlayerScreen
으로 전달되는 구현을 제공합니다.
RoadReelsNavHost.kt
composable(route = Screen.Player.name) {
PlayerScreen(onClose = { navController.popBackStack() })
}
이제 사용자가 막힘 없이 화면 간에 이동할 수 있습니다. 다른 폼 팩터에서의 사용자 경험도 향상될 수 있습니다. 예를 들어, 세로 방향으로 긴 휴대전화에서 사용자의 손이 이미 화면 상단 근처에 있을 때 손안에서 기기를 움직이지 않고 앱을 더 쉽게 탐색할 수 있습니다.
화면 방향에 따라 조정
대부분의 자동차는 대다수의 모바일 기기와 달리 방향이 고정되어 있습니다. 즉, 화면을 회전할 수 없기 때문에 가로 모드와 세로 모드 중 하나를 지원하지만 둘 다 지원하지는 않습니다. 따라서 앱은 양쪽 방향이 모두 지원된다고 가정해서는 안 됩니다.
Android Automotive OS 매니페스트 만들기에서 required
속성을 false
로 설정하여 android.hardware.screen.portrait
및 android.hardware.screen.landscape
기능에 대한 두 개의 <uses-feature>
요소를 추가했습니다. 이렇게 하면 앱이 자동차에 배포하지 못하도록 하는 화면 방향에 대한 암시적 기능 종속성이 존재하지 않습니다. 단, 이러한 매니페스트 요소는 앱의 동작을 변경하지 않으며 앱의 배포 방식만 변경합니다.
현재 이 앱에는 방향이 가로 모드가 아닌 경우 휴대전화 사용자가 기기를 조작하여 방향을 변경할 필요가 없도록 동영상 플레이어가 열리면 활동의 방향을 자동으로 가로 모드로 설정하는 유용한 기능이 있습니다.
안타깝게도 이 동작은 오늘날 도로에 있는 수많은 자동차를 비롯해 세로 모드로 방향이 고정된 기기에서 깜박이는 루프 또는 레터박스를 유발할 수 있습니다.
이 문제를 해결하려면 현재 기기가 지원하는 화면 방향에 기반한 검사를 추가하면 됩니다.
- 구현을 단순화하기 위해 먼저
Extensions.kt
에 다음을 추가합니다.
Extensions.kt
import android.content.Context
import android.content.pm.PackageManager
...
enum class SupportedOrientation {
Landscape,
Portrait,
}
fun Context.supportedOrientations(): List<SupportedOrientation> {
return when (Pair(
packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE),
packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT)
)) {
Pair(true, false) -> listOf(SupportedOrientation.Landscape)
Pair(false, true) -> listOf(SupportedOrientation.Portrait)
// For backwards compat, if neither feature is declared, both can be assumed to be supported
else -> listOf(SupportedOrientation.Landscape, SupportedOrientation.Portrait)
}
}
- 그런 다음 요청된 방향을 설정하도록 호출을 보호합니다. 모바일 기기의 멀티 윈도우 모드에서도 앱이 이와 비슷한 문제에 직면할 수 있으므로 이 경우에도 방향이 동적으로 설정되지 않도록 하는 검사를 포함할 수 있습니다.
PlayerScreen.kt
import com.example.android.cars.roadreels.SupportedOrientation
import com.example.android.cars.roadreels.supportedOrientations
...
LaunchedEffect(Unit) {
...
// Only automatically set the orientation to landscape if the device supports landscape.
// On devices that are portrait only, the activity may enter a compat mode and won't get to
// use the full window available if so. The same applies if the app's window is portrait
// in multi-window mode.
if (context.supportedOrientations().contains(SupportedOrientation.Landscape)
&& !context.isInMultiWindowMode
) {
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
...
}
(활동이 | (활동이 | 검사를 추가한 후에는 Polestar 2 에뮬레이터에서 플레이어 화면에 레터박스가 발생하지 않습니다. |
이 위치는 앱에서 화면 방향을 설정하는 유일한 위치이므로 이제 앱에서 레터박스가 방지됩니다. 여러분의 앱에서 가로 모드 또는 세로 모드 방향 전용 screenOrientation
속성 또는 setRequestedOrientation
호출이 있는지 확인하고(각 방향의 센서, 반전, 변형 포함) 필요에 따라 레터박스를 제한하기 위해 삭제하거나 보호하세요. 자세한 내용은 기기 호환성 모드를 참고하세요.
시스템 표시줄 제어 가능 여부에 따라 조정
앞에서 적용한 변경사항은 앱이 깜박이는 루프에 진입하거나 레터박스가 발생하지 않도록 하지만 시스템 표시줄은 항상 숨길 수 있다는 또 다른 가정이 손상됩니다. 자동차를 사용하는 사용자는 휴대전화나 태블릿을 사용할 때와 다른 요구사항을 갖기 때문에 OEM은 화면에서 항상 실내 온도 조절기와 같은 차량 컨트롤에 액세스할 수 있도록 앱이 시스템 표시줄을 숨기지 못하도록 할 수 있습니다.
앱이 몰입형 모드로 렌더링될 때 표시줄을 숨길 수 있다고 가정하여 시스템 표시줄 뒤에 렌더링될 가능성이 있습니다. 이전 단계에서 앱에서 레터박스가 발생하지 않은 경우 상단 및 하단 재생 컨트롤이 더 이상 표시되지 않는 것을 볼 수 있었습니다. 이 상황에서는 플레이어를 닫는 버튼이 표시되지 않아 앱을 탐색할 수 없으며, 탐색바를 사용할 수 없어 기능에 접근할 수 없습니다.
가장 쉬운 해결 방법은 다음과 같이 플레이어에 systemBars
창 인셋 패딩을 적용하는 것입니다.
PlayerScreen.kt
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
...
Box(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
) {
PlayerView(...)
PlayerControls(...)
}
그러나 이 방법은 시스템 표시줄에 애니메이션이 적용되어 사라질 때 UI 요소가 이리저리 이동하게 되기 때문에 이상적이지 않습니다.
사용자 경험을 개선하려면 제어할 수 있는 인셋을 추적하고 제어할 수 없는 인셋에만 패딩을 적용하도록 앱을 업데이트하면 됩니다.
- 앱의 다른 화면이 창 인셋을 제어해야 할 수 있으므로 제어 가능한 인셋을
CompositionLocal
로 전달하는 것이 좋습니다.com.example.android.cars.roadreels
패키지에 새 파일LocalControllableInsets.kt
를 만들고 다음을 추가합니다.
LocalControllableInsets.kt
import androidx.compose.runtime.compositionLocalOf
// Assume that no insets can be controlled by default
const val DEFAULT_CONTROLLABLE_INSETS = 0
val LocalControllableInsets = compositionLocalOf { DEFAULT_CONTROLLABLE_INSETS }
- 변경사항을 수신 대기하도록
OnControllableInsetsChangedListener
를 설정합니다.
MainActivity.kt
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat.OnControllableInsetsChangedListener
...
class MainActivity : ComponentActivity() {
private lateinit var onControllableInsetsChangedListener: OnControllableInsetsChangedListener
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
var controllableInsetsTypeMask by remember { mutableIntStateOf(DEFAULT_CONTROLLABLE_INSETS) }
onControllableInsetsChangedListener =
OnControllableInsetsChangedListener { _, typeMask ->
if (controllableInsetsTypeMask != typeMask) {
controllableInsetsTypeMask = typeMask
}
}
WindowCompat.getInsetsController(window, window.decorView)
.addOnControllableInsetsChangedListener(onControllableInsetsChangedListener)
RoadReelsTheme {
RoadReelsApp(calculateWindowSizeClass(this))
}
}
}
override fun onDestroy() {
super.onDestroy()
WindowCompat.getInsetsController(window, window.decorView)
.removeOnControllableInsetsChangedListener(onControllableInsetsChangedListener)
}
}
- 테마와 앱 컴포저블을 포함하고 값을
LocalControllableInsets
에 결합하는 최상위 수준CompositionLocalProvider
를 추가합니다.
MainActivity.kt
import androidx.compose.runtime.CompositionLocalProvider
...
CompositionLocalProvider(LocalControllableInsets provides controllableInsetsTypeMask) {
RoadReelsTheme {
RoadReelsApp(calculateWindowSizeClass(this))
}
}
- 플레이어에서 현재 값을 읽고 이를 사용하여 패딩에 사용할 인셋을 확인합니다.
PlayerScreen.kt
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.union
import androidx.compose.ui.unit.dp
import com.example.android.cars.roadreels.LocalControllableInsets
...
val controllableInsetsTypeMask = LocalControllableInsets.current
// When the system bars can be hidden, ignore them when applying padding to the player and
// controls so they don't jump around as the system bars disappear. If they can't be hidden
// include them so nothing renders behind the system bars
var windowInsetsForPadding = WindowInsets(0.dp)
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.statusBars()) == 0) {
windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.statusBars)
}
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.navigationBars()) == 0) {
windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.navigationBars)
}
Box(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(windowInsetsForPadding)
) {
PlayerView(...)
PlayerControls(...)
}
시스템 표시줄을 숨길 수 있는 경우 콘텐츠가 이리저리 이동되지 않음 | 시스템 표시줄을 숨길 수 없는 경우 콘텐츠가 표시된 상태로 유지됨 |
훨씬 낫네요! 콘텐츠가 이리저리 이동되지 않고, 시스템 표시줄을 제어할 수 없는 자동차에서도 컨트롤을 완전히 볼 수 있습니다.
7. Android Automotive OS 품질 요구사항 충족: 운전자 주의 분산 행동
마지막으로, 자동차와 다른 폼 팩터 사이에는 한 가지 주요한 차이점이 있습니다. 바로 자동차는 운전에 사용된다는 점입니다. 따라서 운전 중 방해 요소를 제한하는 것이 매우 중요합니다. Android Automotive OS용 주차 앱은 모두 운전이 시작되면 재생을 일시중지해야 합니다. 운전이 시작되면 시스템 오버레이가 표시되고, 오버레이된 앱에 대해 onPause
수명 주기 이벤트가 호출됩니다. 바로 호출이 진행된 동안 앱이 재생을 일시중지해야 합니다.
운전 시뮬레이션
에뮬레이터에서 플레이어 뷰로 이동하고 콘텐츠 재생을 시작합니다. 그런 다음 단계에 따라 운전을 시뮬레이션하면 앱의 UI는 시스템에 의해 가려지지만 재생은 일시중지되지 않는 것을 볼 수 있습니다. 이는 DD-2 자동차 앱 품질 가이드라인에 위배됩니다.
운전 시작 시 재생 일시중지
androidx.lifecycle:lifecycle-runtime-compose
아티팩트에 수명 주기 이벤트에서 코드를 실행하는 데 도움이 되는LifecycleEventEffect
가 포함된 종속 항목을 추가합니다.
libs.version.toml
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
Build.gradle.kts (Module :app)
implementation(libs.androidx.lifecycle.runtime.compose)
- 프로젝트를 동기화하여 종속 항목을 다운로드한 후
ON_PAUSE
이벤트가 발생하면 실행되어 재생을 일시중지하는LifecycleEventEffect
를 추가합니다.
PlayerScreen.kt
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
...
@Composable
fun PlayerScreen(...) {
...
LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
player.pause()
}
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
player.play()
}
...
}
수정사항을 구현한 상태에서 앞에서와 동일한 단계에 따라 재생 중 운전을 시뮬레이션하면 재생이 중지되어 DD-2 요구사항을 충족하는 것을 볼 수 있습니다.
8. 원격 디스플레이 에뮬레이터에서 앱 테스트
중앙 콘솔에 기본 화면이 있고 앞유리창 근처 대시보드 상단에 보조 화면이 있는 2개의 화면은 자동차에 새로 등장하고 있는 새로운 구성입니다. 이 경우 앱을 중앙 화면과 보조 화면 간에 이동하여 운전자와 승객에게 더 많은 옵션을 제공할 수 있습니다.
Automotive Distant Display 이미지 설치
- 먼저 Android 스튜디오에서 SDK Manager를 열고, 아직 선택하지 않았다면 SDK Platforms 탭을 선택합니다. SDK Manager 창의 오른쪽 하단에서 Show package details 체크박스가 선택되어 있는지 확인합니다.
- 사용 중인 컴퓨터 아키텍처(x86/ARM)에 맞는 Automotive Distant Display with Google APIs 에뮬레이터 이미지를 설치합니다.
Android Automotive OS Android Virtual Device 만들기
- 기기 관리도구를 열고 창 왼쪽의 Category 열에서 Automotive를 선택합니다. 그런 다음 목록에서 Automotive Distant Display 번들 하드웨어 프로필을 선택하고 Next를 클릭합니다.
- 다음 페이지에서 이전 단계의 시스템 이미지를 선택합니다. Next를 클릭하고 원하는 고급 옵션을 선택한 다음 Finish를 클릭하여 AVD를 만듭니다.
앱 실행
기존 app
실행 구성을 사용하여 방금 만든 에뮬레이터에서 앱을 실행합니다. 원격 디스플레이 에뮬레이터 사용의 안내에 따라 앱을 기본 디스플레이와 원격 디스플레이 간에 이동합니다. 앱이 기본/세부정보 화면에 있을 때와 플레이어 화면에 있을 때 이동해 보고 양쪽 화면에서 앱과 상호작용해 보세요.
9. 원격 디스플레이의 앱 경험 개선
원격 디스플레이에서 앱을 사용하면서 다음 두 가지 사항을 확인했을 수 있습니다.
- 앱을 원격 디스플레이로 이동하거나 원격 디스플레이에서 이동할 때 재생이 다시 시작됨
- 앱이 원격 디스플레이에 있을 때 앱과 상호작용(재생 상태 변경 등)할 수 없음
앱 연속성 개선
재생이 다시 시작되는 문제는 구성 변경으로 인해 활동이 다시 생성되는 데서 비롯됩니다. 앱이 Compose를 사용하여 작성되었고 변경되는 구성이 크기와 관련이 있으므로 크기 기반 구성 변경에 관해 활동 재생성을 제한하여 Compose가 구성 변경을 처리하도록 하면 됩니다. 이렇게 하면 활동 재생성으로 인해 재생이 중지되거나 다시 로드되지 않고 양쪽 디스플레이 간에 원활하게 전환됩니다.
AndroidManifest.xml
<activity
android:name="com.example.android.cars.roadreels.MainActivity"
...
android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout|density">
...
</activity>
재생 컨트롤 구현
앱이 원격 디스플레이에 있을 때 제어할 수 없는 문제를 해결하려면 MediaSession
을 구현하면 됩니다. 미디어 세션은 오디오 또는 동영상 플레이어와 상호작용할 보편적인 방법을 제공합니다. 자세한 내용은 MediaSession을 사용하여 재생 제어 및 광고를 참고하세요.
androidx.media3:media3-session
아티팩트에 종속 항목을 추가합니다.
libs.version.toml
androidx-media3-mediasession = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }
build.gradle.kts (Module :app)
implementation(libs.androidx.media3.mediasession)
- 빌더를 사용하여
MediaSession
을 만듭니다.
PlayerScreen.kt
import androidx.media3.session.MediaSession
@Composable
fun PlayerScreen(...) {
...
val mediaSession = remember(context, player) {
MediaSession.Builder(context, player).build()
}
...
}
- 그런 다음
Player
가 컴포지션 트리를 벗어날 때MediaSession
이 해제되도록Player
컴포저블의DisposableEffect
의onDispose
블록에 라인을 추가합니다.
PlayerScreen.kt
DisposableEffect(Unit) {
onDispose {
mediaSession.release()
player.release()
...
}
}
- 마지막으로, 플레이어 화면에서
adb shell cmd media_session dispatch
명령어를 사용하여 미디어 컨트롤을 테스트합니다.
# To play content
adb shell cmd media_session dispatch play
# To pause content
adb shell cmd media_session dispatch pause
# To toggle the playing state
adb shell cmd media_session dispatch play-pause
이제 원격 디스플레이가 있는 자동차에서 앱이 훨씬 더 잘 작동합니다. 게다가 다른 폼 팩터에서도 더 잘 작동합니다. 이제 화면을 회전하거나 사용자가 앱 창의 크기를 조절할 수 있는 기기에서도 앱이 상황에 맞게 조정됩니다.
미디어 세션을 통합한 덕분에 자동차의 하드웨어 및 소프트웨어 컨트롤뿐 아니라 Google 어시스턴트 쿼리나 헤드폰의 일시중지 버튼과 같은 다른 소스로도 앱의 재생을 제어할 수 있어 사용자에게 여러 폼 팩터에서 앱을 제어할 더 많은 옵션이 제공됩니다.
10. 여러 시스템 구성에서 앱 테스트
기본 디스플레이와 원격 디스플레이에서 앱이 잘 작동한다면, 마지막으로 앱이 여러 시스템 표시줄 구성과 디스플레이 컷아웃을 어떻게 처리하는지 확인해야 합니다. 창 인셋 및 디스플레이 컷아웃 사용에 설명된 대로 Android Automotive OS 기기는 일반적으로 모바일 폼 팩터에서 적용되는 가정을 위반하는 구성이 적용될 수 있습니다.
이 섹션에서는 런타임에 구성할 수 있는 에뮬레이터를 다운로드하고, 왼쪽 시스템 표시줄을 갖도록 에뮬레이터를 구성하고, 이 구성에서 앱을 테스트합니다.
Android Automotive with Google APIs 이미지 설치
- 먼저 Android 스튜디오에서 SDK Manager를 열고, 아직 선택하지 않았다면 SDK Platforms 탭을 선택합니다. SDK Manager 창의 오른쪽 하단에서 Show package details 체크박스가 선택되어 있는지 확인합니다.
- 사용 중인 컴퓨터 아키텍처(x86/ARM)에 맞는 API 33 Android Automotive with Google APIs 에뮬레이터 이미지를 설치합니다.
Android Automotive OS Android Virtual Device 만들기
- 기기 관리도구를 열고 창 왼쪽의 Category 열에서 Automotive를 선택합니다. 그런 다음 목록에서 Automotive (1080p landscape) 번들 하드웨어 프로필을 선택하고 Next를 클릭합니다.
- 다음 페이지에서 이전 단계의 시스템 이미지를 선택합니다. Next를 클릭하고 원하는 고급 옵션을 선택한 다음 Finish를 클릭하여 AVD를 만듭니다.
측면 시스템 표시줄 구성
구성 가능한 에뮬레이터를 사용하여 테스트에 설명된 대로 자동차에 있는 여러 시스템 구성을 에뮬레이션할 수 있는 다양한 옵션이 있습니다.
이 Codelab에서는 com.android.systemui.rro.left
를 사용하여 여러 시스템 표시줄 구성을 테스트합니다. 사용 설정하려면 다음 명령어를 사용하세요.
adb shell cmd overlay enable --user 0 com.android.systemui.rro.left
앱이 systemBars
수정자를 Scaffold
의 contentWindowInsets
로 사용하고 있으므로 콘텐츠는 시스템 표시줄의 안전 영역에 표시됩니다. 앱이 시스템 표시줄은 화면의 상단과 하단에만 표시된다고 가정할 경우 어떤 일이 발생하는지 보려면 매개변수를 다음과 같이 변경합니다.
RoadReelsApp.kt
contentWindowInsets = if (route?.equals(Screen.Player.name) == true) WindowInsets(0.dp) else WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
저런! 목록과 세부정보 화면이 시스템 표시줄 뒤에 렌더링됩니다. 앞에서 한 작업 덕분에 시스템 표시줄을 제어할 수 없더라도 플레이어 화면은 정상적으로 표시됩니다.
다음 섹션으로 넘어가기 전에 방금 windowContentPadding
매개변수에 적용한 변경사항을 되돌리세요!
11. 디스플레이 컷아웃 사용
마지막으로, 일부 자동차에는 모바일 기기의 화면과 매우 다른 디스플레이 컷아웃을 갖는 화면이 있습니다. 일부 Android Automotive OS 차량에는 노치나 핀홀 카메라 컷아웃 대신 화면을 곡선으로 만드는 곡선형 화면이 있습니다.
이러한 디스플레이 컷아웃이 있는 경우에 앱이 어떻게 작동하는지 보려면 먼저 다음 명령어를 사용하여 디스플레이 컷아웃을 사용 설정합니다.
adb shell cmd overlay enable --user 0 com.android.internal.display.cutout.emulation.free_form
앱이 얼마나 잘 작동하는지 테스트하려면 직전 섹션에서 사용한 왼쪽 시스템 표시줄도 사용 설정합니다.
adb shell cmd overlay enable --user 0 com.android.systemui.rro.left
앱이 디스플레이 컷아웃에 렌더링되지 않는 것을 볼 수 있습니다. (지금은 컷아웃의 정확한 모양을 확인하기 어렵지만 다음 단계에서 명확해질 것입니다.) 이 정도로도 괜찮고 컷아웃에 렌더링되는 경우에 비해 나은 경험이긴 하지만, 컷아웃에 맞게 조정되지는 않습니다.
디스플레이 컷아웃에 렌더링
사용자에게 최대한 몰입도 높은 경험을 제공하려면 디스플레이 컷아웃에 렌더링하여 훨씬 더 많은 화면 공간을 활용할 수 있습니다.
- 디스플레이 컷아웃에 렌더링하려면 자동차 관련 재정의를 보관하는
integers.xml
파일을 만듭니다. 이렇게 하려면 UI 모드 한정자를 Car Dock 값과 함께 사용합니다. (이 이름은 Android Auto만 존재하던 시절의 유물이지만, Android Automotive OS에서도 사용됩니다.) 또한 Android R부터LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
가 도입되었으므로 값이 30인 Android Version 한정자를 추가합니다. 자세한 내용은 대체 리소스 사용을 참고하세요.
- 방금 만든 파일 (
res/values-car-v30/integers.xml
)에 다음을 추가합니다.
integers.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="windowLayoutInDisplayCutoutMode">3</integer>
</resources>
정수 값 3
은 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
에 대응되며, LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
에 대응되는 res/values/integers.xml
의 기본값 0
을 재정의합니다. 이 정수 값은 enableEdgeToEdge()
에 의해 설정된 모드를 재정의하기 위해 이미 MainActivity.kt
에서 참조됩니다. 이 속성에 관한 자세한 내용은 참조 문서를 참고하세요.
이제 앱을 실행하면 콘텐츠가 컷아웃으로 확장되어 몰입도 높은 경험을 선사하는 것을 볼 수 있습니다. 그러나 상단 앱 바와 콘텐츠의 일부가 디스플레이 컷아웃에 의해 부분적으로 가려져서 앱이 시스템 표시줄은 상단과 하단에만 표시된다고 가정한 경우와 비슷한 문제가 발생합니다.
상단 앱 바 수정
상단 앱 바를 수정하려면 CenterAlignedTopAppBar
컴포저블에 다음 windowInsets
매개변수를 추가하면 됩니다.
RoadReelsApp.kt
import androidx.compose.foundation.layout.safeDrawing
...
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
safeDrawing
은 displayCutout
인셋과 systemBars
인셋을 모두 포함하기 때문에 상단 앱 바를 배치할 때 systemBars
만 사용하는 기본 windowInsets
매개변수보다 한층 개선됩니다.
또한 상단 앱 바가 창 상단에 배치되었으므로 safeDrawing
인셋의 하단 구성요소를 포함하면 안 됩니다. 이렇게 하면 불필요한 패딩이 추가될 수 있습니다.
기본 화면 수정
기본 화면과 세부정보 화면의 콘텐츠를 수정하는 한 가지 방법은 Scaffold
의 contentWindowInsets
에 systemBars
대신 safeDrawing
를 사용하는 것입니다. 그러나 이 옵션을 사용하면 디스플레이 컷아웃이 시작되는 지점에서 콘텐츠가 갑자기 잘려서 몰입감이 훨씬 낮아집니다. 앱이 디스플레이 컷아웃에 전혀 렌더링되지 않은 경우보다 별로 낫지 않죠.
몰입도 높은 사용자 인터페이스를 구현하려면 화면에 있는 각 구성요소의 인셋을 처리하면 됩니다.
Scaffold
의contentWindowInsets
가PlayerScreen
에만 적용되는 대신 항상 0dp가 되도록 업데이트합니다. 이렇게 하면 각 화면 및/또는 화면에 있는 각 구성요소가 인셋과 관련하여 어떻게 작동할지 직접 결정합니다.
RoadReelsApp.kt
Scaffold(
...,
contentWindowInsets = WindowInsets(0.dp)
) { ... }
safeDrawing
인셋의 가로 방향 구성요소를 사용하도록 행 헤더Text
컴포저블의windowInsetsPadding
을 설정합니다. 이들 인셋의 상단 구성요소는 상단 앱 바에서 처리하고 하단 구성요소는 나중에 처리됩니다.
MainScreen.kt
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
...
LazyColumn(
contentPadding = PaddingValues(bottom = dimensionResource(R.dimen.screen_edge_padding))
) {
items(NUM_ROWS) { rowIndex: Int ->
Text(
"Row $rowIndex",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
.padding(
horizontal = dimensionResource(R.dimen.screen_edge_padding),
vertical = dimensionResource(R.dimen.row_header_vertical_padding)
)
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))
)
...
}
LazyRow
의contentPadding
매개변수를 삭제합니다. 그런 다음 모든 썸네일을 완전히 볼 수 있도록 각LazyRow
의 시작과 끝에 대응되는safeDrawing
구성요소의 너비Spacer
를 추가합니다.widthIn
수정자를 사용하여 스페이서의 너비가 콘텐츠 패딩보다 크거나 같아지도록 합니다. 이러한 요소가 없으면 행의 시작과 끝에 있는 항목들이 행의 시작/끝으로 완전히 스와이프되어도 시스템 표시줄 및/또는 디스플레이 컷아웃 뒤에 가려질 수 있습니다.
MainScreen.kt
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsEndWidth
import androidx.compose.foundation.layout.windowInsetsStartWidth
...
LazyRow(
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.list_item_spacing)),
) {
item {
Spacer(
Modifier
.windowInsetsStartWidth(WindowInsets.safeDrawing)
.widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
)
}
items(NUM_ITEMS_PER_ROW) { ... }
item {
Spacer(
Modifier
.windowInsetsEndWidth(WindowInsets.safeDrawing)
.widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
)
}
}
- 마지막으로,
LazyColumn
의 끝에Spacer
를 추가하여 화면 하단의 시스템 표시줄 또는 디스플레이 컷아웃 인셋을 고려합니다.LazyColumn
상단의 경우 상단 앱 바에서 처리하므로 이러한 스페이서가 필요하지 않습니다. 앱이 상단 앱 바 대신 하단 앱 바를 사용하는 경우windowInsetsTopHeight
수정자를 사용하여 목록 시작 부분에Spacer
를 추가합니다. 앱이 상단 앱 바와 하단 앱 바를 모두 사용하는 경우 두 스페이서 모두 필요하지 않습니다.
MainScreen.kt
import androidx.compose.foundation.layout.windowInsetsBottomHeight
...
LazyColumn(...){
items(NUM_ROWS) { ... }
item {
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
}
}
좋습니다. 상단 앱 바를 완전히 볼 수 있고, 행의 끝으로 스크롤하면 모든 썸네일을 완전히 볼 수 있습니다.
세부정보 화면 수정
세부정보 화면은 그다지 나쁘지 않지만 콘텐츠가 잘린다는 문제가 있습니다.
세부정보 화면에는 스크롤 가능한 콘텐츠가 없으므로 최상위 수준 Box
에 windowInsetsPadding
수정자를 추가하기만 하면 됩니다.
DetailScreen.kt
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
...
Box(
modifier = modifier
.padding(dimensionResource(R.dimen.screen_edge_padding))
.windowInsetsPadding(WindowInsets.safeDrawing)
) { ... }
플레이어 화면 수정
PlayerScreen
의 경우 Android Automotive OS 품질 요구사항 충족: 탐색 용이성에서 이미 시스템 표시줄 창 인셋의 일부 또는 전부에 패딩을 적용했지만, 이제 앱이 디스플레이 컷아웃에 렌더링되기 때문에 패딩만으로는 가려지지 않도록 할 수 없습니다. 모바일 기기에서는 디스플레이 컷아웃이 거의 항상 시스템 표시줄 안에 완전히 포함됩니다. 그러나 자동차에서는 디스플레이 컷아웃이 시스템 표시줄을 훨씬 넘어 확장될 수 있어 이러한 가정이 적용되지 않습니다.
이 문제를 해결하려면 windowInsetsForPadding
변수의 초기 값을 0 값에서 displayCutout
으로 변경하면 됩니다.
PlayerScreen.kt
import androidx.compose.foundation.layout.displayCutout
...
var windowInsetsForPadding = WindowInsets(WindowInsets.displayCutout)
좋습니다. 이제 앱이 화면을 최대한 활용하며, 사용성도 뛰어납니다.
모바일 기기에서 앱을 실행하면 몰입감이 한층 향상됩니다. 목록 항목이 탐색 메뉴 뒤쪽을 포함하여 화면 가장자리까지 렌더링됩니다.
12. 축하합니다
첫 번째 주차 앱을 성공적으로 이전하고 최적화했습니다. 이제 배운 내용을 활용하여 자신의 앱에 적용해 보세요.
시도해 볼 만한 작업
- 일부 크기 리소스 값을 재정의하여 자동차에서 실행될 때 요소의 크기를 늘립니다.
- 구성 가능한 에뮬레이터의 더 많은 구성을 사용해 봅니다.
- 여러 OEM 에뮬레이터 이미지를 사용하여 앱을 테스트합니다.
추가 자료
- Android Automotive OS용 주차 앱 빌드
- Android Automotive OS용 동영상 앱 빌드
- Android Automotive OS용 게임 빌드
- Android Automotive OS용 브라우저 빌드
- 자동차용 Android 앱 품질 페이지에서는 우수한 사용자 경험을 제공하고 Play 스토어 검토를 통과하기 위해 앱이 충족해야 하는 기준을 설명합니다. 앱의 카테고리로 필터링하세요.