자동차용 미디어 앱 빌드

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

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 인수를 통해 읽힐 수 있습니다. 이러한 힌트를 따르면 시스템이 루트 콘텐츠를 최적의 탐색 탭으로 표시할 수 있습니다. 이러한 힌트를 따르지 않으면 일부 루트 콘텐츠가 삭제되거나 시스템에서 검색되지 않는 경우가 있을 수 있습니다. 다음 두 가지 힌트가 전송됩니다.

  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. 각 탭 항목에 짧지만 의미 있는 라벨을 제공합니다. 라벨을 짧게 유지하면 문자열이 잘릴 가능성이 줄어듭니다.

미디어 아트워크 표시

미디어 아트워크 항목은 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()
    }
    

    자바

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

    자바

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

자바

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_BROWSABLE은 이 항목의 재생 가능한 하위 요소에 적용되고 DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE은 탐색 가능한 하위 요소에 적용됩니다.

특정 미디어 항목 자체(하위 요소가 아님)의 기본값을 재정의하려면 미디어 항목의 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)
}

자바

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*/)
}

자바

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는 미디어 세션의 메타데이터를 읽고 특정 상수를 찾아 어떤 표시기를 표시할지 판단합니다.

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

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

다음 상수는 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 */)

자바

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())

자바

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())

자바

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());

그림 4. 사용자의 음성 검색과 관련된 미디어 항목을 볼 수 있는 '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)
}

자바

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
}

자바

@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는 사용 설정된 각 작업의 버튼을 표시할 뿐 아니라 개발자가 재생 대기열을 만드는 경우 재생 대기열도 표시합니다. 버튼을 클릭하면 시스템은 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

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)이면(예: 재생 설정을 사용 설정 또는 사용 중지) 여러 상태에 다양한 아이콘을 제공하여 사용자가 작업을 선택할 때 변경사항을 시각적으로 인지할 수 있도록 합니다.

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

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

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

음성 작업 지원

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

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

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

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

검색을 빠르게 처리할 수 없다면 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 as you normally would
        mediaSession.setMetadata(build())
    }
}

자바

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 as you normally would
    mediaSession.setMetadata(builder.build());
}

일반 오류 처리

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

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

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

기타 자료