오디오 포커스 관리

두 개 이상의 Android 앱이 동일한 출력 스트림으로 동시에 오디오를 재생할 수 있으며 시스템은 모든 사운드를 믹스합니다. 이는 기술적으로 인상적이지만 사용자에게는 매우 좋지 않을 수 있습니다. 모든 음악 앱이 동시에 재생되지 않도록 Android는 오디오 포커스라는 아이디어를 도입합니다. 한 번에 하나의 앱만 오디오 포커스를 유지할 수 있습니다.

앱이 오디오를 출력해야 하는 경우 오디오 포커스를 요청해야 합니다. 포커스가 있는 앱은 사운드를 재생할 수 있습니다. 그러나 오디오 포커스를 획득한 후 재생을 완료할 때까지 오디오 포커스를 유지하지 못할 수 있습니다. 다른 앱에서 포커스를 요청할 수 있으며 이 경우 내 앱에서 보유한 오디오 포커스를 다른 앱에서 선점하게 됩니다. 이 경우 내 앱은 사용자가 새로운 오디오 소스를 더 쉽게 들을 수 있도록 재생을 일시중지하거나 볼륨을 낮추어야 합니다.

Android 12 (API 수준 31) 이전에는 오디오 포커스가 시스템에서 관리되지 않았습니다. 따라서 앱 개발자는 오디오 포커스 가이드라인을 준수하는 것이 좋지만 Android 11 (API 수준 30) 이하를 실행하는 기기에서 앱이 오디오 포커스를 잃은 후에도 큰 소리로 계속 재생되는 경우 시스템은 이를 방지할 수 없습니다. 하지만 이러한 앱 동작은 사용자 환경을 저하시키고 사용자가 오작동하는 앱을 제거하게 만드는 경우가 많습니다.

잘 설계된 오디오 앱은 다음과 같은 일반 가이드라인에 따라 오디오 포커스를 관리해야 합니다.

  • 재생을 시작하기 직전에 requestAudioFocus()를 호출하여 AUDIOFOCUS_REQUEST_GRANTED가 반환되는지 확인합니다. 미디어 세션의 onPlay() 콜백에서 requestAudioFocus()를 호출합니다.

  • 다른 앱이 오디오 포커스를 받으면 재생을 중지 또는 일시중지하거나 볼륨을 낮춥니다.

  • 재생이 중지되면 (예: 앱에 재생할 항목이 더 없음) 오디오 포커스를 포기합니다. 사용자가 재생을 일시중지했지만 나중에 재생을 재개할 수 있는 경우 앱은 오디오 포커스를 포기할 필요가 없습니다.

  • AudioAttributes을 사용하여 앱이 재생하는 오디오의 유형을 설명합니다. 예를 들어 음성을 재생하는 앱의 경우 CONTENT_TYPE_SPEECH를 지정합니다.

오디오 포커스는 실행 중인 Android 버전에 따라 다르게 처리됩니다.

Android 12 (API 수준 31) 이상
오디오 포커스는 시스템에서 관리합니다. 다른 앱이 오디오 포커스를 요청하면 시스템은 앱의 오디오 재생을 강제로 페이드 아웃합니다. 또한 시스템은 수신 전화가 올 때 오디오 재생을 음소거합니다.
Android 8.0 (API 수준 26)~Android 11 (API 수준 30)
오디오 포커스는 시스템에서 관리하지 않지만 Android 8.0 (API 수준 26)부터 도입된 몇 가지 변경사항이 포함되어 있습니다.
Android 7.1 (API 수준 25) 이하
오디오 포커스는 시스템에서 관리하지 않으며 앱은 requestAudioFocus()abandonAudioFocus()를 사용하여 오디오 포커스를 관리합니다.

Android 12 이상에서 오디오 포커스

오디오 포커스를 사용하는 미디어 앱이나 게임 앱은 포커스가 손실된 후 오디오를 재생해서는 안 됩니다. Android 12 (API 수준 31) 및 이후 버전에서는 시스템에서 이 동작을 시행합니다. 다른 앱이 포커스를 보유하여 재생되는 동안 앱이 오디오 포커스를 요청하면 시스템은 재생 중인 앱을 강제로 페이드 아웃합니다. 페이드 아웃을 추가하면 한 앱에서 다른 앱으로 이동할 때 더 원활하게 전환됩니다.

이 페이드 아웃 동작은 다음 조건이 충족될 때 발생합니다.

  1. 현재 재생 중인 첫 번째 앱은 다음 모든 기준을 충족합니다.

  2. 두 번째 앱은 AudioManager.AUDIOFOCUS_GAIN으로 오디오 포커스를 요청합니다.

이러한 조건이 충족되면 오디오 시스템은 첫 번째 앱을 페이드 아웃합니다. 페이드 아웃이 끝날 때 시스템은 첫 번째 앱에 포커스 손실을 알립니다. 앱의 플레이어는 앱이 오디오 포커스를 다시 요청할 때까지 음소거 상태로 유지됩니다.

기존 오디오 포커스 동작

오디오 포커스 전환과 관련된 다음과 같은 기타 사례도 알아야 합니다.

자동 볼륨 낮추기

자동 볼륨 낮추기는 Android 8.0(API 수준 26)에서 도입되었습니다. 이 기능은 한 앱의 오디오 수준을 일시적으로 줄여 다른 앱을 명확하게 들을 수 있도록 하는 것입니다.

시스템에서 볼륨 낮추기를 구현하도록 하므로 앱에서 볼륨 낮추기를 구현하지 않아도 됩니다.

자동 볼륨 낮추기는 오디오 알림이 재생 중인 앱에서 포커스를 가져올 때도 발생합니다. 알림 재생의 시작은 볼륨 낮추기 램프의 끝과 동기화됩니다.

자동 볼륨 낮추기는 다음 조건이 충족될 때 발생합니다.

  1. 현재 재생 중인 첫 번째 앱은 다음 모든 기준을 충족합니다.

  2. 두 번째 앱은 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK으로 오디오 포커스를 요청합니다.

이러한 조건이 충족되면 오디오 시스템은 두 번째 앱에 포커스가 있는 동안 첫 번째 앱의 모든 활성 플레이어의 볼륨을 낮춥니다. 두 번째 앱이 포커스를 포기하면 볼륨을 원상태로 돌립니다. 첫 번째 앱은 포커스가 손실될 때 알림을 받지 않으므로 아무 작업도 할 필요가 없습니다.

자동 볼륨 낮추기는 사용자가 음성 콘텐츠를 들을 때는 실행되지 않습니다. 사용자가 프로그램 일부를 놓칠 수 있기 때문입니다. 예를 들어 운전 경로의 음성 안내는 음소거되지 않습니다.

수신 전화 시 현재 오디오 재생 음소거

일부 앱은 전화 통화 중에 올바르게 동작하지 않고 계속 오디오를 재생합니다. 이 경우 사용자가 통화를 들으려고 문제의 앱을 찾아 음소거하거나 종료해야 합니다. 이를 방지하기 위해 시스템은 수신 전화가 있는 동안 다른 앱의 오디오를 음소거할 수 있습니다. 수신 전화가 수신되고 앱이 다음 조건을 충족하면 시스템에서 이 기능을 호출합니다.

  • 앱에 AudioAttributes.USAGE_MEDIA 또는 AudioAttributes.USAGE_GAME 사용 속성이 있습니다.
  • 앱이 오디오 포커스 (모든 포커스 게인)를 성공적으로 요청했고 오디오를 재생 중입니다.

통화 중에 앱이 계속 재생되면 통화가 끝날 때까지 재생이 음소거됩니다. 그러나 앱이 통화 중에 재생되기 시작하면 플레이어가 음소거되지 않습니다. 사용자가 의도적으로 재생을 시작했다고 가정하기 때문입니다.

Android 8.0~Android 11의 오디오 포커스

Android 8.0 (API 수준 26)부터는 requestAudioFocus()를 호출할 때 AudioFocusRequest 매개변수를 제공해야 합니다. AudioFocusRequest에는 앱의 오디오 컨텍스트 및 기능에 관한 정보가 포함되어 있습니다. 시스템은 이 정보를 사용하여 오디오 포커스의 획득과 손실을 자동으로 관리합니다. 오디오 포커스를 해제하려면 AudioFocusRequest를 인수로 사용하는 abandonAudioFocusRequest() 메서드를 호출합니다. 포커스를 요청하고 포기할 때 모두 동일한 AudioFocusRequest 인스턴스를 사용합니다.

AudioFocusRequest를 만들려면 AudioFocusRequest.Builder를 사용합니다. 포커스 요청은 항상 요청 유형을 지정해야 하므로 유형은 빌더의 생성자에 포함됩니다. 요청의 다른 필드를 설정하려면 빌더의 메서드를 사용합니다.

FocusGain 필드는 필수사항이고 다른 필드는 모두 선택사항입니다.

메서드참고
setFocusGain() 이 필드는 모든 요청에서 필수사항입니다. Android 8.0 이전 requestAudioFocus() 호출에 사용된 durationHint와 동일한 값을 갖습니다(AUDIOFOCUS_GAIN, AUDIOFOCUS_GAIN_TRANSIENT, AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 또는 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE).
setAudioAttributes() AudioAttributes는 앱의 사용 사례를 설명합니다. 시스템은 앱이 오디오 포커스를 획득하거나 손실할 때 이를 관찰합니다. 속성은 스트림 유형의 개념에 우선합니다. Android 8.0 (API 수준 26) 이상에서는 볼륨 컨트롤 이외의 작업에 관한 스트림 유형이 지원되지 않습니다. 오디오 플레이어에서 사용하는 포커스 요청에서 동일한 속성을 사용합니다 (이 표 이후에 나오는 예 참조).

AudioAttributes.Builder를 사용하여 속성을 먼저 지정한 다음 이 메서드를 사용하여 요청에 속성을 할당합니다.

지정되지 않은 경우 AudioAttributes의 기본값은 AudioAttributes.USAGE_MEDIA입니다.

setWillPauseWhenDucked() 다른 앱이 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK으로 포커스를 요청하면 포커스가 있는 앱은 일반적으로 onAudioFocusChange() 콜백을 받지 않습니다. 시스템이 스스로 볼륨 낮추기를 할 수 있기 때문입니다. 볼륨을 낮추는 대신 재생을 일시중지해야 하는 경우 자동 볼륨 낮추기에 설명된 대로 setWillPauseWhenDucked(true)를 호출하고 OnAudioFocusChangeListener를 생성 및 설정합니다.
setAcceptsDelayedFocusGain() 다른 앱에서 포커스를 잠그면 오디오 포커스 요청이 실패할 수 있습니다. 이 메서드는 사용 가능할 때 비동기로 포커스를 획득하는 기능인 포커스 획득 지연을 사용 설정합니다.

포커스 획득 지연은 오디오 요청에서도 AudioManager.OnAudioFocusChangeListener를 지정한 경우에만 작동합니다. 포커스가 허용되었음을 알기 위해 앱이 콜백을 수신해야 하기 때문입니다.

setOnAudioFocusChangeListener() 요청에서 willPauseWhenDucked(true) 또는 setAcceptsDelayedFocusGain(true)도 지정하는 경우에만 OnAudioFocusChangeListener가 필요합니다.

리스너를 설정하는 두 가지 방법, 즉 핸들러 인수를 사용하는 방법과 사용하지 않는 방법이 있습니다. 핸들러는 리스너가 실행되는 스레드입니다. 핸들러를 지정하지 않으면 기본 Looper와 연결된 핸들러가 사용됩니다.

다음 예는 AudioFocusRequest.Builder를 사용하여 AudioFocusRequest를 빌드하는 방법 및 오디오 포커스를 요청하고 포기하는 방법을 보여줍니다.

Kotlin자바
// initializing variables for audio focus and playback management
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
    setAudioAttributes(AudioAttributes.Builder().run {
        setUsage(AudioAttributes.USAGE_GAME)
        setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        build()
    })
    setAcceptsDelayedFocusGain(true)
    setOnAudioFocusChangeListener(afChangeListener, handler)
    build()
}
val focusLock = Any()

var playbackDelayed = false
var playbackNowAuthorized = false

// requesting audio focus and processing the response
val res = audioManager.requestAudioFocus(focusRequest)
synchronized(focusLock) {
    playbackNowAuthorized = when (res) {
        AudioManager.AUDIOFOCUS_REQUEST_FAILED -> false
        AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
            playbackNow()
            true
        }
        AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
            playbackDelayed = true
            false
        }
        else -> false
    }
}

// implementing OnAudioFocusChangeListener to react to focus changes
override fun onAudioFocusChange(focusChange: Int) {
    when (focusChange) {
        AudioManager.AUDIOFOCUS_GAIN ->
            if (playbackDelayed || resumeOnFocusGain) {
                synchronized(focusLock) {
                    playbackDelayed = false
                    resumeOnFocusGain = false
                }
                playbackNow()
            }
        AudioManager.AUDIOFOCUS_LOSS -> {
            synchronized(focusLock) {
                resumeOnFocusGain = false
                playbackDelayed = false
            }
            pausePlayback()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
            synchronized(focusLock) {
                // only resume if playback is being interrupted
                resumeOnFocusGain = isPlaying()
                playbackDelayed = false
            }
            pausePlayback()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // ... pausing or ducking depends on your app
        }
    }
}
// initializing variables for audio focus and playback management
audioManager = (AudioManager) Context.getSystemService(Context.AUDIO_SERVICE);
playbackAttributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_GAME)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build();
focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
        .setAudioAttributes(playbackAttributes)
        .setAcceptsDelayedFocusGain(true)
        .setOnAudioFocusChangeListener(afChangeListener, handler)
        .build();
final Object focusLock = new Object();

boolean playbackDelayed = false;
boolean playbackNowAuthorized = false;

// requesting audio focus and processing the response
int res = audioManager.requestAudioFocus(focusRequest);
synchronized(focusLock) {
    if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
        playbackNowAuthorized = false;
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
        playbackNowAuthorized = true;
        playbackNow();
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
        playbackDelayed = true;
        playbackNowAuthorized = false;
    }
}

// implementing OnAudioFocusChangeListener to react to focus changes
@Override
public void onAudioFocusChange(int focusChange) {
    switch (focusChange) {
        case AudioManager.AUDIOFOCUS_GAIN:
            if (playbackDelayed || resumeOnFocusGain) {
                synchronized(focusLock) {
                    playbackDelayed = false;
                    resumeOnFocusGain = false;
                }
                playbackNow();
            }
            break;
        case AudioManager.AUDIOFOCUS_LOSS:
            synchronized(focusLock) {
                resumeOnFocusGain = false;
                playbackDelayed = false;
            }
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
            synchronized(focusLock) {
                // only resume if playback is being interrupted
                resumeOnFocusGain = isPlaying();
                playbackDelayed = false;
            }
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            // ... pausing or ducking depends on your app
            break;
        }
    }
}

자동 볼륨 낮추기

Android 8.0 (API 수준 26)에서 다른 앱이 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK로 포커스를 요청하면 시스템은 앱의 onAudioFocusChange() 콜백을 호출하지 않은 채 볼륨을 낮추고 복원할 수 있습니다.

자동 볼륨 낮추기는 음악 및 동영상 재생 앱에서 허용되는 동작이지만 오디오 북 앱과 같이 음성 콘텐츠를 재생할 때는 유용하지 않습니다. 이 경우에는 앱의 일시중지가 대신 필요합니다.

볼륨 낮추기 요청 시 볼륨을 줄이는 대신 일시중지를 원하는 경우 원하는 일시중지/다시 시작 동작을 구현하는 onAudioFocusChange() 콜백 메서드로 OnAudioFocusChangeListener를 만듭니다. setOnAudioFocusChangeListener()를 호출하여 리스너를 등록하고 setWillPauseWhenDucked(true)를 호출하여 시스템이 자동 볼륨 낮추기 대신 콜백을 사용하도록 지시합니다.

포커스 획득 지연

전화 통화 중과 같이 다른 앱에 의해 포커스가 '잠겨' 있기 때문에 시스템이 오디오 포커스 요청을 허용할 수 없는 경우가 있습니다. 이 경우 requestAudioFocus()AUDIOFOCUS_REQUEST_FAILED를 반환합니다. 이러한 일이 발생하면 앱이 포커스를 획득하지 못한 것이므로 오디오 재생을 계속해서는 안 됩니다.

setAcceptsDelayedFocusGain(true) 메서드는 앱이 비동기적으로 포커스 요청을 처리하도록 허용합니다. 이 플래그를 설정하면 포커스가 잠겨 있을 때 만들어진 요청이 AUDIOFOCUS_REQUEST_DELAYED를 반환합니다. 오디오 포커스를 잠근 조건이 더 이상 존재하지 않는 경우(예: 전화 통화 종료) 시스템은 보류 중인 포커스 요청을 허용하고 onAudioFocusChange()를 호출하여 앱에 알립니다.

포커스 획득 지연을 처리하려면 원하는 동작을 구현하는 onAudioFocusChange() 콜백 메서드로 OnAudioFocusChangeListener를 만들고 setOnAudioFocusChangeListener()를 호출하여 리스너를 등록해야 합니다.

Android 7.1 이하에서 오디오 포커스

requestAudioFocus()를 호출할 때는 현재 포커스가 있고 재생 중인 다른 앱에 적용할 수 있는 지속 시간 힌트를 지정해야 합니다.

  • 예측 가능한 미래에 오디오를 재생하고자 하며 (예: 음악 재생) 이전 오디오 포커스 보유 앱이 재생을 중지할 것으로 예상되는 경우 영구적인 오디오 포커스 (AUDIOFOCUS_GAIN)를 요청합니다.
  • 단기간만 오디오를 재생할 것이고 이전 보유 앱이 재생을 일시중지할 것으로 예상되는 경우 일시적인 포커스 (AUDIOFOCUS_GAIN_TRANSIENT)를 요청합니다.
  • 단기간만 오디오를 재생할 것이므로 이전 포커스 소유 앱이 오디오 출력을 '낮추기만' 하면 계속 재생해도 괜찮음을 나타내려면 볼륨 낮추기와 함께 일시적인 포커스(AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)를 요청합니다. 두 오디오 출력 모두 오디오 스트림에 혼합됩니다. 볼륨 낮추기는 들을 수 있는 운전 경로 등 간헐적으로 오디오 스트림을 사용하는 앱에 특히 적합합니다.

requestAudioFocus() 메서드에는 AudioManager.OnAudioFocusChangeListener도 필요합니다. 이 리스너는 미디어 세션을 소유하고 있는 동일한 활동 또는 서비스에서 만들어야 합니다. 그러면 다른 앱이 오디오 포커스를 획득하거나 포기할 때 내 앱이 수신하는 onAudioFocusChange() 콜백이 구현됩니다.

다음 스니펫은 STREAM_MUSIC 스트림에 관한 영구적인 오디오 포커스를 요청하며 오디오 포커스에서의 후속 변경을 처리하기 위해 OnAudioFocusChangeListener를 등록합니다. (변경 리스너에 관한 내용은 오디오 포커스 변경에 응답에서 설명합니다.)

Kotlin자바
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
lateinit var afChangeListener AudioManager.OnAudioFocusChangeListener

...
// Request audio focus for playback
val result: Int = audioManager.requestAudioFocus(
        afChangeListener,
        // Use the music stream.
        AudioManager.STREAM_MUSIC,
        // Request permanent focus.
        AudioManager.AUDIOFOCUS_GAIN
)

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback
}
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
AudioManager.OnAudioFocusChangeListener afChangeListener;

...
// Request audio focus for playback
int result = audioManager.requestAudioFocus(afChangeListener,
                             // Use the music stream.
                             AudioManager.STREAM_MUSIC,
                             // Request permanent focus.
                             AudioManager.AUDIOFOCUS_GAIN);

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback
}

재생을 마치면 abandonAudioFocus()를 호출합니다.

Kotlin자바
audioManager.abandonAudioFocus(afChangeListener)
// Abandon audio focus when playback complete
audioManager.abandonAudioFocus(afChangeListener);

이렇게 하면 포커스가 더 이상 필요하지 않음을 시스템에 알리고 관련된 OnAudioFocusChangeListener의 등록을 취소합니다. 일시적인 포커스를 요청한 경우에는 일시중지되거나 볼륨을 낮춘 앱에 계속 재생하거나 볼륨을 복원할 수 있다고 알리게 됩니다.

오디오 포커스 변경에 응답

앱은 오디오 포커스를 획득하면 다른 앱이 직접 오디오 포커스를 요청할 경우 이를 해제할 수 있어야 합니다. 이 경우 앱은 requestAudioFocus() 호출 시 지정된 AudioFocusChangeListener에서 onAudioFocusChange() 메서드 호출을 수신합니다.

onAudioFocusChange()에 전달된 focusChange 매개변수는 발생하는 변경의 종류를 나타냅니다. 이는 포커스를 획득하는 앱에서 사용되는 지속 시간 힌트에 해당합니다. 앱은 이에 적절히 대응해야 합니다.

일시적인 포커스 손실
포커스 변경이 일시적인 경우 (AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 또는 AUDIOFOCUS_LOSS_TRANSIENT) 앱은 볼륨을 낮추거나 (자동 볼륨 낮추기를 사용하지 않는 경우) 재생을 일시중지하되 동일한 상태를 유지해야 합니다.

오디오 포커스가 일시적으로 손실된 동안 오디오 포커스를 계속해서 모니터링하여 포커스를 다시 획득할 때 정상적인 재생을 다시 시작할 수 있도록 준비해야 합니다. 차단 앱이 포커스를 포기하면 콜백(AUDIOFOCUS_GAIN)이 수신됩니다. 이때 볼륨을 정상 수준으로 복원하거나 재생을 다시 시작할 수 있습니다.

영구적인 포커스 손실
오디오 포커스 손실이 영구적이면 (AUDIOFOCUS_LOSS) 다른 앱이 오디오를 재생하는 것입니다. 앱은 AUDIOFOCUS_GAIN 콜백을 수신하지 않을 것이므로 재생을 즉시 일시중지해야 합니다. 재생을 다시 시작하려면 사용자가 알림 또는 앱 UI에서 재생 전송 컨트롤을 누르는 등 명시적인 작업을 실행해야 합니다.

다음 코드 스니펫은 OnAudioFocusChangeListeneronAudioFocusChange() 콜백을 구현하는 방법을 보여줍니다. 오디오 포커스가 영구적으로 손실될 경우 중지 콜백 지연을 위해 Handler가 사용됩니다.

Kotlin자바
private val handler = Handler()
private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
    when (focusChange) {
        AudioManager.AUDIOFOCUS_LOSS -> {
            // Permanent loss of audio focus
            // Pause playback immediately
            mediaController.transportControls.pause()
            // Wait 30 seconds before stopping playback
            handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30))
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
            // Pause playback
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // Lower the volume, keep playing
        }
        AudioManager.AUDIOFOCUS_GAIN -> {
            // Your app has been granted audio focus again
            // Raise volume to normal, restart playback if necessary
        }
    }
}
private Handler handler = new Handler();
AudioManager.OnAudioFocusChangeListener afChangeListener =
  new AudioManager.OnAudioFocusChangeListener() {
    public void onAudioFocusChange(int focusChange) {
      if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
        // Permanent loss of audio focus
        // Pause playback immediately
        mediaController.getTransportControls().pause();
        // Wait 30 seconds before stopping playback
        handler.postDelayed(delayedStopRunnable,
          TimeUnit.SECONDS.toMillis(30));
      }
      else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
        // Pause playback
      } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
        // Lower the volume, keep playing
      } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
        // Your app has been granted audio focus again
        // Raise volume to normal, restart playback if necessary
      }
    }
  };

핸들러는 다음과 같은 Runnable을 사용합니다.

Kotlin자바
private var delayedStopRunnable = Runnable {
    mediaController.transportControls.stop()
}
private Runnable delayedStopRunnable = new Runnable() {
    @Override
    public void run() {
        getMediaController().getTransportControls().stop();
    }
};

사용자가 재생을 다시 시작할 때 지연된 중지가 시작되지 않도록 하려면 상태 변경에 대한 응답으로 mHandler.removeCallbacks(mDelayedStopRunnable)를 호출합니다. 예를 들어 콜백의 onPlay(), onSkipToNext() 등에서 removeCallbacks()를 호출합니다. 서비스에서 사용하는 리소스를 정리할 때 onDestroy() 콜백에서도 이 메서드를 호출해야 합니다.