자동차용 미디어 앱 빌드

Android Auto와 Android Automotive OS를 사용하면 미디어 앱 콘텐츠를 차에 탄 사용자에게 제공할 수 있습니다 자동차용 미디어 앱에서는 Android Auto와 Android Automotive OS(또는 미디어 브라우저가 있는 다른 앱)가 콘텐츠를 검색하고 표시할 수 있도록 미디어 브라우저 서비스를 제공해야 합니다.

이 가이드에서는 개발자에게 이미 휴대전화에서 오디오를 재생하는 미디어 앱이 있고 미디어 앱이 Android 미디어 앱 아키텍처를 준수한다고 가정합니다.

이 가이드에서는 앱이 Android Auto 또는 Android Automotive OS에서 작동하는 데 필요한 MediaBrowserServiceMediaSession의 필수 구성요소를 설명합니다. 핵심 미디어 인프라를 완료하면 Android Auto 지원Android Automotive OS 지원을 미디어 앱에 추가할 수 있습니다.

시작하기 전에

  1. Android 미디어 API 문서를 검토합니다.
  2. Android Automotive OS 앱 디자인 가이드라인Android Auto 앱 디자인 가이드라인을 검토합니다.
  3. 이 섹션에 나열된 주요 용어와 개념을 검토합니다.

주요 용어 및 개념

미디어 브라우저 서비스
MediaBrowserServiceCompat API를 준수하는 미디어 앱에서 구현한 Android 서비스입니다. 앱에서 이 서비스를 사용하여 콘텐츠를 노출합니다.
미디어 브라우저
미디어 앱에서 미디어 브라우저 서비스를 검색하고 콘텐츠를 표시하는 데 사용하는 API입니다. Android Auto 및 Android Automotive OS에서는 미디어 브라우저를 사용하여 앱의 미디어 브라우저 서비스를 찾습니다.
미디어 항목

미디어 브라우저는 MediaItem 객체 트리에 콘텐츠를 구성합니다. 미디어 항목에는 다음 두 플래그 중 하나 또는 둘 다가 있을 수 있습니다.

  • 재생 가능: 이 플래그는 항목이 콘텐츠 트리의 리프인 것을 나타냅니다. 항목은 앨범의 노래, 오디오북의 챕터 또는 팟캐스트의 에피소드와 같은 단일 사운드 스트림을 나타냅니다.
  • 탐색 가능: 이 플래그는 항목이 콘텐츠 트리의 노드이고 하위 요소가 있다는 것을 나타냅니다. 예를 들어 항목은 앨범을 나타내고 하위 요소는 앨범의 노래입니다.

탐색 가능하고 재생 가능한 미디어 항목은 재생목록 역할을 합니다. 항목 자체를 선택하여 모든 하위 요소를 재생하거나 하위 요소를 탐색할 수 있습니다.

차량에 최적화

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

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

Android Auto는 미디어 브라우저 서비스의 정보를 사용하여 차량에 최적화된 자체 인터페이스를 작성하므로 Android Auto용 활동을 디자인할 필요가 없습니다.

앱의 매니페스트 파일 구성

미디어 브라우저 서비스를 만들려면 먼저 앱의 매니페스트 파일을 구성해야 합니다.

미디어 브라우저 서비스 선언

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

다음 코드 스니펫은 매니페스트에서 미디어 브라우저 서비스를 선언하는 방법을 보여줍니다. 이 코드를 Android Automotive OS 모듈의 매니페스트 파일과 전화 앱의 매니페스트 파일에 포함해야 합니다.

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

앱 아이콘 지정

Android Auto 및 Android Automotive OS가 시스템 UI에서 앱을 나타내는 데 사용할 수 있는 앱 아이콘을 지정해야 합니다.

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

<!--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 Auto와 Android Automotive OS에서 서비스를 사용하여 다음 작업을 할 수 있습니다.

  • 사용자에게 메뉴를 표시하기 위해 앱의 콘텐츠 계층 구조 탐색
  • 오디오 재생을 제어하기 위해 앱에 있는 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 및 Android Automotive OS는 앱의 미디어 브라우저 서비스를 호출하여 사용 가능한 콘텐츠를 찾습니다. 이 작업을 지원하려면 미디어 브라우저 서비스에서 onGetRoot()onLoadChildren()이라는 두 가지 메서드를 구현해야 합니다.

onGetRoot 구현

서비스의 onGetRoot() 메서드에서는 콘텐츠 계층 구조의 루트 노드에 관한 정보를 반환합니다. Android Auto 및 Android Automotive OS에서는 이 루트 노드를 사용하여 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 Auto, Android Automotive OS)에 콘텐츠 액세스 권한을 제공하려면 서비스에서는 이러한 시스템 앱이 onGetRoot() 메서드를 호출할 때 항상 null이 아닌 BrowserRoot를 반환해야 합니다. Android Automotive OS 시스템 앱의 서명은 자동차의 제조업체와 모델에 따라 다를 수 있으므로 Android Automotive OS를 강력하게 지원하도록 모든 시스템 앱의 연결을 허용해야 합니다.

다음 코드 스니펫은 호출 패키지가 시스템 앱인지 여부를 서비스에서 검사하는 방법을 보여줍니다.

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() 메서드의 패키지 유효성 검사를 구현하는 방법에 관한 자세한 예는 이 클래스를 참고하세요.

시스템 앱을 허용하는 것 외에도 Google 어시스턴트가 MediaBrowserService에 연결하도록 허용해야 합니다. Google 어시스턴트에는 휴대전화(Android Auto 포함)용과 Android Automotive OS용으로 별도의 패키지 이름이 있습니다.

onLoadChildren() 구현

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

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

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

다음 코드 스니펫은 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() 메서드를 참고하세요.

루트 메뉴 구조화

그림 1. 탐색 탭으로 표시된 루트 콘텐츠

Android Auto와 Android Automotive OS에는 루트 메뉴 구조에 관한 특정 제약 조건이 있습니다. 이러한 제약 조건은 루트 힌트를 통해 MediaBrowserService에 전달되어 onGetRoot()로 전달된 Bundle 인수를 통해 읽힐 수 있습니다. 이러한 힌트를 따르면 시스템에서 루트 콘텐츠를 탐색 탭으로 최적으로 표시할 수 있습니다. 그러나 Android Auto에서는 기본적으로 콘텐츠를 이처럼 표시하지 않습니다. 아래 선택 기간을 참고하세요. 이러한 힌트를 따르지 않으면 일부 루트 콘텐츠가 삭제되거나 시스템의 검색 가능성이 작아질 수 있습니다. 다음 두 가지 힌트가 전송됩니다.

  1. 루트 하위 요소 수 제한: 대부분의 경우 이 값은 4로 예상됩니다. 즉, 탭을 4개 이상 표시할 수 없습니다.
  2. 루트 하위 요소에서 지원되는 플래그: 이 값은 MediaItem#FLAG_BROWSABLE로 예상됩니다. 즉, 탐색 가능한 항목만 탭으로 표시할 수 있고 재생 가능한 항목은 표시할 수 없습니다.

다음 코드를 사용하여 관련 루트 힌트를 읽습니다.

Kotlin

import androidx.media.utils.MediaConstants

// Later, in your MediaBrowserServiceCompat
override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle
): BrowserRoot {

  val maximumRootChildLimit = rootHints.getInt(
      MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
      /* defaultValue= */ 4)
  val supportedRootChildFlags = rootHints.getInt(
      MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
      /* defaultValue= */ MediaItem.FLAG_BROWSABLE)

  // rest of method..
}

자바

import androidx.media.utils.MediaConstants;

// Later, in your MediaBrowserServiceCompat
@Override
public BrowserRoot onGetRoot(
    String clientPackageName, int clientUid, Bundle rootHints) {

    int maximumRootChildLimit = rootHints.getInt(
        MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
        /* defaultValue= */ 4);
    int supportedRootChildFlags = rootHints.getInt(
        MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
        /* defaultValue= */ MediaItem.FLAG_BROWSABLE);

    // rest of method...
}

특히 계층 구조가 Android Auto와 Android Automotive OS 외부의 MediaBrowser 통합 간에 다르다면 이러한 힌트 값에 따라 콘텐츠 계층 구조의 구조에 관한 로직을 분기할 수 있습니다. 예를 들어 재생 가능한 루트 항목을 일반적으로 표시하는 경우 지원되는 플래그 힌트 값으로 인해 대신 탐색 가능한 루트 항목 아래에 중첩할 수 있습니다.

루트 힌트 외에도 탭이 최적으로 렌더링되도록 하기 위해 따라야 할 추가 가이드라인이 있습니다.

  1. 각 탭 항목에 흑백(흰색 권장) 아이콘을 제공합니다.
  2. 각 탭 항목에 짧지만 의미 있는 라벨을 제공합니다. 라벨을 짧게 유지하면 문자열이 잘릴 가능성이 줄어듭니다.

Android Auto에서 탭 선택

탐색 탭 지원이 Android Auto에 처음 도입되었습니다. 이전에는 Android Auto에서 루트 콘텐츠를 대신 목록으로 표시했습니다. 탭을 사용하면 사용자의 콘텐츠 액세스 동작이 크게 변경되고 기본적으로 탭이 일부 기존 앱(예: 재생 가능한 루트 항목 또는 여러 탐색 가능한 루트 항목이 포함된 앱)에 최적이 아닐 수 있으므로 Android Auto에서는 모든 앱이 탭이 적용된 인터페이스로 전환되는 2021년 5월까지 개발자들이 탭을 선택할 수 있도록 허용합니다. 그전까지 루트 콘텐츠는 기본적으로 계속 목록으로 표시되지만 onGetRoot() 메서드에서 서비스의 BrowserRoot Bundle에 추가 항목을 전송하여 사용자를 위한 탭을 사용 설정할 수 있습니다.

다음 코드 스니펫은 탭을 사용 설정하는 방법을 보여줍니다.

Kotlin

val TABS_OPT_IN_HINT = "android.media.browse.AUTO_TABS_OPT_IN_HINT"

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

자바

static final String TABS_OPT_IN_HINT =
  "android.media.browse.AUTO_TABS_OPT_IN_HINT";

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

Android Auto 지원 앱을 유지하는 개발자는 먼저 팀에서 탭을 테스트한 후 사용자를 위해 이 기능을 사용 설정하는 것이 좋습니다.

미디어 아트워크 표시

미디어 아트워크 항목은 ContentResolver.SCHEME_CONTENT 또는 ContentResolver.SCHEME_ANDROID_RESOURCE를 사용하여 로컬 URI로 전달해야 합니다. 이 로컬 URI는 애플리케이션 리소스의 비트맵이나 벡터 드로어블로 확인되어야 합니다. 콘텐츠 계층 구조의 항목을 나타내는 MediaDescription 객체의 경우 setIconUri()를 통해 URI를 전달합니다. 현재 재생되는 항목을 나타내는 MediaMetadata 객체의 경우 다음 키 중 하나를 사용하여 putString()을 통해 URI를 전달합니다.

다음은 웹 URI에서 아트를 다운로드하여 로컬 URI를 통해 노출하는 방법을 보여주는 예입니다.

  1. 이미지 파일을 다운로드합니다. 다음 코드 스니펫에서는 Glide를 사용합니다.

    Kotlin

    val artFile = Glide.with(context)
      .downloadOnly()
      .load(imageUri)
      .submit()
      .get()
    

    자바

    File artFile = Glide.with(context)
      .downloadOnly()
      .load(imageUri)
      .submit()
      .get();
    
  2. 파일의 content:// URI를 빌드합니다. 미디어 브라우저 서비스와 미디어 세션은 이 URI를 Android Auto 및 Android Automotive OS로 전달해야 합니다.

    Kotlin

    fun File.asAlbumArtContentURI(): Uri {
      return Uri.Builder()
        .scheme(ContentResolver.SCHEME_CONTENT)
        .authority(AUTHORITY)
        .appendPath(this.path)
        .build()
    }
    

    자바

    public static Uri asAlbumArtContentURI(File file) {
      return Uri.Builder()
        .scheme(ContentResolver.SCHEME_CONTENT)
        .authority(AUTHORITY)
        .appendPath(file.getPath())
        .build();
    }
    
  3. ContentProvider.openFile() 메서드에서 파일에 액세스할 수 있도록 합니다.

    Kotlin

    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
      val context = this.context ?: return null
      val file = File(uri.path)
      if (!file.exists()) {
        throw FileNotFoundException(uri.path)
      }
      // Only allow access to files under cache path
      val cachePath = context.cacheDir.path
      if (!file.path.startsWith(cachePath)) {
        throw FileNotFoundException()
      }
      return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
    }
    

    자바

    @Nullable
    @Override
    public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
        throws FileNotFoundException {
      Context context = this.getContext();
      File file = new File(uri.getPath());
      if (!file.exists()) {
        throw new FileNotFoundException(uri.getPath());
      }
      // Only allow access to files under cache path
      String cachePath = context.getCacheDir().getPath();
      if (!file.getPath().startsWith(cachePath)) {
        throw new FileNotFoundException();
      }
      return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    }
    

콘텐츠 제공자에 관한 자세한 내용은 콘텐츠 제공자 만들기를 참고하세요.

콘텐츠 스타일 적용

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

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

목록 항목

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

그리드 항목

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

기본 콘텐츠 스타일 설정

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

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

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

/**
 * Specifies that the corresponding items should be presented as lists and are
 * represented by a vector icon. This adds a small margin around the icons
 * instead of filling the full available area.
 */
val CONTENT_STYLE_CATEGORY_LIST_ITEM_HINT_VALUE = 3

/**
 * Specifies that the corresponding items should be presented as grids and are
 * represented by a vector icon. This adds a small margin around the icons
 * instead of filling the full available area.
 */
val CONTENT_STYLE_CATEGORY_GRID_ITEM_HINT_VALUE = 4

자바

/* 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;

/*
 * Specifies that the corresponding items should be presented as lists and are
 * represented by a vector icon. This adds a small margin around the icons
 * instead of filling the full available area.
 */
public static final int CONTENT_STYLE_CATEGORY_LIST_ITEM_HINT_VALUE = 3

/*
 * Specifies that the corresponding items should be presented as grids and are
 * represented by a vector icon. This adds a small margin around the icons
 * instead of filling the full available area.
 */
public static final int CONTENT_STYLE_CATEGORY_GRID_ITEM_HINT_VALUE = 4

이러한 상수를 선언한 후 서비스에 있는 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);
}

항목별 콘텐츠 스타일 설정

Content Style 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)
    mediaDescriptionBuilder.setExtras(extras)
    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);
   mediaDescriptionBuilder.setExtras(extras);
   return new MediaBrowser.MediaItem(
       mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE);
}

제목 힌트를 사용하여 항목 그룹화

관련 미디어 항목을 함께 그룹화하려면 항목별 힌트를 사용하세요. 그룹의 모든 미디어 항목은 MediaDescription에서 동일한 문자열을 사용하는 추가 번들을 선언해야 합니다. 이 문자열은 그룹의 제목으로 사용되며 현지화할 수 있습니다.

다음 코드 스니펫은 하위 그룹 제목이 "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")
    mediaDescriptionBuilder.setExtras(extras)
    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");
   mediaDescriptionBuilder.setExtras(extras);
   return new MediaBrowser.MediaItem(
       mediaDescriptionBuilder.build(), /* playable or browsable flag*/);
}

앱은 함께 그룹화하려는 모든 미디어 항목을 연속된 블록으로 전달해야 합니다. 예를 들어 '노래'와 '앨범'이라는 두 미디어 항목 그룹을 같은 순서로 표시하려고 하며 앱이 다음 순서로 다섯 가지 미디어 항목을 전달했다고 가정해보겠습니다.

  1. extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")이 있는 미디어 항목 A
  2. extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")이 있는 미디어 항목 B
  3. extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")이 있는 미디어 항목 C
  4. extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")이 있는 미디어 항목 D
  5. extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")이 있는 미디어 항목 E

'노래' 그룹과 '앨범' 그룹의 미디어 항목이 연속된 블록으로 함께 보관되지 않으므로 Android Auto 및 Android Automotive OS는 대신 다음 4개 그룹으로 이를 해석합니다.

  • 미디어 항목 A가 포함된 '노래' 그룹 1
  • 미디어 항목 B가 포함된 '앨범' 그룹 2
  • 미디어 항목 C와 D가 포함된 '노래' 그룹 3
  • 미디어 항목 E가 포함된 '앨범' 그룹 4

이러한 항목을 두 그룹으로 표시하려면 앱에서는 대신 다음 순서로 앱을 전달합니다.

  1. extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")이 있는 미디어 항목 A
  2. extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")이 있는 미디어 항목 C
  3. extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")이 있는 미디어 항목 D
  4. extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")이 있는 미디어 항목 B
  5. extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")이 있는 미디어 항목 E

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

그림 2. 노래와 아티스트를 식별하는 메타데이터와 선정적인 콘텐츠를 나타내는 아이콘이 있는 재생 뷰

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

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

Kotlin

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

/**
* Bundle extra indicating the download status of a media item.
* Same as MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS.
*/
var EXTRA_DOWNLOAD_STATUS = "android.media.extra.DOWNLOAD_STATUS"

/**
* 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 the download status of a media item.
 * Same as MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS.
 */
String EXTRA_DOWNLOAD_STATUS = "android.media.extra.DOWNLOAD_STATUS";

/**
 * 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_DOWNLOAD_STATUSLong 값을 선언하면 됩니다. 재생 뷰에는 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, 1)
  .putLong(
    EXTRA_DOWNLOAD_STATUS, MediaDescriptionCompat.STATUS_DOWNLOADED)
  .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, 1)
        .putLong(
            EXTRA_DOWNLOAD_STATUS, MediaDescriptionCompat.STATUS_DOWNLOADED)
        .build());

그림 3. 사용자의 음성 검색과 관련된 미디어 항목을 볼 수 있는 'Google 검색결과' 옵션이 포함된 재생 뷰

앱은 사용자가 검색어를 시작할 때 사용자에게 표시되는 문맥 검색결과를 제공할 수 있습니다. Android Auto 및 Android Automotive OS에서는 이러한 결과를 검색어 인터페이스나 세션 초반의 검색어에 기반한 어포던스를 통해 표시합니다.

탐색 가능한 검색결과를 표시하려면 상수를 만들고 서비스에 있는 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 Auto 및 Android Automotive OS에서는 사용자가 검색어 인터페이스나 'Google 검색결과' 어포던스를 호출할 때마다 사용자의 검색어를 이 메서드로 전달합니다. 제목 항목을 사용해 서비스의 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<List<MediaItem>> result) {

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

  new AsyncTask<Void, Void, Void>() {
    List<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);
      }
    }
  }.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 Auto 및 Android Automotive OS는 서비스의 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 Auto 및 Android Automotive OS는 콜백 객체의 메서드 중 하나를 호출합니다.

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

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

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

참고: Android Automotive OS 또는 Android Auto가 미디어 브라우저 서비스에 연결될 때 앱에서 자동으로 음악 재생을 시작해서는 안 됩니다. 자세한 내용은 초기 재생 상태 설정을 참고하세요.

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

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

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

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

표준 재생 작업 설정

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

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

앱에서는 앱 콘텐츠와 관련이 있다면 다음 작업을 추가로 지원할 수 있습니다.

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

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

사용하지 않는 공간 예약

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

이러한 공간을 맞춤 작업으로 채우지 않으려면 이 공간을 예약하여 앱이 상응하는 기능을 지원하지 않을 때마다 Android Auto 및 Android Automotive OS에서 이 공간을 비워두도록 할 수 있습니다. 이렇게 하려면 예약된 각 함수에 상응하는 상수가 포함된 추가 번들로 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 PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT =
        "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT"
private const val PLAYBACK_SLOT_RESERVATION_SKIP_TO_PREV =
        "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS"
private const val PLAYBACK_SLOT_RESERVATION_QUEUE =
        "android.media.playback.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 PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT =
    "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
private static final String PLAYBACK_SLOT_RESERVATION_SKIP_TO_PREV =
    "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
private static final String PLAYBACK_SLOT_RESERVATION_QUEUE =
    "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE";

초기 PlaybackState 설정

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

이 작업을 실행하려면 미디어 세션의 초기 PlaybackStateSTATE_STOPPED, STATE_PAUSED, STATE_NONE 또는 STATE_ERROR로 설정합니다.

Android Auto와 Android Automotive OS 내의 미디어 세션은 운전하는 동안에만 지속되므로 사용자는 이러한 세션을 자주 시작하거나 중지합니다. 운전 사이의 원활한 환경을 촉진하기 위해 사용자의 이전 세션 상태(예: 마지막으로 재생한 미디어 항목, PlaybackState, 대기열)를 추적하여 미디어 앱에서 재개 요청을 수신할 때 사용자가 자동으로 중단된 부분부터 시작할 수 있도록 합니다.

맞춤 재생 작업 추가

맞춤 재생 작업을 추가하면 미디어 앱에서 지원하는 추가 작업을 표시할 수 있습니다. 공간이 있는 경우(예약은 하지 않음) 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 메서드를 참고하세요.

맞춤 작업 아이콘

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

맞춤 작업이 스테이트풀(Stateful)이면(예: 재생 설정을 사용 설정 또는 사용 중지) 여러 상태에 다양한 아이콘을 제공하여 사용자가 작업을 선택할 때 변경사항을 시각적으로 인지할 수 있도록 합니다.

사용 중지된 작업에 대체 아이콘 스타일 제공

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

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

음성 액션 지원

미디어 앱에서는 주의를 분산하는 요소를 최소화하는 안전하고 편리한 환경을 운전자에게 제공할 수 있는 음성 작업을 지원해야 합니다. 예를 들어 앱에서 이미 하나의 미디어 항목을 재생하고 있다면 사용자는 "보헤미안 랩소디 재생"이라고 말하여 자동차 디스플레이를 보거나 터치하지 않고도 다른 노래를 재생하라고 앱에 알려줄 수 있습니다.

앱에서 음성 지원 재생 작업을 구현하는 방법에 관한 자세한 예는 Google 어시스턴트 및 미디어 앱을 참고하세요.

주의 분산 방지 수단 구현

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 Auto와 Android Automotive OS에서 사용자에게 오류 메시지를 표시할 수 있습니다.

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

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

기타 자료