자동차용 미디어 앱 빌드

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. 디자인 안내는 미디어 앱 만들기를 참고하세요.
  3. 이 섹션에 나열된 주요 용어와 개념을 검토합니다.

주요 용어 및 개념

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

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

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

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

차량에 최적화

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에서 앱을 나타내는 데 사용할 수 있는 앱 아이콘을 지정해야 합니다. 두 가지 아이콘 유형이 필요합니다.

  • 런처 아이콘
  • 저작자 표시 아이콘

런처 아이콘

런처 아이콘은 런처 및 아이콘 트레이와 같은 시스템 UI에서 앱을 나타냅니다. 다음과 같은 매니페스트 선언을 통해 모바일 앱의 아이콘을 사용하여 자동차 미디어 앱을 나타내려 한다고 지정할 수 있습니다.

<application
    ...
    android:icon="@mipmap/ic_launcher"
    ...
/>

모바일 앱의 아이콘과는 다른 아이콘을 사용하려면 매니페스트에서 미디어 브라우저 서비스의 <service> 요소에 android:icon 속성을 설정하세요.

<application>
    ...
    <service
        ...
        android:icon="@mipmap/auto_launcher"
        ...
    />
</application>

저작자 표시 아이콘

그림 1. 미디어 카드의 저작자 표시 아이콘

저작자 표시 아이콘은 미디어 카드와 같이 미디어 콘텐츠가 우선하는 위치에 사용됩니다. 알림에 사용되는 작은 아이콘을 재사용해 보세요. 이 아이콘은 단색이어야 합니다. 다음 매니페스트 선언을 통해 앱을 나타내는 데 사용할 아이콘을 지정할 수 있습니다.

<application>
    ...
    <meta-data
        android:name="androidx.car.app.TintableAttributionIcon"
        android:resource="@drawable/ic_status_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 문자열을 불투명 토큰으로 취급합니다. 클라이언트 앱은 하위 메뉴를 탐색하거나 미디어 항목을 재생하려 할 때 이 토큰을 전달합니다. 앱은 이 토큰을 적절한 미디어 항목과 연결하는 일을 담당합니다.

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

루트 메뉴 구조화

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

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

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

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...
}

Java

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 통합 간에 다르다면 이러한 힌트 값에 따라 콘텐츠 계층 구조의 구조에 관한 로직을 브랜칭할 수 있습니다. 예를 들어 재생 가능한 루트 항목을 일반적으로 표시하는 경우 지원되는 플래그 힌트 값으로 인해 대신 탐색 가능한 루트 항목 아래에 중첩할 수 있습니다.

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

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

미디어 아트워크 표시

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

다음 단계에서는 웹 URI에서 아트를 다운로드하여 로컬 URI를 통해 노출하는 방법을 설명합니다. 더 완전한 예는 범용 Android 뮤직 플레이어 샘플 앱에서 openFile() 구현과 주변 메서드를 참고하세요.

  1. 웹 URI에 상응하는 content:// URI를 빌드합니다. 미디어 브라우저 서비스와 미디어 세션은 이 콘텐츠 URI를 Android Auto 및 Android Automotive OS로 전달합니다.

    Kotlin

    fun Uri.asAlbumArtContentURI(): Uri {
      return Uri.Builder()
        .scheme(ContentResolver.SCHEME_CONTENT)
        .authority(CONTENT_PROVIDER_AUTHORITY)
        .appendPath(this.getPath()) // Make sure you trust the URI
        .build()
    }
    

    Java

    public static Uri asAlbumArtContentURI(Uri webUri) {
      return new Uri.Builder()
        .scheme(ContentResolver.SCHEME_CONTENT)
        .authority(CONTENT_PROVIDER_AUTHORITY)
        .appendPath(webUri.getPath()) // Make sure you trust the URI!
        .build();
    }
    
  2. ContentProvider.openFile() 구현에서 상응하는 URI의 파일이 존재하는지 확인합니다. 존재하지 않으면 이미지 파일을 다운로드하고 캐시합니다. 다음 코드 스니펫은 Glide를 사용합니다.

    Kotlin

    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
      val context = this.context ?: return null
      val file = File(context.cacheDir, uri.path)
      if (!file.exists()) {
        val remoteUri = Uri.Builder()
            .scheme("https")
            .authority("my-image-site")
            .appendPath(uri.path)
            .build()
        val cacheFile = Glide.with(context)
            .asFile()
            .load(remoteUri)
            .submit()
            .get(DOWNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
    
        cacheFile.renameTo(file)
        file = cacheFile
      }
      return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
    }
    

    Java

    @Nullable
    @Override
    public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
        throws FileNotFoundException {
      Context context = this.getContext();
      File file = new File(context.getCacheDir(), uri.getPath());
      if (!file.exists()) {
        Uri remoteUri = new Uri.Builder()
            .scheme("https")
            .authority("my-image-site")
            .appendPath(uri.getPath())
            .build();
        File cacheFile = Glide.with(context)
            .asFile()
            .load(remoteUri)
            .submit()
            .get(DOWNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
    
        cacheFile.renameTo(file);
        file = cacheFile;
      }
      return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    }
    

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

콘텐츠 스타일 적용

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

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

목록 항목

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

그리드 항목

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

기본 콘텐츠 스타일 설정

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

다음 추가 항목은 번들에서 키로 사용할 수 있습니다.

키는 다음 정수 상수 값에 매핑하여 이러한 항목의 표시에 영향을 줄 수 있습니다.

  • DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM: 상응하는 항목이 목록 항목으로 표시됩니다.
  • DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM: 상응하는 항목이 그리드 항목으로 표시됩니다.
  • DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM: 상응하는 항목이 '카테고리' 목록 항목으로 표시됩니다. 이는 일반 목록 항목과 같습니다. 단, 항목 아이콘 주위에 여백이 적용되는 점은 다른데 아이콘이 작을 때 더 보기 좋기 때문입니다. 아이콘은 색조 조정이 가능한 벡터 드로어블이어야 합니다. 이 힌트는 탐색 가능한 항목에만 제공될 것으로 예상됩니다.
  • DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM: 상응하는 항목이 '카테고리' 그리드 항목으로 표시됩니다. 이는 일반 그리드 항목과 같습니다. 단, 항목 아이콘 주위에 여백이 적용되는 점은 다른데 아이콘이 작을 때 더 보기 좋기 때문입니다. 아이콘은 색조 조정이 가능한 벡터 드로어블이어야 합니다. 이 힌트는 탐색 가능한 항목에만 제공될 것으로 예상됩니다.

다음 코드 스니펫은 탐색 가능한 항목의 기본 콘텐츠 스타일을 그리드로 설정하고 재생 가능한 항목을 목록으로 설정하는 방법을 보여줍니다.

Kotlin

import androidx.media.utils.MediaConstants

@Nullable
override fun onGetRoot(
    @NonNull clientPackageName: String,
    clientUid: Int,
    @Nullable rootHints: Bundle
): BrowserRoot {
    val extras = Bundle()
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM)
    return BrowserRoot(ROOT_ID, extras)
}

Java

import androidx.media.utils.MediaConstants;

@Nullable
@Override
public BrowserRoot onGetRoot(
    @NonNull String clientPackageName,
    int clientUid,
    @Nullable Bundle rootHints) {
    Bundle extras = new Bundle();
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM);
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM);
    return new BrowserRoot(ROOT_ID, extras);
}

항목별 콘텐츠 스타일 설정

Content Style API를 사용하면 탐색 가능한 미디어 항목의 하위 요소뿐만 아니라 모든 미디어 항목 자체의 기본 콘텐츠 스타일을 재정의할 수 있습니다.

탐색 가능한 미디어 항목의 하위 요소 기본값을 재정의하려면 미디어 항목의 MediaDescription에 추가 항목 번들을 만들고 앞에서 언급된 동일한 힌트를 추가합니다. DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE은 이 항목의 재생 가능한 하위 요소에 적용되고 DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE은 탐색 가능한 하위 요소에 적용됩니다.

특정 미디어 항목 자체(하위 요소가 아님)의 기본값을 재정의하려면 미디어 항목의 MediaDescription에 추가 항목 번들을 만들고 DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM 키를 사용하여 힌트를 추가합니다. 위에서 설명한 같은 값을 사용하여 이 항목의 프레젠테이션을 지정합니다.

다음 코드 스니펫은 항목 자체 및 하위 요소의 기본 콘텐츠 스타일을 모두 재정의하는 탐색 가능한 MediaItem을 만드는 방법을 보여줍니다. 항목 자체를 카테고리 목록 항목으로, 탐색 가능한 하위 요소를 목록 항목으로, 재생 가능한 하위 요소를 그리드 항목으로 스타일을 지정합니다.

Kotlin

import androidx.media.utils.MediaConstants

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(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM)
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM)
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
    mediaDescriptionBuilder.setExtras(extras)
    return MediaBrowser.MediaItem(
        mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)
}

Java

import androidx.media.utils.MediaConstants;

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(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM);
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM);
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM);
    mediaDescriptionBuilder.setExtras(extras);
    return new MediaBrowser.MediaItem(
        mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE);
}

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

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

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

Kotlin

import androidx.media.utils.MediaConstants

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(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
        "Songs")
    mediaDescriptionBuilder.setExtras(extras)
    return MediaBrowser.MediaItem(
        mediaDescriptionBuilder.build(), /* playable or browsable flag*/)
}

Java

import androidx.media.utils.MediaConstants;

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(
       MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
       "Songs");
   mediaDescriptionBuilder.setExtras(extras);
   return new MediaBrowser.MediaItem(
       mediaDescriptionBuilder.build(), /* playable or browsable flag*/);
}

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

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

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

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

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

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

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

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

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

그림 4. 첫 번째 항목의 재생되지 않은 콘텐츠에는 점이 표시되고 두 번째 항목의 부분 재생 콘텐츠에는 진행률 표시줄이 표시된 탐색 뷰

다음 상수는 MediaItem 설명 추가 항목과 MediaMetadata 추가 항목에서 모두 사용할 수 있습니다.

다음 상수는 MediaItem 설명 추가 항목에서만 사용할 수 있습니다.

사용자가 미디어 브라우저 트리를 탐색하는 동안 표시기가 보이게 하려면 이러한 상수 중 하나 이상을 포함하는 추가 번들을 만들어 MediaDescription.Builder.setExtras() 메서드에 전달하면 됩니다.

다음 코드 스니펫은 70% 완료된 선정적인 미디어 항목의 표시기를 표시하는 방법을 보여줍니다.

Kotlin

import androidx.media.utils.MediaConstants

val extras = Bundle()
extras.putLong(
    MediaConstants.METADATA_KEY_IS_EXPLICIT,
    MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
extras.putInt(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED)
extras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.7)
val description =
    MediaDescriptionCompat.Builder()
        .setMediaId(/*...*/)
        .setTitle(resources.getString(/*...*/))
        .setExtras(extras)
        .build()
return MediaBrowserCompat.MediaItem(description, /* flags */)

Java

import androidx.media.utils.MediaConstants;

Bundle extras = new Bundle();
extras.putLong(
    MediaConstants.METADATA_KEY_IS_EXPLICIT,
    MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT);
extras.putInt(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED);
extras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.7);
MediaDescriptionCompat description =
    new MediaDescriptionCompat.Builder()
        .setMediaId(/*...*/)
        .setTitle(resources.getString(/*...*/))
        .setExtras(extras)
        .build();
return new MediaBrowserCompat.MediaItem(description, /* flags */);

현재 재생되고 있는 미디어 항목의 표시기를 표시하려면 mediaSessionMediaMetadataCompat에서 METADATA_KEY_IS_EXPLICIT 또는 EXTRA_DOWNLOAD_STATUSLong 값을 선언하면 됩니다. 재생 뷰에는 DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS 또는 DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE 표시기를 표시할 수 없습니다.

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

Kotlin

import androidx.media.utils.MediaConstants

mediaSession.setMetadata(
    MediaMetadataCompat.Builder()
        .putString(
            MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Song Name")
        .putString(
            MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name")
        .putString(
            MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
            albumArtUri.toString())
        .putLong(
            MediaConstants.METADATA_KEY_IS_EXPLICIT,
            MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
        .putLong(
            MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
            MediaDescriptionCompat.STATUS_DOWNLOADED)
        .build())

Java

import androidx.media.utils.MediaConstants;

mediaSession.setMetadata(
    new MediaMetadataCompat.Builder()
        .putString(
            MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Song Name")
        .putString(
            MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name")
        .putString(
            MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
            albumArtUri.toString())
        .putLong(
            MediaConstants.METADATA_KEY_IS_EXPLICIT,
            MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
        .putLong(
            MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
            MediaDescriptionCompat.STATUS_DOWNLOADED)
        .build());

콘텐츠가 재생되는 동안 탐색 뷰의 진행률 표시줄 업데이트

앞서 설명한 대로 DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE 추가 항목을 사용하여 탐색 뷰에서 부분 재생 콘텐츠의 진행률 표시줄을 표시할 수 있습니다. 그러나 사용자가 Android Auto 또는 Android Automotive OS에서 부분 재생 콘텐츠를 계속 재생하면 시간이 지나면서 이 표시기는 부정확해집니다.

Android Auto 및 Android Automotive OS에서 진행률 표시줄을 최신 상태로 유지하려면 MediaMetadataCompatPlaybackStateCompat에 추가 정보를 제공하여 진행 중인 콘텐츠를 탐색 뷰의 미디어 항목에 연결하면 됩니다. 미디어 항목에 자동으로 업데이트되는 진행률 표시줄을 적용하려면 다음 요구사항이 충족되어야 합니다.

다음 코드 스니펫은 현재 재생 중인 항목이 탐색 뷰의 항목에 연결되어 있다고 표시하는 방법을 보여줍니다.

Kotlin

import androidx.media.utils.MediaConstants

// When the MediaItem is constructed to show in the browse view.
// Suppose the item was 25% complete when the user launched the browse view.
val mediaItemExtras = Bundle()
mediaItemExtras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.25)
val description =
    MediaDescriptionCompat.Builder()
        .setMediaId("my-media-id")
        .setExtras(mediaItemExtras)
        // ...and any other setters.
        .build()
return MediaBrowserCompat.MediaItem(description, /* flags */)

// Elsewhere, when the user has selected MediaItem for playback.
mediaSession.setMetadata(
    MediaMetadataCompat.Builder()
        .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "my-media-id")
        // ...and any other setters.
        .build())

val playbackStateExtras = Bundle()
playbackStateExtras.putString(
    MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID, "my-media-id")
mediaSession.setPlaybackState(
    PlaybackStateCompat.Builder()
        .setExtras(playbackStateExtras)
        // ...and any other setters.
        .build())

Java

import androidx.media.utils.MediaConstants;

// When the MediaItem is constructed to show in the browse view.
// Suppose the item was 25% complete when the user launched the browse view.
Bundle mediaItemExtras = new Bundle();
mediaItemExtras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.25);
MediaDescriptionCompat description =
    new MediaDescriptionCompat.Builder()
        .setMediaId("my-media-id")
        .setExtras(mediaItemExtras)
        // ...and any other setters.
        .build();
return MediaBrowserCompat.MediaItem(description, /* flags */);

// Elsewhere, when the user has selected MediaItem for playback.
mediaSession.setMetadata(
    new MediaMetadataCompat.Builder()
        .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "my-media-id")
        // ...and any other setters.
        .build());

Bundle playbackStateExtras = new Bundle();
playbackStateExtras.putString(
    MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID, "my-media-id");
mediaSession.setPlaybackState(
    new PlaybackStateCompat.Builder()
        .setExtras(playbackStateExtras)
        // ...and any other setters.
        .build());

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

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

탐색 가능한 검색결과를 표시하려면 서비스의 onGetRoot() 메서드 추가 번들에 상수 키 BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED를 포함하여 불리언 true에 매핑합니다.

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

Kotlin

import androidx.media.utils.MediaConstants

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

Java

import androidx.media.utils.MediaConstants;

@Nullable
@Override
public BrowserRoot onGetRoot(
    @NonNull String clientPackageName,
    int clientUid,
    @Nullable Bundle rootHints) {
    Bundle extras = new Bundle();
    extras.putBoolean(
        MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_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.
}

Java

@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.
}

맞춤 탐색 작업

단일 맞춤 탐색 작업

그림 6. 단일 맞춤 탐색 작업

맞춤 탐색 작업을 사용하면 자동차의 미디어 앱에 있는 앱의 MediaItem 객체에 맞춤 아이콘과 라벨을 추가할 수 있고 이러한 작업과의 사용자 상호작용을 처리할 수 있습니다. 이를 통해 '다운로드', '대기열에 추가', '라디오 재생', '즐겨찾기' 또는 '삭제' 작업을 추가하는 등 다양한 방법으로 미디어 앱의 기능을 확장할 수 있습니다.

맞춤 탐색 작업 더보기 메뉴

그림 7. 맞춤 탐색 작업 더보기

OEM에서 표시할 수 있는 것보다 더 많은 맞춤 작업이 있으면 더보기 메뉴가 사용자에게 표시됩니다.

작동 방식

각 맞춤 탐색 작업은 다음으로 정의됩니다.

  • 작업 ID(고유한 문자열 식별자)
  • 작업 라벨(사용자에게 표시되는 텍스트)
  • 작업 아이콘 URI(색조를 조정할 수 있는 벡터 드로어블)

맞춤 탐색 작업 목록은 BrowseRoot의 일부로 전역적으로 정의합니다. 그런 다음 이러한 작업의 하위 집합을 개별 MediaItem.에 연결할 수 있습니다.

사용자가 맞춤 탐색 작업과 상호작용하면 앱은 onCustomAction()에서 콜백을 수신합니다. 그러면 작업을 처리하고 필요한 경우 MediaItem의 작업 목록을 업데이트할 수 있습니다. 이는 '즐겨찾기' 및 '다운로드'와 같은 스테이트풀(Stateful) 작업에 유용합니다. '라디오 재생'과 같이 업데이트가 필요 없는 작업의 경우 작업 목록을 업데이트하지 않아도 됩니다.

탐색 노드 루트의 맞춤 탐색 작업

그림 8. 맞춤 탐색 작업 툴바

맞춤 탐색 작업은 탐색 노드 루트에도 연결할 수 있습니다. 이러한 작업은 기본 툴바 아래의 보조 툴바에 표시됩니다.

맞춤 탐색 작업 구현 방법

다음은 맞춤 탐색 작업을 프로젝트에 추가하는 단계입니다.

  1. MediaBrowserServiceCompat 구현에서 다음 두 메서드를 재정의합니다.
  2. 런타임 시 작업 한도를 파싱합니다.
    • onGetRoot()에서 rootHints BundleBROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT 키를 사용하여 각 MediaItem에 허용되는 최대 작업 수를 가져옵니다. 한도가 0이면 기능이 시스템에서 지원되지 않음을 나타냅니다.
  3. 맞춤 탐색 작업의 전역 목록을 빌드합니다.
    • 각 작업에 대해 다음 키를 사용하여 Bundle 객체를 만듭니다. * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID: 작업 ID * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL: 작업 라벨 * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI: 작업 아이콘 URI * 모든 작업 Bundle 객체를 목록에 추가합니다.
  4. 전역 목록을 BrowseRoot에 추가합니다.
  5. MediaItem 객체에 작업을 추가합니다.
    • DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST 키를 사용하여 MediaDescriptionCompat 추가 항목에 작업 ID 목록을 포함하는 방식으로 개별 MediaItem 객체에 작업을 추가할 수 있습니다. 이 목록은 BrowseRoot에서 정의한 전역 작업 목록의 하위 집합이어야 합니다.
  6. 작업을 처리하고 진행 상황이나 결과를 반환합니다.

다음은 맞춤 탐색 작업을 시작하기 위해 BrowserServiceCompat에서 변경할 수 있는 사항입니다.

BrowserServiceCompat 재정의

MediaBrowserServiceCompat에서 다음 메서드를 재정의해야 합니다.

public void onLoadItem(String itemId, @NonNull Result<MediaBrowserCompat.MediaItem> result)

public void onCustomAction(@NonNull String action, Bundle extras, @NonNull Result<Bundle> result)

작업 한도 파싱

지원되는 맞춤 탐색 작업의 수를 확인해야 합니다.

public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) {
    rootHints.getInt(
            MediaConstants.BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT, 0)
}

맞춤 탐색 작업 빌드

각 작업은 별도의 Bundle로 패키징해야 합니다.

  • 작업 ID
    bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID,
                    "<ACTION_ID>")
    
  • 작업 라벨
    bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL,
                    "<ACTION_LABEL>")
    
  • 작업 아이콘 URI
    bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI,
                    "<ACTION_ICON_URI>")
    

Parceable ArrayList에 맞춤 탐색 작업 추가

모든 맞춤 탐색 작업 Bundle 객체를 ArrayList에 추가합니다.

private ArrayList<Bundle> createCustomActionsList(
                                        CustomBrowseAction browseActions) {
    ArrayList<Bundle> browseActionsBundle = new ArrayList<>();
    for (CustomBrowseAction browseAction : browseActions) {
        Bundle action = new Bundle();
        action.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID,
                browseAction.mId);
        action.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL,
                getString(browseAction.mLabelResId));
        action.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI,
                browseAction.mIcon);
        browseActionsBundle.add(action);
    }
    return browseActionsBundle;
}

탐색 루트에 맞춤 탐색 작업 목록 추가

public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
                             Bundle rootHints) {
    Bundle browserRootExtras = new Bundle();
    browserRootExtras.putParcelableArrayList(
            BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST,
            createCustomActionsList()));
    mRoot = new BrowserRoot(ROOT_ID, browserRootExtras);
    return mRoot;
}

MediaItem에 작업 추가

MediaDescriptionCompat buildDescription (long id, String title, String subtitle,
                String description, Uri iconUri, Uri mediaUri,
                ArrayList<String> browseActionIds) {

    MediaDescriptionCompat.Builder bob = new MediaDescriptionCompat.Builder();
    bob.setMediaId(id);
    bob.setTitle(title);
    bob.setSubtitle(subtitle);
    bob.setDescription(description);
    bob.setIconUri(iconUri);
    bob.setMediaUri(mediaUri);

    Bundle extras = new Bundle();
    extras.putStringArrayList(
          DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST,
          browseActionIds);

    bob.setExtras(extras);
    return bob.build();
}
MediaItem mediaItem = new MediaItem(buildDescription(...), flags);

onCustomAction 결과 빌드

  • 다음과 같이 Bundle extras에서 mediaId를 파싱합니다.
    @Override
    public void onCustomAction(
              @NonNull String action, Bundle extras, @NonNull Result<Bundle> result){
      String mediaId = extras.getString(MediaConstans.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID);
    }
    
  • 비동기 결과의 경우 결과를 분리합니다. result.detach()
  • 결과 번들을 빌드합니다.
    • 사용자에게 전하는 메시지
      mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE,
                mContext.getString(stringRes))
      
    • 항목 업데이트(항목의 작업을 업데이트하는 데 사용)
      mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM, mediaId);
      
    • 재생 뷰를 엽니다.
      //Shows user the PBV without changing the playback state
      mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM, null);
      
    • 탐색 노드 업데이트
      //Change current browse node to mediaId
      mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE, mediaId);
      
  • 오류인 경우 result.sendError(resultBundle).를 호출합니다.
  • 진행 상황이 업데이트되면 result.sendProgressUpdate(resultBundle)를 호출합니다.
  • result.sendResult(resultBundle)를 호출하여 완료합니다.

작업 상태 업데이트

result.sendProgressUpdate(resultBundle) 메서드를 EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM 키와 함께 사용하면 작업의 새로운 상태를 반영하도록 MediaItem을 업데이트할 수 있습니다. 이를 통해 사용자에게 작업의 진행 상황과 결과에 관한 실시간 피드백을 제공할 수 있습니다.

예: 다운로드 작업

다음은 이 기능을 사용하여 세 가지 상태가 있는 다운로드 작업을 구현하는 방법을 보여주는 예입니다.

  1. Download: 작업의 초기 상태입니다. 사용자가 이 작업을 선택하면 'Downloading'으로 바꾸고 sendProgressUpdate를 호출하여 UI를 업데이트할 수 있습니다.
  2. Downloading: 이 상태는 다운로드가 진행 중임을 나타냅니다. 이 상태를 사용하여 진행률 표시줄이나 다른 표시기를 사용자에게 표시할 수 있습니다.
  3. Downloaded: 이 상태는 다운로드가 완료되었음을 나타냅니다. 다운로드가 완료되면 'Downloading'을 'Downloaded'로 바꾸고 EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM 키로 sendResult를 호출하여 항목을 새로고침해야 함을 나타낼 수 있습니다. 또한 EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE 키를 사용하여 사용자에게 성공 메시지를 표시할 수 있습니다.

이 접근 방식을 사용하면 다운로드 프로세스 및 현재 상태에 관한 명확한 피드백을 사용자에게 제공할 수 있습니다. 아이콘을 통해 다운로드 상태를 25%, 50%, 75%로 표시하는 세부정보를 추가할 수 있습니다.

예: 즐겨찾기 작업

또 다른 예는 다음과 같은 두 상태가 있는 즐겨찾기 작업입니다.

  1. Favorite: 이 작업은 사용자의 즐겨찾기 목록에 없는 항목에 표시됩니다. 사용자가 이 작업을 선택하면 'Favorited'로 바꾸고 EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM 키로 sendResult를 호출하여 UI를 업데이트할 수 있습니다.
  2. Favorited: 이 작업은 사용자의 즐겨찾기 목록에 있는 항목에 표시됩니다. 사용자가 이 작업을 선택하면 'Favorite'로 바꾸고 EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM 키로 sendResult를 호출하여 UI를 업데이트할 수 있습니다.

이 접근 방식을 통해 사용자는 즐겨찾기 항목을 명확하고 일관된 방식으로 관리할 수 있습니다.

이러한 예에서는 맞춤 탐색 작업의 유연성을 보여주며 이러한 작업을 사용하여 실시간 피드백으로 다양한 기능을 구현해 자동차의 미디어 앱 사용자 환경을 개선하는 방법을 보여줍니다.

이 기능을 구현하는 전체 예는 TestMediaApp 프로젝트를 참고하세요.

재생 컨트롤 사용 설정

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 that implements MediaSession.Callback
        // to handle play control requests.
        setCallback(MyMediaSessionCallback())
    }
    sessionToken = session.sessionToken
    ...
}

Java

public void onCreate() {
    super.onCreate();
    ...
    // Start a new MediaSession.
    MediaSessionCompat session = new MediaSessionCompat(this, "session tag");
    setSessionToken(session.getSessionToken());

    // Set a callback object that implements MediaSession.Callback
    // to handle play control requests.
    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는 오디오 콘텐츠를 자동차 시스템으로 자동 전송하여 자동차 스피커를 통해 재생합니다.

오디오 콘텐츠 재생에 관한 자세한 내용은 MediaPlayer 개요, 오디오 앱 개요, ExoPlayer 개요를 참고하세요.

표준 재생 작업 설정

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

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

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

사용자에게 표시될 수 있는 재생 목록을 만들 수도 있지만 필수는 아닙니다. 이렇게 하려면 setQueue()setQueueTitle() 메서드를 호출하고 ACTION_SKIP_TO_QUEUE_ITEM 작업을 사용 설정하고 콜백 onSkipToQueueItem()을 정의합니다.

또한 현재 재생 중인 음악을 알려주는 표시기인 지금 재생 중 아이콘에 대한 지원을 추가합니다. 이렇게 하려면 setActiveQueueItemId() 메서드를 호출하고 현재 재생 중인 항목의 ID를 재생목록에 전달합니다. 재생목록이 변경될 때마다 setActiveQueueItemId()를 업데이트해야 합니다.

Android Auto 및 Android Automotive OS는 사용 설정된 각 작업과 재생 목록의 버튼을 표시합니다. 버튼을 클릭하면 시스템은 MediaSessionCompat.Callback에서 상응하는 콜백을 호출합니다.

사용하지 않는 공간 예약

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

이러한 공간을 맞춤 작업으로 채우지 않으려면 이 공간을 예약하여 앱이 상응하는 기능을 지원하지 않을 때마다 Android Auto 및 Android Automotive OS에서 이 공간을 비워두도록 할 수 있습니다. 이렇게 하려면 예약된 각 함수에 상응하는 상수가 포함된 추가 번들로 setExtras() 메서드를 호출하세요. SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXTACTION_SKIP_TO_NEXT에 상응하고 SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREVACTION_SKIP_TO_PREVIOUS에 상응합니다. 이러한 상수를 번들에서 키로 사용하고 그 값에 부울 true를 사용합니다.

초기 PlaybackState 설정

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

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

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

맞춤 재생 작업 추가

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

맞춤 작업을 사용하여 표준 작업과는 다른 동작을 제공합니다. 표준 작업을 대체하거나 복제하는 데 사용하지 마세요.

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

다음 코드 스니펫은 맞춤 '무선 채널 시작' 작업을 추가하는 방법을 보여줍니다.

Kotlin

val customActionExtras = Bundle()
customActionExtras.putInt(
  androidx.media3.session.MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT,
  androidx.media3.session.CommandButton.ICON_RADIO)

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

자바

Bundle customActionExtras = new Bundle();
customActionExtras.putInt(
  androidx.media3.session.MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT,
  androidx.media3.session.CommandButton.ICON_RADIO);

stateBuilder.addCustomAction(
    new PlaybackStateCompat.CustomAction.Builder(
        CUSTOM_ACTION_START_RADIO_FROM_MEDIA,
        resources.getString(R.string.start_radio_from_media),
        startRadioFromMediaIcon) // or R.drawable.media3_icon_radio
    .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 메서드를 참고하세요.

맞춤 작업 아이콘

개발자가 만드는 각 맞춤 작업에는 아이콘이 필요합니다.

해당 아이콘의 설명이 CommandButton.ICON_ 상수 중 하나와 일치하는 경우 맞춤 작업의 추가 항목에 있는 EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT 키에 이 정수 값을 설정해야 합니다. 지원되는 시스템에서는 CustomAction.Builder에 전달된 아이콘 리소스를 재정의하여 시스템 구성요소가 작업 및 기타 재생 작업을 일관된 스타일로 렌더링할 수 있습니다.

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

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

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

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

그림 6. 오프 스타일 맞춤 작업 아이콘 샘플

오디오 형식 표시

현재 재생 중인 미디어가 특수 오디오 형식을 사용한다고 나타내려면 이 기능을 지원하는 자동차에서 렌더링되는 아이콘을 지정하면 됩니다. 현재 재생 중인 미디어 항목의 추가 항목 번들에서 KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URIKEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI를 설정할 수 있습니다(MediaSession.setMetadata()에 전달됨). 두 추가 항목을 모두 설정하여 다양한 레이아웃을 수용해야 합니다.

또한 KEY_IMMERSIVE_AUDIO 추가 항목을 설정하여 자동차 OEM에 이 오디오가 몰입형 오디오임을 알릴 수 있으며 자동차 OEM은 몰입형 콘텐츠를 방해할 수 있는 오디오 효과를 적용할지 결정할 때 매우 주의해야 합니다.

현재 재생 중인 미디어 항목을 구성하여 자막, 설명 또는 둘 다를 다른 미디어 항목으로 연결되도록 할 수 있습니다. 이를 통해 사용자가 관련 항목으로 빠르게 이동할 수 있습니다. 예를 들어 동일한 아티스트의 다른 노래나 팟캐스트의 다른 에피소드 등으로 이동할 수 있습니다. 자동차에서 이 기능을 지원하는 경우 사용자는 링크를 탭하여 해당 콘텐츠를 탐색할 수 있습니다.

링크를 추가하려면 KEY_SUBTITLE_LINK_MEDIA_ID 메타데이터(자막에서 연결) 또는 KEY_DESCRIPTION_LINK_MEDIA_ID(설명에서 연결)를 구성합니다. 자세한 내용은 해당 메타데이터 필드의 참조 문서를 확인하세요.

음성 작업 지원

미디어 앱에서는 주의를 분산하는 요소를 최소화하는 안전하고 편리한 환경을 운전자에게 제공하기 위해 음성 작업을 지원해야 합니다. 예를 들어 앱에서 하나의 미디어 항목을 재생하고 있다면 사용자는 "[노래 제목] 재생"이라고 말하여 자동차 디스플레이를 보거나 터치하지 않고도 다른 노래를 재생하라고 앱에 알려줄 수 있습니다. 사용자는 핸들에 있는 적절한 버튼을 클릭하거나 "Hey Google"이라는 핫워드를 말하여 쿼리를 시작할 수 있습니다.

Android Auto 또는 Android Automotive OS에서 음성 작업을 감지하여 해석하면 이 음성 작업은 onPlayFromSearch()를 통해 앱에 전달됩니다. 이 콜백을 수신하면 앱은 query 문자열과 일치하는 콘텐츠를 찾아 재생을 시작합니다.

사용자는 특히 장르나 아티스트, 앨범, 노래 제목, 라디오 채널, 재생목록 등 다양한 검색어 카테고리를 쿼리에 지정할 수 있습니다. 검색 지원 기능을 구축할 때는 앱에 적합한 모든 카테고리를 고려하세요. Android Auto 또는 Android Automotive OS에서는 지정된 쿼리가 특정 카테고리에 적합하다고 감지하면 extras 매개변수에 추가 항목을 더합니다. 다음 추가 항목을 전송할 수 있습니다.

사용자가 검색어를 지정하지 않으면 Android Auto 또는 Android Automotive OS에서 전송할 수 있는 빈 query 문자열을 고려하세요. 사용자가 "음악 재생해 줘"라고 말하는 경우를 예로 들 수 있습니다. 이 경우 앱은 최근에 재생한 트랙이나 새로 추천된 트랙을 시작할 수 있습니다.

검색을 빠르게 처리할 수 없다면 onPlayFromSearch()에서 차단하지 마세요. 대신 재생 상태를 STATE_CONNECTING으로 설정하고 비동기 스레드에서 검색을 실행하세요.

재생이 시작되면 미디어 세션의 재생목록을 관련 콘텐츠로 채우는 것이 좋습니다. 예를 들어 사용자가 앨범을 재생하도록 요청하는 경우 앱은 앨범의 트랙 목록으로 재생목록을 채울 수 있습니다. 사용자가 쿼리와 일치하는 다른 트랙을 선택할 수 있도록 탐색 가능한 검색결과 지원을 구현하는 것도 좋습니다.

"재생해 줘" 쿼리 외에도 Android Auto 및 Android Automotive OS에서는 "음악 일시중지해 줘", "다음 노래"와 같은 음성 쿼리를 인식하여 재생을 제어하고 이러한 명령어를 onPause(), onSkipToNext()와 같은 적절한 미디어 세션 콜백과 일치시킵니다.

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

주의 분산 방지 수단 구현

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

자동차에서 알람 억제

Android Auto 미디어 앱은 사용자가 재생 버튼을 누르는 등 재생을 시작하지 않는 한 자동차 스피커를 통한 오디오 재생을 시작하면 안 됩니다. 미디어 앱에서 사용자가 예약한 알람이 울리더라도 자동차 스피커를 통해 음악 재생이 시작되면 안 됩니다.

이 요구사항을 충족하려면 앱에서 오디오를 재생하기 전에 CarConnection을 신호로 사용하면 됩니다. 앱은 자동차 연결 유형LiveData를 관찰하고 CONNECTION_TYPE_PROJECTION과 같은지 확인하여 전화가 자동차 화면에 투영되는지 확인할 수 있습니다.

사용자의 전화가 투영되는 경우 알람을 지원하는 미디어 앱에서는 다음 중 하나를 실행해야 합니다.

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

미디어 광고 처리

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

Kotlin

import androidx.media.utils.MediaConstants

override fun onPlayFromMediaId(mediaId: String, extras: Bundle?) {
    MediaMetadataCompat.Builder().apply {
        if (isAd(mediaId)) {
            putLong(
                MediaConstants.METADATA_KEY_IS_ADVERTISEMENT,
                MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
        }
        // ...add any other properties you normally would.
        mediaSession.setMetadata(build())
    }
}

Java

import androidx.media.utils.MediaConstants;

@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
    MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
    if (isAd(mediaId)) {
        builder.putLong(
            MediaConstants.METADATA_KEY_IS_ADVERTISEMENT,
            MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT);
    }
    // ...add any other properties you normally would.
    mediaSession.setMetadata(builder.build());
}

일반 오류 처리

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

예를 들어 사용자의 현재 리전에서 콘텐츠를 사용할 수 없는 경우 오류 메시지를 설정할 때 ERROR_CODE_NOT_AVAILABLE_IN_REGION 오류 코드를 사용할 수 있습니다.

Kotlin

mediaSession.setPlaybackState(
    PlaybackStateCompat.Builder()
        .setState(PlaybackStateCompat.STATE_ERROR)
        .setErrorMessage(PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION, getString(R.string.error_unsupported_region))
        // ...and any other setters.
        .build())

Java

mediaSession.setPlaybackState(
    new PlaybackStateCompat.Builder()
        .setState(PlaybackStateCompat.STATE_ERROR)
        .setErrorMessage(PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION, getString(R.string.error_unsupported_region))
        // ...and any other setters.
        .build());

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

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

기타 리소스