【Android】マナーモード、イヤホン接続時でも音を鳴らしたい

こんにちは、 id:numanuma08 です。Androidアプリの要件で「端末がマナーモードでも通知音を鳴らしたい」とか「イヤホンが繋がっていてもデバイスから音を出したい」とか言われた時の対応方法です。

マナーモードでも通知音を鳴らしたい

マナーモードでも通知音を鳴らしてユーザーに気が付いてほしい、そんな要件があったとします。まずはよく考えてください。エンドユーザー目線ではマナーモードにしているのは、スマホから音を出したくないからです。普通の通知を実装しているならバイブレーション機能を使うなど適切な実装があります。その辺りの検討が完了しているかどうか、要件が定まったフローを良く改めてください。

改めましたか?それでは実装方法です。マナーモードで音を鳴らす場合、NotificationManager.isNotificationPolicyAccessGrantedでアプリからマナーモード状態へのアクセスが許可されているかどうか調べる必要があります。許可されていない場合、設定アプリを開いてマナーモード状態へのアクセス許可をユーザーに与えてもらわなければなりません。

val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult())
launcher.launch(Intent().apply { action = Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS })

このチェックやリクエストを行って、実際の音声はMediaPlayerなど音声再生用のAPIで再生します。Notificationを使って音を鳴らすとマナーモードなどの設定が優先されますから。

以下はマナーモードの場合でも音量を最大にして再生するサンプルです。

val audioManager = context.getSystemService(AudioManager::class.java)
val notificationManager = context.getSystemService(NotificationManager::class.java)
// 現在のモードを取得しておく
var originalRingMode = audioManager.ringerMode
// 現在の音量を取得しておく
val originalVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
val maxNotificationVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)

// マナーモード時の処理
val isDndModeEnabled =
  notificationManager.currentInterruptionFilter != NotificationManager.INTERRUPTION_FILTER_ALL
if (
  isDndModeEnabled && originalRingMode == AudioManager.RINGER_MODE_SILENT && originalVolume != 0
) {
  originalRingMode = AudioManager.RINGER_MODE_NORMAL
}

val newVolume = ceil(maxNotificationVolume * volume).toInt()

audioManager.ringerMode = AudioManager.RINGER_MODE_NORMAL
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newVolume, 0)

mediaPlayer =
  MediaPlayer.create(context, appSettings.reminderSound.soundResource).apply {
    setAudioAttributes(
      AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_MEDIA)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build()
    )
  }

とりあえず上記のコードでマナーモード時でも音声を再生可能です。再生が終わったらボリュームなどを元に戻しましょう。

audioManager.ringerMode = originalRingMode
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, originalVolume, 0)

イヤホン接続時でもスピーカーから音を鳴らしたい

AndroidのAudioManagerにはAudioManager.setCommunicationDevice(API Level 33から)またはAudioManager.isSpeakerphoneOnというAPIがあり、これを使うと音声通話用ストリームの再生デバイスを本体スピーカーに設定できます。このAPIを使って再生デバイスを設定、リセットするコードは以下です。

/** スピーカーからの再生を設定する */
fun AudioManager.setCommunicationDevice() {
  // これを設定しないと再生デバイスが変わらない
  //  android.permission.MODIFY_AUDIO_SETTINGS が必要
  mode = AudioManager.MODE_IN_COMMUNICATION
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    val device =
        getDevices(AudioManager.GET_DEVICES_OUTPUTS).firstOrNull {
          it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
        }
    requireNotNull(device) { "device is null" }
    setCommunicationDevice(device)
  } else {
    @Suppress("DEPRECATION")
    isSpeakerphoneOn = true
  }
}

/** スピーカーからの再生を解除する */
fun AudioManager.resetCommunicationDevice() {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    clearCommunicationDevice()
  } else {
    @Suppress("DEPRECATION")
    isSpeakerphoneOn = false
  }
}

// 再生をスピーカーから行う
audioManager.setCommunicationDevice()
// 再生デバイスを元に戻す
audioManager.resetCommunicationDevice()

また、AndroidManifestで以下のPermissionを宣言しなければなりません。

 <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

さて、音声通話用ストリームを使うので音声再生のためのAudioAttributeは以下となります。

val audioAttributes =
            AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
                .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                .build()

また、イヤホンを使っている場合音楽など再生している可能性があるためそれらを中断しなければなりません。AudioManager.requestFocusを使ってこれを実現します。

val audioFocusRequest =
            AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
                .setAudioAttributes(audioAttributes)
                .build()
// バックグラウンドで音声が再生されていても、こちらの音声を優先するため
// 他のアプリの音声を一時的にミュートにする
audioManager.requestAudioFocus(audioFocusRequest)

最後にMediaPlayerで音声を再生します。

mediaPlayer.setOnCompletionListener {
        // 再生先デバイスをリセット
        audioManager.resetCommunicationDevice()
        continuation.resume(Unit) { mediaPlayer.release() }
        // 音量を元に戻す
        audioManager.setStreamVolume(stream, currentVolume, 0)
        // バックグラウンド再生を再開
        audioManager.abandonAudioFocusRequest(audioFocusRequest)
}

最後に、音声再生に関するコードの全体を掲載します。

  suspend fun play() =
      withContext(Dispatchers.IO) {
        val audioManager = context.getSystemService(AudioManager::class.java)

        val audioAttributes =
            AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
                .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                .build()
        val audioFocusRequest =
            AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
                .setAudioAttributes(audioAttributes)
                .build()
        // バックグラウンドで音声が再生されていても、こちらの音声を優先するため
        // 他のアプリの音声を一時的にミュートにする
        audioManager.requestAudioFocus(audioFocusRequest)

        // 再生をスピーカーから行う
        audioManager.setCommunicationDevice()
        val stream = audioAttributes.volumeControlStream
        // 現在の音量を取得する
        val currentVolume = audioManager.getStreamVolume(stream)
        // 音量を最大に設定する
        audioManager.setStreamVolume(
            stream, audioManager.getStreamMaxVolume(stream), AudioManager.FLAG_SHOW_UI)

        val mediaPlayer =
            MediaPlayer.create(context, R.raw.appearing).apply {
              setAudioAttributes(audioAttributes)
            }
        mediaPlayer.start()
        // 再生が終了したらスピーカーからの再生を解除する
        suspendCancellableCoroutine { continuation ->
          mediaPlayer.setOnCompletionListener {
            audioManager.resetCommunicationDevice()
            continuation.resume(Unit) { mediaPlayer.release() }
            // 音量を元に戻す
            audioManager.setStreamVolume(stream, currentVolume, 0)
            audioManager.abandonAudioFocusRequest(audioFocusRequest)
          }
          continuation.invokeOnCancellation {
            mediaPlayer.stop()
            mediaPlayer.release()
            audioManager.resetCommunicationDevice()
            // 音量を元に戻す
            audioManager.setStreamVolume(stream, currentVolume, 0)
            audioManager.abandonAudioFocusRequest(audioFocusRequest)
          }
        }
      }
}

まとめ

マナーモードやイヤホン接続時でも音声を再生する方法を紹介しました。アプリとして実現は出来ますが、ユーザーの体験を大きく損ねる可能性もありますので、利用する際は十分な検討と説明が必要である点をよく考えてください。