자동차용 Android 미디어 앱 빌드

Android Automotive OS 및 Android Auto를 사용하면 미디어 앱 콘텐츠를 차에 탄 사용자에게 제공할 수 있습니다. 주행 중에 Android에서 앱 환경을 사용 설정하는 방법에 관한 개요는 자동차용 Android 개요를 참조하세요.

이 가이드에서는 이미 독자에게 미디어 전화 앱이 있다고 가정합니다. 이 가이드에서는 Android Automotive OS용 앱을 빌드하는 방법과 Android Auto용 전화 앱을 확장하는 방법에 관해 설명합니다.

시작하기 전에

앱을 빌드하기 전에 자동차용 Android 시작하기에 설명된 단계를 따른 후 이 섹션에 있는 정보를 검토하세요.

주요 용어 및 개념

미디어 탐색 서비스
MediaBrowseServiceCompat API를 준수하는 미디어 앱으로 구현된 Android 서비스입니다. 앱에서는 이 서비스를 사용하여 미디어 탐색 콘텐츠를 Android Automotive OS 및 Android Auto에 노출합니다.
미디어 탐색
미디어 앱에서 콘텐츠를 Android Automotive OS 및 Android Auto에 노출하기 위해 사용하는 API입니다.
미디어 항목
미디어 탐색 트리에 있는 개별 MediaBrowserCompat.MediaItem 개체입니다. 미디어 항목은 다음 유형 중 하나일 수 있습니다.
  • 재생 가능: 이 항목은 앨범의 노래, 책을 구성하는 장 또는 팟캐스트의 에피소드와 같은 실제 사운드 스트림을 나타냅니다.
  • 탐색 가능: 이 항목에서는 재생 가능한 미디어 항목을 그룹으로 구성합니다. 예를 들어 장을 책으로, 노래를 앨범으로 또는 에피소드를 팟캐스트로 그룹화할 수 있습니다.

참고: 탐색 가능한 동시에 재생 가능한 미디어 항목은 재생 가능 유형으로 취급됩니다.

차량에 최적화

Android Automotive OS 설계 가이드라인을 준수하는 Android Automotive OS 앱을 위한 활동입니다. 이러한 활동의 인터페이스는 Android Automotive OS에서 작성하지 않으므로 앱이 설계 가이드라인을 준수하는지 확인해야 합니다. 일반적으로 여기에는 더 큰 탭 타겟 및 글꼴 크기, 주야간 모드 지원, 더 높은 대비율이 포함됩니다.

차량에 최적화된 사용자 인터페이스는 사용자의 주의 또는 상호작용이 더 오래 필요할 수 있기 때문에 CUXR(Car User Experience Restrictions)이 적용되지 않는 경우에만 표시가 허용됩니다. CUXR은 차량이 정차 또는 주차 중일 때는 적용되지 않지만 차량이 움직이면 항상 적용됩니다.

Android Auto는 미디어 탐색 서비스의 정보를 사용하여 자체 차량에 최적화된 인터페이스를 가져오므로 Android Auto를 위한 활동을 설계할 필요가 없습니다.

앱의 manifest 파일 구성

앱을 Android Automotive OS에서 사용할 수 있고 전화 앱에서 Android Auto의 미디어 서비스를 지원한다는 것을 나타내도록 앱의 manifest 파일을 구성해야 합니다.

Android Automotive OS 지원 선언

Android Automotive OS에 배포하는 앱은 전화 앱과 분리되어 있어야 합니다. 코드를 재사용하고 앱을 쉽게 빌드하여 출시할 수 있도록 모듈Android App Bundle을 사용하는 것이 좋습니다. Android Automotive OS 모듈의 manifest 파일에 다음 항목을 추가하여 모듈의 코드가 Android Automotive OS로 제한됨을 표시하세요.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
              package="com.example.media">
       <uses-feature
               android:name="android.hardware.type.automotive"
               android:required="true"/>
    </manifest>
    

Android Auto 미디어 지원 선언

Android Auto에서는 전화 앱을 사용하여 운전자에 최적화된 환경을 사용자에게 제공합니다. 다음 manifest 항목을 사용하여 전화 앱에서 Android Auto를 지원한다는 것을 선언하세요.

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

이 manifest 항목은 앱에서 지원하는 자동차 기능을 선언하는 XML 파일을 가리킵니다. 미디어 앱이 있음을 나타내려면 프로젝트의 res/xml/ 디렉터리에 automotive_app_desc.xml이라는 XML 파일을 추가하세요. 이 파일에는 다음 내용이 포함되어야 합니다.

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

미디어 탐색 서비스 선언

Android Automotive OS와 Android Auto 모두 미디어 항목을 탐색하기 위해 미디어 브라우저 서비스를 통해 앱에 연결합니다. manifest에서 미디어 탐색 서비스를 선언하면 Android Automotive OS 및 Android Auto에서 서비스를 검색하고 앱에 연결할 수 있습니다.

다음 코드 스니펫에서는 manifest에서 미디어 탐색 서비스를 선언하는 방법을 보여줍니다. 이 코드를 Android Automotive OS 모듈의 manifest 파일과 전화 앱의 manifest 파일에 포함해야 합니다.

<application>
        ...
        <service android:name=".MyMediaBrowserService"
                 android:exported="true">
            <intent-filter>
                <action android:name="android.media.browse.MediaBrowserService"/>
            </intent-filter>
        </service>
        ...
    <application>
    

앱 아이콘 지정

Android Automotive OS 및 Android Auto에서는 사용자가 앱을 사용하고 자동차와 상호작용할 때 앱의 아이콘을 다양한 위치에서 사용자에게 표시합니다. 예를 들어 사용자가 탐색 앱을 실행 중인데 노래 하나가 끝나고 새 노래가 시작되면 앱의 아이콘이 포함된 알림이 사용자에게 표시됩니다. 또한 Android Auto 및 Android Automotive OS에서는 사용자가 미디어 콘텐츠를 탐색할 때 다른 위치에서 앱 아이콘을 표시합니다.

다음 manifest 선언을 통해 앱을 나타내는 데 사용할 아이콘을 지정할 수 있습니다.

<!--The android:icon attribute is used by Android Automotive OS-->
    <application
        ...
        android:icon="@mipmap/ic_launcher">
        ...
        <!--Used by Android Auto-->
        <meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
                   android:resource="@drawable/ic_auto_icon" />
        ...
    <application>
    

미디어 탐색 서비스 빌드

MediaBrowserServiceCompat 클래스를 확장하여 미디어 탐색 서비스를 만듭니다. 그런 다음 Android Automotive OS와 Android Auto에서 서비스를 사용하여 다음 작업을 할 수 있습니다.

  • 사용자에게 메뉴를 표시하기 위해 앱의 콘텐츠 계층 구조 탐색
  • 오디오 재생을 제어하기 위해 앱에 있는 MediaSessionCompat 개체의 토큰 가져오기

미디어 브라우저 서비스 워크플로

이 섹션에서는 일반적인 사용자 워크플로가 진행되는 과정에서 Android Automotive OS 및 Android Auto가 미디어 탐색 서비스와 상호작용하는 방식에 관해 설명합니다.

  1. 사용자가 Android Automotive OS 또는 Android Auto에서 앱을 시작합니다.
  2. Android Automotive OS 또는 Android Auto는 onCreate() 메서드를 사용하여 앱의 미디어 탐색 서비스에 접속합니다. onCreate() 메서드를 구현할 때 MediaSessionCompat 개체와 이 개체의 콜백 개체를 만들어 등록해야 합니다.
  3. Android Automotive OS 또는 Android Auto에서 서비스의 onGetRoot() 메서드를 호출하여 콘텐츠 계층 구조에 있는 루트 미디어 항목을 가져옵니다. 루트 미디어 항목은 표시되지 않지만 앱에서 더 많은 콘텐츠를 검색하는 데 사용됩니다.
  4. Android Automotive OS 또는 Android Auto에서는 서비스의 onLoadChildren() 메서드를 호출하여 루트 미디어 항목의 하위 요소를 가져옵니다. Android Automotive OS 및 Android Auto에서는 이러한 미디어 항목을 최상위 수준의 콘텐츠 항목으로 표시합니다. 최상위 수준의 콘텐츠 항목은 탐색 가능해야 합니다.
  5. 사용자가 탐색 가능한 미디어 항목을 선택하면 선택한 메뉴 항목의 하위 요소를 가져오기 위해 서비스의 onLoadChildren() 메서드가 다시 호출됩니다.
  6. 사용자가 재생 가능한 미디어 항목을 선택하면 Android Automotive OS 또는 Android Auto에서 적절한 미디어 세션 콜백 메서드를 호출하여 이 작업을 실행합니다.
  7. 앱에서 지원하는 경우 사용자는 콘텐츠를 검색할 수도 있습니다. 이 경우 Android Automotive OS 또는 Android Auto에서는 서비스의 onSearch() 메서드를 호출합니다.

사용자가 미디어 앱 내에서 탐색하는 방법

사용자가 앱의 콘텐츠를 빠르게 탐색할 수 있도록 Android Auto에는 사용자가 터치 키보드에서 문자를 선택할 수 있는 탐색 기능이 포함되어 있습니다. 그러면 선택한 문자로 시작하는 항목의 목록이 현재 창 목록에 표시됩니다. 이 기능은 정렬된 콘텐츠와 정렬되지 않은 콘텐츠 모두에서 작동하며 현재는 영어로만 제공됩니다.

그림 1. 차량 화면의 알파 선택도구

그림 2. 차량 화면의 알파벳순 목록 보기

콘텐츠 계층 구조 빌드

Android Automotive OS 및 Android Auto에서는 앱의 미디어 탐색 서비스를 호출하여 사용할 수 있는 콘텐츠를 찾습니다. 이 작업을 지원하려면 브라우저 서비스에서 onGetRoot()onLoadChildren()이라는 두 가지 메서드를 구현해야 합니다.

onGetRoot 구현

서비스의 onGetRoot() 메서드에서는 콘텐츠 계층 구조의 루트 노드에 관한 정보를 반환합니다. Android Automotive OS와 Android Auto에서는 이 루트 노드를 사용하여 onLoadChildren() 메서드로 나머지 콘텐츠를 요청합니다.

다음 코드 스니펫은 onGetRoot() 메서드를 간단히 구현한 것입니다.

Kotlin

    override fun onGetRoot(
        clientPackageName: String,
        clientUid: Int,
        rootHints: Bundle?
    ): BrowserRoot? =
        // Verify that the specified package is allowed to access your
        // content! You'll need to write your own logic to do this.
        if (!isValid(clientPackageName, clientUid)) {
            // If the request comes from an untrusted package, return null.
            // No further calls will be made to other media browsing methods.

            null
        } else MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null)
    

자바

    @Override
    public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
        Bundle rootHints) {

        // Verify that the specified package is allowed to access your
        // content! You'll need to write your own logic to do this.
        if (!isValid(clientPackageName, clientUid)) {
            // If the request comes from an untrusted package, return null.
            // No further calls will be made to other media browsing methods.

            return null;
        }

        return new MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null);
    }
    

이 메서드에 관한 더 자세한 예는 GitHub의 범용 Android 뮤직 플레이어 샘플 앱에서 onGetRoot() 메서드를 참조하세요.

onGetRoot()에 패키지 유효성 검사 추가

서비스의 onGetRoot() 메서드가 호출되면 호출 패키지는 서비스에 식별 정보를 전달합니다. 서비스에서는 이 정보를 사용하여 이 패키지가 콘텐츠에 액세스할 수 있는지 여부를 확인할 수 있습니다. 예를 들어 허용 목록과 clientPackageName을 비교하고 패키지의 APK에 서명하는 데 사용된 인증서를 확인하여 앱의 콘텐츠에 액세스할 수 있는 권한을 승인된 패키지 목록으로 제한할 수 있습니다. 패키지를 확인할 수 없으면 null을 반환하여 콘텐츠 액세스를 거부합니다.

콘텐츠에 액세스할 수 있는 시스템 앱(예: Android Automotive OS 및 Android Auto)을 제공하려면 시스템 앱에서 onGetRoot() 메소드를 호출할 때 서비스에서 항상 null이 아닌 BrowserRoot를 반환해야 합니다. 다음 코드 스니펫에서는 호출 패키지가 시스템 앱인지 여부를 서비스에서 검사하는 방법을 보여줍니다.

fun isKnownCaller(
        callingPackage: String,
        callingUid: Int
    ): Boolean {
        ...
        val isCallerKnown = when {
           // If the system is making the call, allow it.
           callingUid == Process.SYSTEM_UID -> true
           // If the app was signed by the same certificate as the platform
           // itself, also allow it.
           callerSignature == platformSignature -> true
           // ... more cases
        }
        return isCallerKnown
    }
    

이 코드 스니펫은 GitHub의 범용 Android 뮤직 플레이어 샘플 앱에 있는 PackageValidator 클래스에서 발췌한 것입니다. 서비스의 onGetRoot() 메서드에 대한 패키지 유효성 검사를 구현하는 방법에 관한 더 자세한 예는 이 클래스를 참조하세요.

onLoadChildren() 구현

Android Automotive OS와 Android Auto에서는 루트 노드 개체를 수신한 후 이 루트 노드 개체에서 onLoadChildren()을 호출하여 하위 요소를 가져오는 방식으로 최상위 메뉴를 빌드합니다. 클라이언트 앱에서는 하위 요소 노드 개체를 사용해 이와 동일한 메서드를 호출하여 하위 메뉴를 빌드합니다.

콘텐츠 계층 구조의 각 노드는 MediaBrowserCompat.MediaItem 개체로 표시됩니다. 이러한 미디어 항목은 각기 고유 ID 문자열로 식별됩니다. 클라이언트 앱에서는 이러한 ID 문자열을 불투명 토큰으로 취급합니다. 클라이언트 앱은 하위 메뉴를 탐색하거나 미디어 항목을 재생하려 할 때 이 토큰을 전달합니다. 앱은 이 토큰을 적절한 미디어 항목과 연결하는 일을 담당합니다.

참고: Android Automotive OS와 Android Auto는 메뉴의 각 수준에 표시할 수 있는 미디어 항목의 수에 제한이 있습니다. 이러한 제한은 운전자의 주의를 분산하는 요소를 최소화하고 음성 명령으로 앱을 작동하는 데 도움이 됩니다. 자세한 내용은 콘텐츠 세부정보 탐색Android Auto 앱 검색 창을 참조하세요.

다음 코드 스니펫은 onLoadChildren() 메서드를 간단히 구현한 것입니다.

Kotlin

    override fun onLoadChildren(
        parentMediaId: String,
        result: Result<List<MediaBrowserCompat.MediaItem>>
    ) {
        // Assume for example that the music catalog is already loaded/cached.

        val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()

        // Check if this is the root menu:
        if (MY_MEDIA_ROOT_ID == parentMediaId) {

            // build the MediaItem objects for the top level,
            // and put them in the mediaItems list
        } else {

            // examine the passed parentMediaId to see which submenu we're at,
            // and put the children of that menu in the mediaItems list
        }
        result.sendResult(mediaItems)
    }
    

자바

    @Override
    public void onLoadChildren(final String parentMediaId,
        final Result<List<MediaBrowserCompat.MediaItem>> result) {

        // Assume for example that the music catalog is already loaded/cached.

        List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();

        // Check if this is the root menu:
        if (MY_MEDIA_ROOT_ID.equals(parentMediaId)) {

            // build the MediaItem objects for the top level,
            // and put them in the mediaItems list
        } else {

            // examine the passed parentMediaId to see which submenu we're at,
            // and put the children of that menu in the mediaItems list
        }
        result.sendResult(mediaItems);
    }
    

이 메서드에 관한 전체 예시는 GitHub의 범용 Android 뮤직 플레이어 샘플 앱에서 onLoadChildren() 메서드를 참조하세요.

콘텐츠 스타일 적용

탐색 가능 또는 재생 가능 항목을 사용하여 콘텐츠 계층 구조를 빌드한 후에는 그러한 항목이 자동차에 표시되는 방식을 결정하는 콘텐츠 스타일을 적용할 수 있습니다.

다음과 같은 콘텐츠 스타일을 사용할 수 있습니다.

목록 항목

이 콘텐츠 스타일에서는 이미지보다 제목 및 메타데이터의 우선순위가 더 높습니다.

그리드 항목

이 콘텐츠 스타일에서는 제목 및 메타데이터보다 이미지의 우선순위가 더 높습니다.

제목 항목

이 콘텐츠 스타일에서는 목록 항목 콘텐츠 스타일보다 훨씬 더 많은 메타데이터를 표시합니다. 이 콘텐츠 스타일을 사용하려면 각 미디어 항목에 추가 메타데이터를 제공해야 합니다.

기본 콘텐츠 스타일 설정

서비스에서 onGetRoot() 메서드의 BrowserRoot 추가 번들에 특정 상수를 포함하여 미디어 항목이 표시되는 방식에 전역 기본값을 설정할 수 있습니다. Android Automotive OS 및 Android Auto는 탐색 트리에서 각 항목과 관련된 추가 번들을 읽고 이 상수를 찾아 적절한 스타일을 결정합니다.

앱에서 이 상수를 선언하려면 다음 코드를 사용하세요.

Kotlin

    /** Declares that ContentStyle is supported */
    val CONTENT_STYLE_SUPPORTED = "android.media.browse.CONTENT_STYLE_SUPPORTED"

    /**
    * Bundle extra indicating the presentation hint for playable media items.
    */
    val CONTENT_STYLE_PLAYABLE_HINT = "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT"

    /**
    * Bundle extra indicating the presentation hint for browsable media items.
    */
    val CONTENT_STYLE_BROWSABLE_HINT = "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT"

    /**
    * Specifies the corresponding items should be presented as lists.
    */
    val CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1

    /**
    * Specifies that the corresponding items should be presented as grids.
    */
    val CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2
    

자바

    /** Declares that ContentStyle is supported */
    public static final String CONTENT_STYLE_SUPPORTED =
       "android.media.browse.CONTENT_STYLE_SUPPORTED";

    /**
    * Bundle extra indicating the presentation hint for playable media items.
    */
    public static final String CONTENT_STYLE_PLAYABLE_HINT =
       "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT";

    /**
    * Bundle extra indicating the presentation hint for browsable media items.
    */
    public static final String CONTENT_STYLE_BROWSABLE_HINT =
       "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT";

    /**
    * Specifies the corresponding items should be presented as lists.
    */
    public static final int CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1;

    /**
    * Specifies that the corresponding items should be presented as grids.
    */
    public static final int CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2;
    

이 상수를 선언한 후 서비스에서 onGetRoot() 메서드의 추가 번들에 이 상수를 포함하여 기본 콘텐츠 스타일을 설정하세요. 다음 코드 스니펫에서는 탐색 가능한 항목의 기본 콘텐츠 스타일을 그리드로 설정하고 재생 가능한 항목을 목록으로 설정하는 방법을 보여줍니다.

Kotlin

    @Nullable
    override fun onGetRoot(
        @NonNull clientPackageName: String,
        clientUid: Int,
        @Nullable rootHints: Bundle
    ): BrowserRoot {
        val extras = Bundle()
        extras.putBoolean(CONTENT_STYLE_SUPPORTED, true)
        extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE)
        extras.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE)
        return BrowserRoot(ROOT_ID, extras)
    }
    

자바

    @Nullable
    @Override
    public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
       @Nullable Bundle rootHints) {
       Bundle extras = new Bundle();
       extras.putBoolean(CONTENT_STYLE_SUPPORTED, true);
       extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE);
       extras.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE);
       return new BrowserRoot(ROOT_ID, extras);
    }
    

항목별 콘텐츠 스타일 설정

콘텐츠 스타일 API를 사용하면 탐색 가능한 미디어 항목의 하위 요소에 기본 콘텐츠 스타일을 재정의할 수 있습니다. 기본값보다 우선 적용되게 하려면 미디어 항목의 MediaDescription에 추가 번들을 만드세요.

다음 코드 스니펫에서는 기본 콘텐츠 스타일을 재정의하는 탐색 가능한 MediaItem을 만드는 방법을 보여줍니다.

Kotlin

    private fun createBrowsableMediaItem(
        mediaId: String,
        folderName: String,
        iconUri: Uri
    ): MediaBrowser.MediaItem {
        val mediaDescriptionBuilder = MediaDescription.Builder()
        mediaDescriptionBuilder.setMediaId(mediaId)
        mediaDescriptionBuilder.setTitle(folderName)
        mediaDescriptionBuilder.setIconUri(iconUri)
        val extras = Bundle()
        extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE)
        extras.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE)
        return MediaBrowser.MediaItem(
            mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)
    }
    

자바

    private MediaBrowser.MediaItem createBrowsableMediaItem(String mediaId,
       String folderName, Uri iconUri) {
       MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
       mediaDescriptionBuilder.setMediaId(mediaId);
       mediaDescriptionBuilder.setTitle(folderName);
       mediaDescriptionBuilder.setIconUri(iconUri);
       Bundle extras = new Bundle();
       extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE);
       extras.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE);
       return new MediaBrowser.MediaItem(
           mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE);
    }
    

제목 항목 추가

미디어 항목을 제목 항목으로 표시하려면 항목을 그룹화하는 항목별 콘텐츠 스타일을 사용하세요. 그룹의 모든 미디어 항목에서는 동일한 문자열을 사용하는 추가 번들을 MediaDescription에서 선언해야 합니다. 이 문자열은 그룹의 제목으로 사용되며 현지화할 수 있습니다.

Android Automotive OS와 Android Auto에서는 이러한 방식으로 그룹화된 항목은 정렬하지 않습니다. 미디어 항목은 표시할 순서대로 함께 전달해야 합니다.

예를 들어 앱이 다음과 같은 순서로 미디어 항목 3개를 전달했다고 가정하겠습니다.

  • extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")이 포함된 미디어 항목 1
  • extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")이 포함된 미디어 항목 2
  • extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")이 포함된 미디어 항목 3

Android Automotive OS와 Android Auto에서는 미디어 항목 1과 미디어 항목 3을 'Songs'라는 그룹으로 병합하는 대신에 별도로 유지합니다.

다음 코드 스니펫에서는 하위 그룹 제목이 "Songs"MediaItem을 만드는 방법을 보여줍니다.

Kotlin

    val EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT = "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"

    private fun createMediaItem(
        mediaId: String,
        folderName: String,
        iconUri: Uri
    ): MediaBrowser.MediaItem {
        val mediaDescriptionBuilder = MediaDescription.Builder()
        mediaDescriptionBuilder.setMediaId(mediaId)
        mediaDescriptionBuilder.setTitle(folderName)
        mediaDescriptionBuilder.setIconUri(iconUri)
        val extras = Bundle()
        extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
        return MediaBrowser.MediaItem(
            mediaDescriptionBuilder.build(), /* playable or browsable flag*/)
    }
    

자바

    public static final String EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT =
      "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT";

    private MediaBrowser.MediaItem createMediaItem(String mediaId, String folderName, Uri iconUri) {
       MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
       mediaDescriptionBuilder.setMediaId(mediaId);
       mediaDescriptionBuilder.setTitle(folderName);
       mediaDescriptionBuilder.setIconUri(iconUri);
       Bundle extras = new Bundle();
       extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs");
       return new MediaBrowser.MediaItem(
           mediaDescriptionBuilder.build(), /* playable or browsable flag*/);
    }
    

추가 메타데이터 표시기 표시

그림 3. 메타데이터 표시기가 포함된 재생 보기

추가 메타데이터 표시기를 포함하면 미디어 탐색 트리의 콘텐츠와 재생 중 콘텐츠를 한눈에 알아볼 수 있는 정보를 제공할 수 있습니다. 탐색 트리 내에서 Android Automotive OS와 Android Auto는 항목과 관련된 추가 정보를 읽고 특정 상수를 찾아 어떤 표시기를 표시할지 결정합니다. 미디어 재생 중에 Android Automotive OS와 Android Auto는 미디어 세션의 메타데이터를 읽고 특정 상수를 찾아 어떤 표시기를 표시할지 결정합니다.

다음 코드를 사용하여 앱에서 메타데이터 표시기 상수를 선언하세요.

Kotlin

    // Bundle extra indicating that a song contains explicit content.
    var EXTRA_IS_EXPLICIT = "android.media.IS_EXPLICIT"

    /**
    * Bundle extra indicating that a media item is available offline.
    * Same as MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS.
    */
    var EXTRA_IS_DOWNLOADED = "android.media.extra.DOWNLOAD_STATUS"

    /**
    * Bundle extra value indicating that an item should show the corresponding
    * metadata.
    */
    var EXTRA_METADATA_ENABLED_VALUE:Long = 1

    /**
    * Bundle extra indicating the played state of long-form content (such as podcast
    * episodes or audiobooks).
    */
    var EXTRA_PLAY_COMPLETION_STATE = "android.media.extra.PLAYBACK_STATUS"

    /**
    * Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
    * not been played at all.
    */
    var STATUS_NOT_PLAYED = 0

    /**
    * Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
    * been partially played (i.e. the current position is somewhere in the middle).
    */
    var STATUS_PARTIALLY_PLAYED = 1

    /**
    * Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
    * been completed.
    */
    var STATUS_FULLY_PLAYED = 2
    

자바

    // Bundle extra indicating that a song contains explicit content.
    String EXTRA_IS_EXPLICIT = "android.media.IS_EXPLICIT";

    /**
     * Bundle extra indicating that a media item is available offline.
     * Same as MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS.
     */
    String EXTRA_IS_DOWNLOADED = "android.media.extra.DOWNLOAD_STATUS";

    /**
     * Bundle extra value indicating that an item should show the corresponding
     * metadata.
     */
    long EXTRA_METADATA_ENABLED_VALUE = 1;

    /**
     * Bundle extra indicating the played state of long-form content (such as podcast
     * episodes or audiobooks).
     */
    String EXTRA_PLAY_COMPLETION_STATE = "android.media.extra.PLAYBACK_STATUS";

    /**
     * Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
     * not been played at all.
     */
    int STATUS_NOT_PLAYED = 0;

    /**
     * Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
     * been partially played (i.e. the current position is somewhere in the middle).
     */
    int STATUS_PARTIALLY_PLAYED = 1;

    /**
     * Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
     * been completed.
     */
    int STATUS_FULLY_PLAYED = 2;
    

이 상수를 선언한 후에는 이 상수를 사용하여 메타데이터 표시기를 표시할 수 있습니다. 사용자가 미디어 탐색 트리를 탐색하는 동안 표시기가 나타나게 하려면 이 상수 중 하나 이상을 포함하는 추가 번들을 만들고 이 번들을 MediaDescription.Builder.setExtras() 메서드로 전달하세요.

다음 코드 스니펫에서는 가사가 음란한 부분 재생 미디어 항목의 표시기를 표시하는 방법을 보여줍니다.

Kotlin

    val extras = Bundle()
    extras.putLong(EXTRA_IS_EXPLICIT, 1)
    extras.putInt(EXTRA_PLAY_COMPLETION_STATE, STATUS_PARTIALLY_PLAYED)
    val description = MediaDescriptionCompat.Builder()
    .setMediaId(/*...*/)
    .setTitle(resources.getString(/*...*/))
    .setExtras(extras)
    .build()
    return MediaBrowserCompat.MediaItem(description, /* flags */)
    

자바

    Bundle extras = new Bundle();
    extras.putLong(EXTRA_IS_EXPLICIT, 1);
    extras.putInt(EXTRA_PLAY_COMPLETION_STATE, STATUS_PARTIALLY_PLAYED);

    MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
      .setMediaId(/*...*/)
      .setTitle(resources.getString(/*...*/))
      .setExtras(extras)
      .build();
    return new MediaBrowserCompat.MediaItem(description, /* flags */);
    

현재 재생되고 있는 미디어 항목의 표시기를 표시하려면 mediaSessionMediaMetadata.Builder() 메서드에 있는 EXTRA_IS_EXPLICIT 또는 EXTRA_IS_DOWNLOADED에 긴 값을 선언하면 됩니다. 재생 뷰에는 EXTRA_PLAY_COMPLETION_STATE 표시기를 표시할 수 없습니다.

다음 코드 스니펫은 재생 보기에 있는 현재 노래의 가사가 음란하고 이 노래가 다운로드된 상태임을 나타내는 방법을 보여줍니다.

Kotlin

    mediaSession.setMetadata(
      MediaMetadata.Builder()
      .putString(
        MediaMetadata.METADATA_KEY_DISPLAY_TITLE, "Song Name")
      .putString(
        MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name")
      .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, albumArtUri.toString())
      .putLong(
        EXTRA_IS_EXPLICIT, EXTRA_METADATA_ENABLED_VALUE)
      .putLong(
        EXTRA_IS_DOWNLOADED, EXTRA_METADATA_ENABLED_VALUE)
      .build())
    

자바

    mediaSession.setMetadata(
        new MediaMetadata.Builder()
            .putString(
                MediaMetadata.METADATA_KEY_DISPLAY_TITLE, "Song Name")
            .putString(
                MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name")
            .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, albumArtUri.toString())
            .putLong(
                EXTRA_IS_EXPLICIT, EXTRA_METADATA_ENABLED_VALUE)
            .putLong(
                EXTRA_IS_DOWNLOADED, EXTRA_METADATA_ENABLED_VALUE)
            .build());
    

사용자가 콘텐츠를 탐색할 수 있도록 앱에서는 사용자가 음성 검색을 수행할 때마다 검색어와 관련된 검색 결과 그룹을 탐색하도록 허용할 수 있습니다. Android Automotive OS와 Android Auto에서는 이 결과가 인터페이스에 '결과 더 보기' 막대로 표시됩니다.

그림 4. 자동차 화면의 결과 더 보기 옵션

탐색 가능한 검색 결과를 표시하려면 상수를 만들고 서비스에서 onGetRoot() 메서드의 추가 번들에 이 상수를 포함해야 합니다.

다음 코드 스니펫에서는 onGetRoot() 메서드에서 지원을 사용 설정하는 방법을 보여줍니다.

Kotlin

    // Bundle extra indicating that onSearch() is supported
    val EXTRA_MEDIA_SEARCH_SUPPORTED = "android.media.browse.SEARCH_SUPPORTED"

    @Nullable
    fun onGetRoot(
        @NonNull clientPackageName: String,
        clientUid: Int,
        @Nullable rootHints: Bundle
    ): BrowserRoot {
        val extras = Bundle()
        extras.putBoolean(EXTRA_MEDIA_SEARCH_SUPPORTED, true)
        return BrowserRoot(ROOT_ID, extras)
    }
    

자바

    public static final String EXTRA_MEDIA_SEARCH_SUPPORTED =
       "android.media.browse.SEARCH_SUPPORTED";

    @Nullable
    @Override
    public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
       @Nullable Bundle rootHints) {
       Bundle extras = new Bundle();
       extras.putBoolean(EXTRA_MEDIA_SEARCH_SUPPORTED, true);
       return new BrowserRoot(ROOT_ID, extras);
    }
    

검색 결과를 제공하려면 미디어 탐색 서비스에서 onSearch() 메서드를 재정의하세요. 사용자가 '결과 더 보기' 지원성을 호출할 때마다 Android Automotive OS와 Android Auto에서는 사용자의 검색어를 이 메서드로 전달합니다. 제목 항목을 사용하여 서비스의 onSearch() 메서드에서 검색 결과를 구조화하면 결과를 더 쉽게 탐색할 수 있습니다. 예를 들어 앱에서 음악을 재생하는 경우 '앨범', '아티스트' 및 '노래'를 기준으로 검색 결과를 구조화할 수 있습니다.

다음 코드 스니펫은 onSearch() 메서드를 간단히 구현한 것입니다.

Kotlin

    fun onSearch(query: String, extras: Bundle) {
      // Detach from results to unblock the caller (if a search is expensive)
      result.detach()
      object:AsyncTask() {
        internal var searchResponse:ArrayList
        internal var succeeded = false
        protected fun doInBackground(vararg params:Void):Void {
          searchResponse = ArrayList()
          if (doSearch(query, extras, searchResponse))
          {
            succeeded = true
          }
          return null
        }
        protected fun onPostExecute(param:Void) {
          if (succeeded)
          {
            // Sending an empty List informs the caller that there were no results.
            result.sendResult(searchResponse)
          }
          else
          {
            // This invokes onError() on the search callback
            result.sendResult(null)
          }
          return null
        }
      }.execute()
    }
    // Populates resultsToFill with search results. Returns true on success or false on error
    private fun doSearch(
        query: String,
        extras: Bundle,
        resultsToFill: ArrayList
    ): Boolean {
      // Implement this method
    }
    

자바

    @Override
    public void onSearch(final String query, final Bundle extras,
                            Result<ArrayList<MediaItem>> result) {

      // Detach from results to unblock the caller (if a search is expensive)
      result.detach();

      new AsyncTask<Void, Void, Void>() {
        ArrayList<MediaItem> searchResponse;
        boolean succeeded = false;
        @Override
        protected Void doInBackground(Void... params) {
          searchResponse = new ArrayList<MediaItem>();
          if (doSearch(query, extras, searchResponse)) {
            succeeded = true;
          }
          return null;
        }

        @Override
        protected void onPostExecute(Void param) {
          if (succeeded) {
           // Sending an empty List informs the caller that there were no results.
           result.sendResult(searchResponse);
          } else {
            // This invokes onError() on the search callback
            result.sendResult(null);
          }
          return null;
        }
      }.execute()
    }

    /** Populates resultsToFill with search results. Returns true on success or false on error */
    private boolean doSearch(String query, Bundle extras, ArrayList<MediaItem> resultsToFill) {
        // Implement this method
    }
    

재생 컨트롤 사용

Android Automotive OS와 Android Auto에서는 서비스의 MediaSessionCompat을 통해 재생 컨트롤 명령어를 전송합니다. 세션을 등록하고 관련된 콜백 메서드를 구현해야 합니다.

미디어 세션 등록

미디어 탐색 서비스의 onCreate() 메서드에서 MediaSessionCompat을 만든 다음 setSessionToken()을 호출하여 미디어 세션을 등록하세요.

다음 코드 스니펫에서는 미디어 세션을 만들고 등록하는 방법을 보여줍니다.

Kotlin

    override fun onCreate() {
        super.onCreate()

        ...
        // Start a new MediaSession
        val session = MediaSessionCompat(this, "session tag").apply {
            // Set a callback object to handle play control requests, which
            // implements MediaSession.Callback
            setCallback(MyMediaSessionCallback())
        }
        sessionToken = session.sessionToken

        ...
    }
    

자바

    public void onCreate() {
        super.onCreate();

        ...
        // Start a new MediaSession
        MediaSessionCompat session = new MediaSessionCompat(this, "session tag");
        setSessionToken(session.getSessionToken());

        // Set a callback object to handle play control requests, which
        // implements MediaSession.Callback
        session.setCallback(new MyMediaSessionCallback());

        ...
    }
    

미디어 세션 개체를 만들 때 재생 컨트롤 요청을 처리하는 데 사용되는 콜백 개체를 설정합니다. 앱의 MediaSessionCompat.Callback 클래스를 구현하여 이 콜백 개체를 만듭니다. 다음 섹션에서는 이 개체를 구현하는 방법에 관해 설명합니다.

재생 명령어 구현

사용자가 앱에서 미디어 항목의 재생을 요청하면 Android Automotive OS와 Android Auto에서는 앱의 미디어 탐색 서비스에서 얻은 앱의 MediaSessionCompat 개체에서 MediaSessionCompat.Callback 클래스를 사용합니다. 사용자가 재생 일시 정지 또는 다음 트랙으로 건너뛰기와 같은 콘텐츠 재생을 컨트롤하려는 경우 Android Automotive OS와 Android Auto에서는 콜백 개체의 메서드 중 하나를 호출합니다.

콘텐츠 재생을 처리하려면 앱에서 추상 MediaSessionCompat.Callback 클래스를 확장하고 앱이 지원하는 메서드를 구현해야 합니다.

앱에서 제공하는 콘텐츠 유형에 타당한 다음 콜백 메서드를 모두 구현해야 합니다.

onPrepare()
미디어 소스가 변경되면 호출됩니다. Android Automotive OS에서도 부팅 직후에 이 메소드를 호출합니다. 미디어 앱에서는 이 메서드를 구현해야 합니다.
onPlay()
사용자가 특정 항목을 선택하지 않고 재생을 선택하면 호출됩니다. 앱에서는 기본 콘텐츠를 재생해야 합니다. onPause()로 재생이 일시중지된 경우 앱에서 재생을 다시 시작해야 합니다.

참조: Android Automotive OS 또는 Android Auto에서 미디어 탐색 서비스에 연결할 때 앱에서 자동으로 음악을 재생해서는 안 됩니다. 자세한 내용은 초기 재생 상태 설정을 참조하세요.

onPlayFromMediaId()
사용자가 특정 항목 재생을 선택하면 호출됩니다. 이 메서드에는 미디어 탐색 서비스가 콘텐츠 계층 구조의 미디어 항목에 할당한 ID가 전달됩니다.
onPlayFromSearch()
사용자가 검색어에서 재생을 선택하면 호출됩니다. 앱에서는 전달된 검색 문자열에 근거하여 적절한 선택을 해야 합니다.
onPause()
사용자가 재생 일시중지를 선택하면 호출됩니다.
onSkipToNext()
사용자가 다음 항목으로 건너뛰기를 선택하면 호출됩니다.
onSkipToPrevious()
사용자가 이전 항목으로 건너뛰기를 선택하면 호출됩니다.
onStop()
사용자가 재생 중지를 선택하면 호출됩니다.

원하는 기능을 제공하려면 앱에서 이러한 메서드를 재정의해야 합니다. 앱에서 지원하지 않는 메서드는 구현할 필요가 없습니다. 예를 들어 앱에서 스포츠 방송과 같은 실시간 스트림을 재생하는 경우 onSkipToNext() 메서드를 구현하는 것은 타당하지 않습니다. 대신에 onSkipToNext()의 기본 구현을 사용할 수 있습니다.

앱에 특별한 로직이 있어야 자동차 스피커를 통해 콘텐츠를 재생할 수 있는 것은 아닙니다. 앱에서 콘텐츠 재생 요청을 수신하면 일반적으로 사용자의 전화 스피커 또는 헤드폰을 통해 콘텐츠를 재생하는 것과 같은 방식으로 오디오를 재생해야 합니다. Android Automotive OS와 Android Auto에서는 오디오 콘텐츠를 자동차 시스템으로 자동 전송하여 자동차 스피커를 통해 재생합니다.

오디오 콘텐츠 재생에 관한 자세한 내용은 미디어 재생, 오디오 재생 관리ExoPlayer를 참조하세요.

표준 재생 작업 설정

Android Automotive OS와 Android Auto는 PlaybackStateCompat 개체에서 사용 설정된 작업에 근거하여 재생 컨트롤을 표시합니다.

기본적으로 앱에서는 다음 작업을 지원해야 합니다.

또한 사용자에게 표시되는 재생 대기열을 만들어야 하는 경우도 있습니다. 이렇게 하려면 setQueue()setQueueTitle() 메서드를 호출하고, ACTION_SKIP_TO_QUEUE_ITEM 작업을 사용 설정하고, 콜백 onSkipToQueueItem()을 정의해야 합니다.

Android Automotive OS와 Android Auto에서는 사용 설정된 각 작업에 대해 버튼을 표시할 뿐 아니라 재생 대기열을 만들기로 한 경우 이 목록도 표시합니다.

사용하지 않는 공간 예약

Android Automotive OS와 Android Auto에서는 ACTION_SKIP_TO_PREVIOUSACTION_SKIP_TO_NEXT의 UI에 공간을 예약합니다. 또한 Android Auto에서는 재생 대기열의 공간을 예약합니다. 앱에서 이러한 기능 중 하나를 지원하지 않는 경우 Android Automotive OS와 Android Auto에서는 이 공간을 이용해 사용자가 만드는 사용자설정 작업을 표시합니다.

이 공간을 사용자설정 작업으로 채우고 싶지 않다면 앱에서 해당 기능을 지원하지 않을 때마다 Android Automotive OS와 Android Auto에서 공백을 비워 둘 수 있도록 이 공간을 예약할 수 있습니다. 이렇게 하려면 예약된 각 함수에 상응하는 상수가 포함된 추가 번들로 setExtras() 메서드를 호출하세요. 공간을 예약하려는 각 상수를 true로 설정합니다.

다음 코드 스니펫에서는 사용하지 않는 공간을 예약하는 데 사용할 수 있는 상수를 보여줍니다.

Kotlin

    // Use these extras to show the transport control buttons for the corresponding actions,
    // even when they are not enabled in the PlaybackState.
    private const val SLOT_RESERVATION_SKIP_TO_NEXT =
            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT"
    private const val SLOT_RESERVATION_SKIP_TO_PREV =
            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS"
    private const val SLOT_RESERVATION_QUEUE =
            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE"
    

자바

    // Use these extras to show the transport control buttons for the corresponding actions,
    // even when they are not enabled in the PlaybackState.
    private static final String SLOT_RESERVATION_SKIP_TO_NEXT =
        "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
    private static final String SLOT_RESERVATION_SKIP_TO_PREV =
        "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
    private static final String SLOT_RESERVATION_QUEUE =
        "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE";
    

초기 PlaybackState 설정

Android Automotive OS와 Android Auto가 미디어 탐색 서비스와 통신할 때 미디어 세션은 PlaybackState를 사용하여 콘텐츠 재생 상태를 전달합니다. Android Automotive OS 또는 Android Auto가 미디어 탐색 서비스에 연결할 때 앱에서 자동으로 음악을 재생해서는 안 됩니다. 대신에 자동차의 상태 또는 사용자 작업에 따라 재생을 재개하거나 시작하려면 Android Automotive OS 및 Android Auto를 사용하세요.

이렇게 하려면 미디어 세션의 초기 PlaybackStateSTATE_STOPPED, STATE_PAUSED, STATE_NONE 또는 STATE_ERROR로 설정하세요.

사용자설정 재생 작업 추가

사용자설정 재생 작업을 추가하면 미디어 앱에서 지원하는 추가 작업을 표시할 수 있습니다. 공간이 있는 경우(예약은 하지 않음) Android는 전송 컨트롤에 사용자설정 작업을 추가합니다. 그렇지 않은 경우에는 사용자설정 작업이 더보기 메뉴에 표시됩니다. 사용자설정 작업은 PlaybackState에 추가되는 순서대로 표시됩니다.

PlaybackStateCompat.Builder 클래스의 addCustomAction() 메서드를 사용하여 이러한 작업을 추가할 수 있습니다.

다음 코드 스니펫에서는 사용자설정 '무선 채널 시작' 작업을 추가하는 방법을 보여줍니다.

Kotlin

    stateBuilder.addCustomAction(
            PlaybackStateCompat.CustomAction.Builder(
                    CUSTOM_ACTION_START_RADIO_FROM_MEDIA,
                    resources.getString(R.string.start_radio_from_media),
                    startRadioFromMediaIcon
            ).run {
                setExtras(customActionExtras)
                build()
            }
    )
    

자바

    stateBuilder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder(
        CUSTOM_ACTION_START_RADIO_FROM_MEDIA,
        resources.getString(R.string.start_radio_from_media), startRadioFromMediaIcon)
        .setExtras(customActionExtras)
        .build());
    

이 메서드에 관한 더 자세한 예는 GitHub의 범용 Android 뮤직 플레이어 샘플 앱에서 setCustomAction() 메서드를 참조하세요.

사용자설정 작업을 만든 후 미디어 세션에서는 onCustomAction() 메서드를 재정의하여 작업에 응답할 수 있습니다.

다음 코드 스니펫에서는 앱이 '무선 채널 시작' 작업에 응답하는 방법을 보여줍니다.

Kotlin

    override fun onCustomAction(action: String, extras: Bundle?) {
        when(action) {
            CUSTOM_ACTION_START_RADIO_FROM_MEDIA -> {
                ...
            }
        }
    }
    

자바

    @Override
    public void onCustomAction(@NonNull String action, Bundle extras) {
        if (CUSTOM_ACTION_START_RADIO_FROM_MEDIA.equals(action)) {
            ...
        }
    }
    

이 메서드에 관한 더 자세한 예는 GitHub의 범용 Android 뮤직 플레이어 샘플 앱에서 onCustomAction 메서드를 참조하세요.

사용자설정 작업 아이콘

개발자가 만드는 각 사용자설정 작업에는 아이콘 리소스가 필요합니다. 자동차의 앱은 다양한 화면 크기와 밀도에서 실행될 수 있으므로 개발자가 제공하는 아이콘은 벡터 드로어블이어야 합니다. 벡터 드로어블을 사용하면 세부정보 손실 없이 자산을 확장할 수 있습니다. 또한 벡터 드로어블을 사용하면 해상도가 낮을 때 가장자리와 모서리를 픽셀 경계에 맞추기도 쉬워집니다.

사용하지 않는 작업에 대체 아이콘 스타일 제공

현재 컨텍스트에서 사용자설정 작업을 사용할 수 없는 경우 사용자설정 작업 아이콘을 작업이 사용 중지 되었음을 나타내는 대체 아이콘으로 바꾸세요.

그림 5. 샘플 오프 스타일 사용자설정 작업 아이콘.

음성 액션 지원

미디어 앱에서는 주의를 분산하는 요소를 최소화하는 안전하고 편리한 환경을 운전자에게 제공할 수 있는 음성 액션을 지원해야 합니다. 예를 들어 앱에서 이미 미디어 항목 한 개를 재생 중인 경우 사용자는 "[항목] 재생해 줘"라고 말하여 자동차의 디스플레이를 보거나 터치하지 않고도 다른 항목을 재생하도록 앱에 지시할 수 있습니다.

음성 액션 지원 선언

다음 코드 스니펫에서는 앱의 manifest 파일에서 음성 액션 지원을 선언하는 방법을 보여줍니다. 이 코드를 Android Automotive OS 모듈의 manifest 파일과 전화 앱의 manifest 파일에 포함해야 합니다.

<activity>
        <intent-filter>
            <action android:name=
                 "android.media.action.MEDIA_PLAY_FROM_SEARCH" />
            <category android:name=
                 "android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>
    

음성 검색어 파싱

사용자가 “[앱 이름]에서 재즈를 재생해 줘” 또는 “[노래 제목] 듣고 싶어”와 같이 특정 미디어 항목을 검색하면 onPlayFromSearch() 콜백 메서드에서는 쿼리 매개변수 및 추가 번들에 있는 음성 검색 결과를 수신합니다.

앱에서는 다음 단계에 따라 음성 검색어를 파싱하고 재생을 시작할 수 있습니다.

  1. 음성 검색에서 반환된 추가 번들 및 검색어 문자열을 사용하여 결과를 필터링합니다.
  2. 이 결과에 근거하여 재생 대기열을 빌드합니다.
  3. 결과 중에서 관련성이 가장 높은 미디어 항목을 재생합니다.

onPlayFromSearch() 메서드는 음성 검색에서 더 자세한 정보가 포함된 추가 매개변수를 받아들입니다. 이러한 추가 매개변수는 앱에서 재생할 오디오 콘텐츠를 찾는 데 도움이 됩니다. 검색 결과에서 이 데이터를 제공할 수 없는 경우 원시 검색어를 파싱하고 쿼리에 근거하여 적절한 트랙을 재생하도록 로직을 구현할 수 있습니다.

Android Automotive OS와 Android Auto에서는 다음과 같은 추가 매개변수를 지원합니다.

다음 코드 스니펫에서는 MediaSession.Callback 구현에서 onPlayFromSearch() 메서드를 재정의하여 음성 검색어를 파싱하고 재생을 시작하는 방법을 보여줍니다.

Kotlin

    override fun onPlayFromSearch(query: String?, extras: Bundle?) {
        if (query.isNullOrEmpty()) {
            // The user provided generic string e.g. 'Play music'
            // Build appropriate playlist queue
        } else {
            // Build a queue based on songs that match "query" or "extras" param
            val mediaFocus: String? = extras?.getString(MediaStore.EXTRA_MEDIA_FOCUS)
            if (mediaFocus == MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE) {
                isArtistFocus = true
                artist = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST)
            } else if (mediaFocus == MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE) {
                isAlbumFocus = true
                album = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM)
            }

            // Implement additional "extras" param filtering
        }

        // Implement your logic to retrieve the queue
        var result: String? = when {
            isArtistFocus -> artist?.also {
                searchMusicByArtist(it)
            }
            isAlbumFocus -> album?.also {
                searchMusicByAlbum(it)
            }
            else -> null
        }
        result = result ?: run {
            // No focus found, search by query for song title
            query?.also {
                searchMusicBySongTitle(it)
            }
        }

        if (result?.isNotEmpty() == true) {
            // Immediately start playing from the beginning of the search results
            // Implement your logic to start playing music
            playMusic(result)
        } else {
            // Handle no queue found. Stop playing if the app
            // is currently playing a song
        }
    }
    

자바

    @Override
    public void onPlayFromSearch(String query, Bundle extras) {
        if (TextUtils.isEmpty(query)) {
            // The user provided generic string e.g. 'Play music'
            // Build appropriate playlist queue
        } else {
            // Build a queue based on songs that match "query" or "extras" param
            String mediaFocus = extras.getString(MediaStore.EXTRA_MEDIA_FOCUS);
            if (TextUtils.equals(mediaFocus,
                    MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE)) {
                isArtistFocus = true;
                artist = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST);
            } else if (TextUtils.equals(mediaFocus,
                    MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE)) {
                isAlbumFocus = true;
                album = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM);
            }

            // Implement additional "extras" param filtering
        }

        // Implement your logic to retrieve the queue
        if (isArtistFocus) {
            result = searchMusicByArtist(artist);
        } else if (isAlbumFocus) {
            result = searchMusicByAlbum(album);
        }

        if (result == null) {
            // No focus found, search by query for song title
            result = searchMusicBySongTitle(query);
        }

        if (result != null && !result.isEmpty()) {
            // Immediately start playing from the beginning of the search results
            // Implement your logic to start playing music
            playMusic(result);
        } else {
            // Handle no queue found. Stop playing if the app
            // is currently playing a song
        }
    }
    

앱에서 오디오 콘텐츠를 재생하기 위해 음성 검색을 구현하는 방법에 관한 더 자세한 예는 범용 미디어 플레이어 샘플을 참조하세요.

빈 쿼리 처리

사용자가 “[앱 이름]에서 음악 재생해 줘”라고 말하면 Android Automotive OS 또는 Android Auto에서는 앱을 시작하고 앱의 onPlayFromSearch() 메서드를 호출하여 오디오를 재생하려고 합니다. 그러나 사용자가 미디어 항목의 이름을 말하지 않았으므로 onPlayFromSearch() 메서드에서는 빈 쿼리 매개변수를 수신합니다. 이 경우 앱은 최신 재생목록 또는 무작위 대기열의 노래와 같은 오디오를 즉시 재생하여 응답해야 합니다.

음성이 사용 설정된 재생 작업 구현

사용자가 운전 중에 미디어 콘텐츠를 청취하는 동안 핸즈프리 환경을 제공하려면 앱에서는 사용자가 음성 액션으로 콘텐츠 재생을 제어할 수 있게 허용해야 합니다. 사용자가 “다음 노래”, “음악 일시중지” 또는 “음악 다시 시작”과 같은 명령어를 말하면 시스템에서는 이에 상응하는 콜백 메서드를 트리거하고 개발자는 이 메서드에서 재생 컨트롤 작업을 구현합니다.

음성이 사용 설정된 재생 작업을 제공하려면 먼저 앱의 MediaSession 개체에서 다음 플래그를 설정하여 하드웨어 컨트롤을 사용하도록 설정하세요.

Kotlin

    session.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
            or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
    )
    

자바

    session.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
        MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
    

플래그를 설정한 후 앱에서 지원하는 재생 컨트롤을 사용하여 콜백 메서드를 구현하세요. Android Automotive OS와 Android Auto에서는 다음과 같은 음성 사용 설정 재생 작업을 지원합니다.

예시 문구 콜백 메서드
"다음 노래" onSkipToNext()
"이전 노래" onSkipToPrevious()
"음악 일시중지" onPause()
"음악 중지" onStop()
"음악 다시 시작" onPlay()

앱에서 음성이 사용 설정된 재생 작업을 구현하는 방법에 관한 더 자세한 예는 범용 미디어 플레이어 샘플을 참조하세요.

Android Automotive OS의 설정 및 로그인 활동 구현

미디어 브라우저 서비스 외에 Android Automotive OS 앱에 차량에 최적화된 설정 및 로그인 활동을 제공할 수도 있습니다. 이러한 활동을 통해 Android Media API에 포함되지 않은 앱 기능을 제공할 수 있습니다.

설정 활동 추가

사용자가 자동차에서 앱의 설정을 구성할 수 있도록 차량에 최적화된 설정 활동을 추가할 수 있습니다. 설정 활동에서는 사용자 계정 로그인 또는 로그아웃, 사용자 계정 전환과 같은 다른 워크플로를 제공할 수도 있습니다.

설정 활동 워크플로

설정 활동에서는 사용자에게 다양한 워크플로를 제공할 수 있습니다. 다음 이미지에서는 사용자가 Android Automotive OS를 사용하여 설정 활동과 상호작용하는 방법을 보여줍니다.

설정 활동의 워크플로

그림 6. 설정 활동의 워크플로를 나타낸 다이어그램

설정 활동 선언

다음 코드 스니펫에 표시된 대로 앱의 manifest 파일에서 설정 활동을 선언해야 합니다.

<application>
        ...
        <activity android:name=".AppSettingsActivity"
                  android:exported="true"
                  android:theme="@style/SettingsActivity"
                  android:label="@string/app_settings_activity_title">
            <intent-filter>
                <action android:name="android.intent.action.APPLICATION_PREFERENCES"/>
            </intent-filter>
        </activity>
        ...
    <application>
    

설정 활동 구현

사용자가 앱을 시작하면 Android Automotive OS에서는 개발자가 선언한 설정 활동을 감지하고 지원성을 표시합니다. 사용자는 자동차의 디스플레이에서 이 지원성을 탭하거나 선택하여 활동으로 이동할 수 있습니다. Android Automotive OS에서는 설정 활동을 시작하라고 앱에 지시하는 ACTION_APPLICATION_PREFERENCES 인텐트를 전송합니다.

로그인 활동 추가

사용자가 앱을 사용하려면 로그인해야 하는 앱인 경우 앱 로그인 및 로그아웃을 처리하는 차량 최적화 로그인 활동을 추가할 수 있습니다. 로그인 및 로그아웃 워크플로를 설정 활동에 추가할 수도 있지만 사용자가 로그인해야만 앱을 사용할 수 있는 경우에는 전용 로그인 활동을 사용해야 합니다.

로그인 활동 워크플로

다음 이미지에서는 사용자가 Android Automotive OS를 사용하여 로그인 활동과 상호작용하는 방법을 보여줍니다.

로그인 활동의 워크플로

그림 7. 로그인 활동의 워크플로를 나타낸 다이어그램

앱 시작 시 로그인 요구

사용자가 로그인 활동을 사용하여 로그인해야 앱을 사용할 수 있게 하려면 미디어 탐색 서비스에서 다음 작업을 완료해야 합니다.

  1. setState() 메서드를 사용해 미디어 세션의 PlaybackStateSTATE_ERROR로 설정합니다. 이는 오류가 해결될 때까지 다른 작업을 할 수 없다고 Android Automotive OS에 알리는 것입니다.
  2. 미디어 세션의 PlaybackState 오류 코드를 ERROR_CODE_AUTHENTICATION_EXPIRED로 설정합니다. 이는 사용자가 인증을 해야 한다는 것을 Android Automotive OS에 알리는 것입니다.
  3. setErrorMessage() 메서드를 사용해 미디어 세션의 PlaybackState 오류 메시지를 설정합니다. 이 오류 메시지는 사용자에게 표시되는 것이므로 사용자의 현재 언어에 맞게 현지화해야 합니다.
  4. setExtras() 메서드를 사용해 미디어 세션의 PlaybackState 추가 매개변수를 설정합니다. 다음 두 키를 포함합니다.

    • android.media.extras.ERROR_RESOLUTION_ACTION_LABEL: 로그인 워크플로를 시작하는 버튼에 표시되는 문자열입니다. 이 문자열은 사용자에게 표시되는 것이므로 사용자의 현재 언어에 맞게 현지화해야 합니다.
    • android.media.extras.ERROR_RESOLUTION_ACTION_INTENT: 사용자가 android.media.extras.ERROR_RESOLUTION_ACTION_LABEL에서 참조하는 버튼을 탭하면 사용자를 로그인 활동으로 안내하는 PendingIntent입니다.

다음 코드 스니펫에서는 앱을 사용하려면 로그인하도록 사용자에게 요구하는 방법을 보여줍니다.

Kotlin

    val signInIntent = Intent(this, SignInActivity::class.java)
    val signInActivityPendingIntent = PendingIntent.getActivity(this, 0,
        signInIntent, 0)
    val extras = Bundle().apply {
        putString(
            "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL",
            "Sign in"
        )
        putParcelable(
            "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT",
            signInActivityPendingIntent
        )
    }

    val playbackState = PlaybackStateCompat.Builder()
            .setState(PlaybackStateCompat.STATE_ERROR, 0, 0f)
            .setErrorMessage(
                PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED,
                "Authentication required"
            )
            .setExtras(extras)
            .build()
    mediaSession.setPlaybackState(playbackState)
    

자바

    Intent signInIntent = new Intent(this, SignInActivity.class);
    PendingIntent signInActivityPendingIntent = PendingIntent.getActivity(this, 0,
        signInIntent, 0);
    Bundle extras = new Bundle();
    extras.putString(
        "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL",
        "Sign in");
    extras.putParcelable(
        "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT",
        signInActivityPendingIntent);

    PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder()
        .setState(PlaybackStateCompat.STATE_ERROR, 0, 0f)
        .setErrorMessage(
                PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED,
                "Authentication required"
        )
        .setExtras(extras)
        .build();
    mediaSession.setPlaybackState(playbackState);
    

사용자가 성공적으로 인증한 후 앱에서는 PlaybackStateSTATE_ERROR가 아닌 상태로 다시 설정하고 활동의 finish() 메서드를 호출하여 사용자를 다시 Android Automotive OS로 이동시켜야 합니다.

로그인 활동 구현

Google에서는 사용자가 자동차에서 앱에 로그인하는 과정을 지원하는 데 사용할 수 있는 다양한 ID 도구를 제공합니다. Firebase 인증과 같은 도구에서는 맞춤설정 인증 환경을 빌드하는 데 도움이 되는 풀스택 도구를 제공합니다. 또한 사용자의 기존 사용자 인증 정보 또는 기타 기술을 활용하여 사용자가 원활하게 로그인할 수 있는 환경을 빌드할 수 있게 지원하는 도구도 있습니다.

다음 도구를 사용하면 이전에 다른 기기에서 로그인한 적이 있는 사용자가 더 쉽게 로그인할 수 있는 환경을 빌드하는 데 도움이 됩니다.

  • Google 로그인: 전화 앱과 같은 다른 기기에 Google 로그인을 이미 구현했다면 Android Automotive OS 앱에도 Google 로그인을 구현하여 기존 Google 로그인 사용자를 지원해야 합니다.
  • Google 자동 완성: 사용자가 다른 Android 기기에서 Google 자동 완성을 선택한 경우에는 사용자 인증 정보가 Google 비밀번호 관리자에 저장됩니다. 그런 다음 사용자가 Android Automotive OS 앱에 로그인하면 Google 자동 완성에서는 관련성이 있는 저장된 사용자 인증 정보를 추천합니다. Google 자동 완성을 사용하면 애플리케이션을 개발할 필요가 없습니다. 그러나 애플리케이션 개발자는 품질을 개선하기 위해 앱을 최적화해야 합니다. Google 자동 완성은 Android Oreo 8.0(API 레벨 26) 이상(Android Automotive OS 포함)을 실행하는 모든 기기에서 지원합니다.

로그인 보호 작업 처리

어떤 앱의 경우 사용자가 익명으로 액세스할 수 있는 작업이 있는 반면, 로그인해야 수행할 수 있는 작업도 있습니다. 예를 들어 사용자가 로그인하지 않아도 앱에서 음악을 재생할 수 있지만 노래를 건너뛰려면 로그인해야 합니다.

이 경우 사용자가 제한된 작업(노래 건너뛰기)을 수행하려 하면 앱에서는 치명적이지 않은 오류를 발행하여 사용자가 인증하도록 제안할 수 있습니다. 시스템에서는 치명적이지 않은 오류를 사용하여 현재 미디어 항목의 재생을 방해하지 않고 사용자에게 메시지를 표시합니다. 치명적이지 않은 오류 처리를 구현하려면 다음 단계를 완료하세요.

  1. 미디어 세션에서 PlaybackStateerrorCodeERROR_CODE_AUTHENTICATION_EXPIRED로 설정합니다. 이는 사용자가 인증을 해야 한다는 것을 Android Automotive OS에 알리는 것입니다.
  2. 미디어 세션에서 PlaybackStatestate를 그대로 유지합니다. STATE_ERROR로 설정해서는 안 됩니다. 이는 오류가 치명적이지 않다는 것을 시스템에 알리는 것입니다.
  3. setExtras() 메서드를 사용해 미디어 세션의 PlaybackState 추가 매개변수를 설정합니다. 다음 두 키를 포함합니다.

    • android.media.extras.ERROR_RESOLUTION_ACTION_LABEL: 로그인 워크플로를 시작하는 버튼에 표시되는 문자열입니다. 이 문자열은 사용자에게 표시되는 것이므로 사용자의 현재 언어에 맞게 현지화해야 합니다.
    • android.media.extras.ERROR_RESOLUTION_ACTION_INTENT: 사용자가 android.media.extras.ERROR_RESOLUTION_ACTION_LABEL에서 참조하는 버튼을 탭하면 사용자를 로그인 활동으로 안내하는 PendingIntent입니다.
  4. 미디어 세션의 나머지 PlaybackState 상태를 그대로 유지합니다. 이렇게 하면 사용자가 로그인할지 여부를 결정하는 동안 현재 미디어 항목을 계속 재생할 수 있습니다.

주의 분산 방지 수단 구현

Android Auto 사용 중에는 사용자의 전화가 자동차의 스피커에 연결되어 있으므로 운전자의 주의가 분산되지 않도록 추가 예방 조치를 취해야 합니다.

자동차 모드 감지

사용자가 의식적으로 재생을 시작하지 않는 한(예: 앱에서 재생을 누름) Android Auto 미디어 앱에서는 자동차 스피커를 통한 오디오 재생을 시작해서는 안 됩니다. 미디어 앱에서 사용자가 예약한 경보가 울리더라도 자동차 스피커를 통한 음악 재생을 시작해서는 안 됩니다. 이 요구사항을 충족하려면 앱에서 오디오를 재생하기 전에 전화가 자동차 모드에 있는지 확인해야 합니다. 앱에서 getCurrentModeType() 메소드를 호출하여 전화가 자동차 모드에 있는지 확인할 수 있습니다.

사용자의 전화가 자동차 모드에 있는 경우 경보를 지원하는 미디어 앱에서는 다음 중 한 가지 작업을 해야 합니다.

  • 경보 사용을 중지합니다.
  • STREAM_ALARM을 통해 경보를 재생하고 전화 화면에 경보를 사용 중지할 수 있는 UI를 제공합니다.

다음 코드 스니펫에서는 앱이 자동차 모드에서 실행 중인지 확인하는 방법을 보여줍니다.

Kotlin

    fun isCarUiMode(c: Context): Boolean {
        val uiModeManager = c.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
        return if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_CAR) {
            LogHelper.d(TAG, "Running in Car mode")
            true
        } else {
            LogHelper.d(TAG, "Running in a non-Car mode")
            false
        }
    }
    

자바

     public static boolean isCarUiMode(Context c) {
          UiModeManager uiModeManager = (UiModeManager) c.getSystemService(Context.UI_MODE_SERVICE);
          if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
                LogHelper.d(TAG, "Running in Car mode");
                return true;
          } else {
              LogHelper.d(TAG, "Running in a non-Car mode");
              return false;
            }
      }
    

미디어 광고 처리

기본적으로 Android Auto에서는 오디오 재생 세션 중에 미디어 메타데이터가 변경되면 알림을 표시합니다. 미디어 앱이 음악 재생에서 광고 실행으로 전환할 때 사용자에게 알림을 표시하는 것은 사용자의 주의를 분산하므로 불필요한 일입니다. 이 경우 Android Auto에서 알림을 표시하지 않게 하려면 다음 코드 스니펫과 같이 미디어 메타데이터 키 android.media.metadata.ADVERTISEMENT1로 설정해야 합니다.

Kotlin

    const val EXTRA_METADATA_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT"
    ...
    override fun onPlayFromMediaId(mediaId: String, extras: Bundle?) {
        MediaMetadataCompat.Builder().apply {
            // ...
            if (isAd(mediaId)) {
                putLong(EXTRA_METADATA_ADVERTISEMENT, 1)
            }
            // ...
            mediaSession.setMetadata(build())
        }
    }
    

자바

    public static final String EXTRA_METADATA_ADVERTISEMENT =
        "android.media.metadata.ADVERTISEMENT";

    @Override
    public void onPlayFromMediaId(String mediaId, Bundle extras) {
        MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
        // ...
        if (isAd(mediaId)) {
            builder.putLong(EXTRA_METADATA_ADVERTISEMENT, 1);
        }
        // ...
        mediaSession.setMetadata(builder.build());
    }
    

일반 오류 처리

앱에 오류가 발생하면 재생 상태를 STATE_ERROR로 설정하고 setErrorMessage() 메서드를 사용해 오류 메시지를 제공해야 합니다. 오류 메시지는 사용자에게 표시되는 것이므로 사용자의 현재 언어에 맞게 현지화해야 합니다. 그러면 Android Automotive OS와 Android Auto에서는 사용자에게 다음과 같은 오류 메시지를 표시할 수 있습니다.

오류 상태에 관한 자세한 내용은 미디어 세션 작업: 상태 및 오류를 참조하세요.

Android Auto 사용자가 오류를 해결하기 위해 전화 앱을 열어야 하는 경우 메시지에서는 이러한 정보를 사용자에게 제공해야 합니다. 예를 들어 오류 메시지에 '로그인하세요' 대신에 '[앱 이름]에 로그인하세요'라고 표시해야 합니다.

기타 리소스