The Google Assistant and media apps Android


The Google Assistant and media apps

https://developer.android.com/guide/topics/media-apps/interacting-with-assistant

구글 어시스턴트는 구글 홈, 전화기 그리고 그 외의 다양한 장치를 제어하기 위해 음성 명령을 사용할 수 있게 한다. 미디어 명령 ("비욘세의 음악 재생") 를 인해하기 위한 빌트인 기능을 가지며 미디어 컨트롤 지원한다. 

어시스턴트는 미디어 세션을 사용하는 안드로이드 미디어 앱과 교신한다. 이 것은 앱 실행하고 재생하는데 인텐트나 서비스를 사용한다. 최상의 결과를 위해, 이 페이지의 모든 기능을 구현하는 것이 좋다.

일러두기: 이 가이드는 어떻게 미디어 앱을 생성하는지 보여주어 어시스턴트가 미디어를 컨트롤할 수 있게 한다. 어떻게 안드로이드 앱이 어시스턴트 API를 사용해 어시스턴트 사용자 경험을 향상시키는지에 대해서는 Optimizing Content for the Assistant 를 보라.

미디어 세션 사용하기

모든 오디오와 비디오 앱은 반드시 미디어 세션을 구현하여 어시스턴트가 플레이백이 시작하면 트랜스포트 컨트롤을 작동할 수 있게 한다.

앱의 미디어 세션은 반드시 지원하는 액션을 정의해야 하며 미디어 세션 콜백에 연관된 구현을 수행한다. 지원되는 액션은 setActions()에서 정의한다.

Universal Music Player 샘플 프로젝트는 어떻게 미디어 세션을 설정하는지 좋은 예시이다.

플레이백 액션

서비스로부터 플레이백을 시작하려면 미디어 세션은 반드시 이들 PLAY액션과 이들 콜백을 가져야한다.

ActionCallback
ACTION_PLAYonPlay()
ACTION_PLAY_FROM_SEARCHonPlayFromSearch()

세션은 PREPARE액션과 이들 콜백을 구현해야 한다.

ActionCallback
ACTION_PREPAREonPrepare()
ACTION_PREPARE_FROM_SEARCHonPrepareFromSearch()

준비 API를 구현함으로서 음성명령 후 재생 지연이 감소될 수 있다. 플레이백 지연을 증가할 미디어 앱은 캐싱 컨텐트를 시작하고 미디어 플레이백을 준비하는데 추가시간을 사용할 수 있다.

만약 onPrepare(), onPlay(), onPrepareFromSearch() 또는 onPlayFromSearch() 는 검색 쿼리없이 호출된다면, 미디어 앱은 현 미디어를 재생해야 한다. 만약 현 미디어가 없으면 다른 것을 재생시도하는 것이 좋다.

어시스턴트가 오직 이 섹션내에서 나열된 액션을 사용하므로 최상의 실현은 다른 어플리케이션과의 호환성을 허용해줄 모든 준비와 재생 API를 구현하는 것이다.

트랜스포트 컨트롤

앱의 미디어 세션이 활성화 된 후 어시스턴트는 음성명령을 통해 재생을 제어하고 미디어 메타데이터를 업데이트할 수 있다. 이를 수행하기 위해 다음 액션을 확인하고 연관된 콜백을 작성한다.

ActionCallbackDescription
ACTION_SKIP_TO_NEXTonSkipToNext()Next video
ACTION_SKIP_TO_PREVIOUSonSkipToPrevious()Previous song
ACTION_PAUSE,ACTION_PLAY_PAUSEonPause()Pause
ACTION_STOPonStop()Stop
ACTION_PLAYonPlay()Resume
ACTION_SEEK_TOonSeekTo()Rewind by 30 seconds
ACTION_SET_RATINGonSetRating(android.support.v4.media.RatingCompat)Thumbs up/down.

일러두기:
- 시크가 작동하기 위해서는 PlaybackState 는 state, position, playback, speed, 그리고 update time 을 최신상태로 반영되어야한다. 상태가 변경되면 setPlaybackState()를 반드시 호출한다.
- 미디어 앱은 미디어 세션 메타데이터를 최신으로 유지한다. 이것은 "어떤 음악이 재생중?" 과 같은 질문을 지원한다. 앱은 반드시 setMetadata()를 어플리커블 필드가 변경되면 호출해야 한다.
- MediaSession.setRatingType() 은 반드시 앱의 레이팅의 형식을 지정하며 앱은 반드시 onSetRating()을 구현한다. 만약 레이팅을 지정하지 않으면 레이팅 형식을 RATING_NONE으로 설정해야 한다.

에러

어시스턴트는 에러가 발생할 때 미디어 세션으로 부터 에러를 처리할 수 있으며 사용자에게 보고한다. Working with a media session에서 설명되어 있듯 미디어 세션이 트랜스포트 상태를 업데이트하고 PlaybackState에 맞게 에러코드를 설정한다. 어시스턴트는 getErrorCode()에의해 반환된 모든 에러코드를 인식할 수 있다.

인텐트로 플레이백

어시스턴트는 오디오나 비디오 앱을 실행할 수 있으며 딥링크로 인텐트를 보냄으로서 플레이백을 시작할 수 있다.

인텐트와 딥링크는 다른 소스로부터 올 수 있다.
- 어시스턴트가 모바일앱을 시작할 때 구글 서치를 사용해 마드된 컨텐트를 얻어 그 링크로 와치액션을 지원하도록 할 수 있다.
- 어시스턴트가 TV앱을 시작하면 앱은 미디어 컨텐트를 위한 URI를 노출하기 위해 TV 검색 프로바이더를 포함할 수 있다. 어시스턴트는 컨텐트 프로바이더에 질의를 보내어 해당 딥링크와 추가적인 액션을 위한 URI를 포함하는 인텐트를 반환한다. 만약 쿼리가 인텐트에 액션을 담고 있으면 어시스턴트는 그 액션을 보내고 앱에 URI를 되돌려준다. 만약 프로바이더가 액션을 지정하지 않으면 어시스턴트는 그 인텐트에 ACTION_VIEW를 추가할 것이다.

어시스턴트는 인텐드에 EXTRA_START_PLAYBACK을 true로 추가해 앱에 보낸다. 앱은 EXTRA_START_PLAYBACK 인텐트를 받으면 재생을 시작해야 한다.

일러두기: 어시스턴트는 장치상의 컨텐트 프로바이더로부터의 쿼리 결과를 7일까지 캐시할 수 있으며 사용자의 반복된 요청에 새로운 쿼리를 실행하는 것보다 캐시된 인텐트를 보낸다. 이 의미는 더이상 가용하지 않은 컨텐트를 재생할 수 있도록 요청받을 수 있다는 뜻이다. 이런 상황을 정중하게 처리해야 한다. 실행할 액티비티에서 사용자가 알 수 있도록 에러를 보여준다.

활성중에 인텐트 다루기

사용자는 이전 요청에의해 컨텐트를 재생중에도 다른 것을 재생하도록 어시스턴트에 요청할 수 있다. 이 의미는 재생 액티비티는 이미 실행중이고 활성된 동안 새로운 인텐트를 얻을 수 있게 할 수 있다.

새로운 요청을 다루기 위해 딥링크로 인텐트를 지원하는 액티비티는 onNewIntent() 를 오버라이드하여야 한다.

재생을 시작할 때 어시스턴트는 앱으로 보내기 위해 추가적인 플래그를 추가한다. 특별히, FLAG_ACTIVITY_CLEAR_TOP 또는 FLAG_ACTIVITY_NEW_TASK 또는 둘 모두로 추가한다. 비록 코드는 이들 플래그를 다룰 필요는 없지만, 안드로이드 시스템은 이에 반응한다. 이전 URI 가 재생중인동안 새로운 URI 도착에서 두 번째 재생 요청이 있을 때 앱의 작용에 영향을 준다. 이 경우에 앱의 반응을 테스트하는 것이 좋다. 상황을 시뮬레이트하기 위해서는 adb 명령을 사용한다. (상수 0x14000000 은 두 플래그의 비트 OR 불린이다.)

adb shelll 'am start -a android.intent.action.VIEW --ez android.intent.extra.START_PLAYBACK true -d <first_uri>' -f 0x14000000
adb shelll 'am start -a android.intent.action.VIEW --ez android.intent.extra.START_PLAYBACK true -d <second_uri>' -f 0x14000000

서비스로부터 플레이백

만약 앱이 어시스턴틑로부터 연결을 허용하는 미디어 브라우져 서비스를 가지면 어시스턴트는 서비스의 미디어 세션과의 교신에 의해 앱을 시작할 수 있다. 미디어 브라우져 서비스는 액티비티를 시작하지 않는 것이 좋다. 어시스턴트는 setSessionActivity() 로 지정한 PendingIntent 에 기반한 액티비티를 시작할 것이다.

일러두기: 현재, 어시스턴트는 비디오 앱을 시작할 때 서비스를 사용하지 않는다. (이 것은 인텐트로 시작) 그러나, 미래 호환을 위해 비디오 앱에서의 미디어 브라우져 서비스를 포함하는 것을 추천한다.

미디어 브라우져 서비스를 초기화할 때 미디어 세션.Token을 설정한다. 지원되는 플레이백 액션을 초기화때를 포함해 항상 지원해야 함을 기억한다. 어시스턴트는 미디어 앱이 어시스턴트가 첫 플레이백 명령을 보내기 전에 플레이백 액션을 설정함을 기대한다.

서비스로부터 시작하려면, 어시스턴트가 미디어 브라우져 클라이언트 API를 구현한다. 그 것은 TransportControls 호출을 수행해 앱의 미디어 세션상에서 PLAY 액션 콜백을 시작하도록 한다.

다음 다이어그램은 어시스턴트와 연결된 미디어 세션 콜백으로의해 생성된 명령의 순서를 보여준다. (만약 앱이 이들을 지원해야만 준비 콜백이 보내진다) 모든 호출은 비동기호출이다. 어시스턴트는 앱으로부터 어떤 반응도 기다리지 않는다.

Starting playback with a media session

사용자가 재생하기 위해 음성 명령을 하면, 어시스턴트 짧은 안내로 반응한다. 알림이 완료되면 어시스턴트는 PLAY액션을 완료한다. 그 것은 어떤 특정 재생 상태를 기다리지 않는다.

만약 앱이 ACTIONPREPARE* 액션을 지원하면 어시스턴트는 알림을 시작하기 전에 PREPARE액션을 호출한다.

미디어 브라우져 서비스에 연결

앱시작을 위해 서비스를 사용하기 위해, 어시스턴트는 반드시 미디어 브라우져 서비스를 연결하고 MediaSession.Token을 얻어야만 한다. 연결 요청은 서비스의 onGetRoot() 메소드로에서 다뤄진다. 요청을 다루는 두 가지 방법이 있다.
- 모든 연결 요청 허용
- 어시스턴트 앱으로부터의 연결요청 허용

모든 연결요청 허용
MediaSession으로 명령을 보내기 위해 어시스턴트를 허용하기 위해 BrowserRoot 를 반드시 반환해야 한다. 연결하기 위한 가장 쉬운방법은 모든 MediaBrowser 앱이 MediaBrowserService 에 연결하도록 허용하는 것이다. 널이 아닌 BrowserRoot 를 반환해야 한다. 이 것은 Universal Music Player에서 얻은 실행가능한 코드이다.

override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
  // 앱의 컨텐트 탐색에 임의 앱을 허용하지 않기 위해 기원을 확인할 필요가 있다.
  if (!mPackageValidator.isCallerAllowed(this, clientPackageName, clientUid)) {
    // 비신뢰 패키지로 부터 요청이 왔다면 빈 브라우져 루트를 반환
    // 만약 널이 반환되면 미디어 브라우져는 연결할 수 없고 추가적인 연결은 다른 미디어 브라우징 메소드로 만들어지지 않는다
    return MediaBrowserServiceCompat.BrowserRoot(MEDIA_ID_EMPTY_ROOT, null)
  }
  // 탐색을 위한 브라우져 루트를 반환
}

어시스턴트 앱 패키지와 시그니쳐 허용
어시스턴트를 패키지 이름과 시그니쳐를 확인함으로서 미디어 브라우져 서비스에 명시적으로 연결할 수 있도록 할 수 있다. 미디어 브라우져 서비스의 onGetRoot 메소드내에서 패키지 이름을 받을 것이다. 어시스턴트가 미디어세션으로 명령을 보낼 수 있게 하려면 BrowserRoot 를 반드시 반환해야 한다. Universal Music Player 샘플은 패키지 이름과 시그니쳐의 리스트를 유지한다. 아래의 패키지 이름과 시그니쳐는 구글 어시스턴트에서 사용되는 것이다.

<signing_certificate name="Google" release="false"
                     package="com.google.android.googlequicksearchbox">
    MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD
    VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
    VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
    AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
    Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET
    MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
    A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
    ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
    hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR
    24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy
    xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X
    W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC
    69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA
    cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw
    HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c
    xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
    CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
    QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
    CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud
    EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP
    zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla
    XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a
    IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a
    ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW
    Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
</signing_certificate>

<signing_certificate name="Google" release="true"
                     package="com.google.android.googlequicksearchbox">
    MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNV
    BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBW
    aWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4G
    A1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQx
    CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3Vu
    dGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9p
    ZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgC
    ggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JO
    Rland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/
    8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfY
    wXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LW
    uT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0z
    OHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Yl
    mn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14al
    oXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE
    BxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsT
    B0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTAD
    AQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4
    rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCE
    yj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh
    5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTb
    Qe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZM
    cUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
</signing_certificate>



덧글

댓글 입력 영역