VoIP: continue reworking audio device management (make it works for Android < M)

This commit is contained in:
ganfra 2021-01-25 19:32:37 +01:00
parent d29ab94617
commit 05361c13f1
4 changed files with 151 additions and 32 deletions

View File

@ -44,7 +44,7 @@ import timber.log.Timber
/**
* Foreground service to manage calls
*/
class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener {
class CallService : VectorService() {
private val connections = mutableMapOf<String, CallConnection>()
private val knownCalls = mutableSetOf<String>()
@ -58,9 +58,6 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
private var callRingPlayerIncoming: CallRingPlayerIncoming? = null
private var callRingPlayerOutgoing: CallRingPlayerOutgoing? = null
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null
private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null
// A media button receiver receives and helps translate hardware media playback buttons,
// such as those found on wired and wireless headsets, into the appropriate callbacks in your app
private var mediaSession: MediaSessionCompat? = null
@ -84,18 +81,12 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
alertManager = vectorComponent().alertManager()
callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext)
callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext)
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this)
bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(this, this)
}
override fun onDestroy() {
super.onDestroy()
callRingPlayerIncoming?.stop()
callRingPlayerOutgoing?.stop()
wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(this, it) }
wiredHeadsetStateReceiver = null
bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(this, it) }
bluetoothHeadsetStateReceiver = null
mediaSession?.release()
mediaSession = null
}
@ -346,14 +337,4 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
return this@CallService
}
}
override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
Timber.v("## VOIP: onHeadsetEvent $event")
callManager.onWiredDeviceEvent(event)
}
override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
Timber.v("## VOIP: onBTHeadsetEvent $event")
callManager.onWirelessDeviceEvent(event)
}
}

View File

@ -0,0 +1,140 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("DEPRECATION")
package im.vector.app.features.call.audio
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import im.vector.app.core.services.BluetoothHeadsetReceiver
import im.vector.app.core.services.WiredHeadsetStateReceiver
import timber.log.Timber
import java.util.HashSet
internal class API21AudioDeviceDetector(private val context: Context,
private val audioManager: AudioManager,
private val callAudioManager: CallAudioManager
) : CallAudioManager.AudioDeviceDetector, WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener {
private var bluetoothAdapter: BluetoothAdapter? = null
private var connectedBlueToothHeadset: BluetoothProfile? = null
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null
private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null
private val onAudioDeviceChangeRunner = Runnable {
val devices = getAvailableSoundDevices()
callAudioManager.replaceDevices(devices)
Timber.i(" Available audio devices: $devices")
callAudioManager.updateAudioRoute()
}
private fun getAvailableSoundDevices(): Set<CallAudioManager.Device> {
return HashSet<CallAudioManager.Device>().apply {
if (isBluetoothHeadsetOn()) add(CallAudioManager.Device.WIRELESS_HEADSET)
if(isWiredHeadsetOn()){
add(CallAudioManager.Device.HEADSET)
}else {
add(CallAudioManager.Device.PHONE)
}
add(CallAudioManager.Device.SPEAKER)
}
}
private fun isWiredHeadsetOn(): Boolean {
return audioManager.isWiredHeadsetOn
}
private fun isBluetoothHeadsetOn(): Boolean {
Timber.v("## VOIP: AudioManager isBluetoothHeadsetOn")
try {
if (connectedBlueToothHeadset == null) return false.also {
Timber.v("## VOIP: AudioManager no connected bluetooth headset")
}
if (!audioManager.isBluetoothScoAvailableOffCall) return false.also {
Timber.v("## VOIP: AudioManager isBluetoothScoAvailableOffCall false")
}
return true
} catch (failure: Throwable) {
Timber.e("## VOIP: AudioManager isBluetoothHeadsetOn failure ${failure.localizedMessage}")
return false
}
}
/**
* Helper method to trigger an audio route update when devices change. It
* makes sure the operation is performed on the audio thread.
*/
private fun onAudioDeviceChange() {
callAudioManager.runInAudioThread(onAudioDeviceChangeRunner)
}
override fun start() {
Timber.i("Start using $this as the audio device handler")
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(context, this)
bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(context, this)
val bm: BluetoothManager? = context.getSystemService()
val adapter = bm?.adapter
Timber.d("## VOIP Bluetooth adapter $adapter")
bluetoothAdapter = adapter
adapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceDisconnected(profile: Int) {
Timber.d("## VOIP onServiceDisconnected $profile")
if (profile == BluetoothProfile.HEADSET) {
connectedBlueToothHeadset = null
onAudioDeviceChange()
}
}
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
Timber.d("## VOIP onServiceConnected $profile , proxy:$proxy")
if (profile == BluetoothProfile.HEADSET) {
connectedBlueToothHeadset = proxy
onAudioDeviceChange()
}
}
}, BluetoothProfile.HEADSET)
onAudioDeviceChange()
}
override fun stop() {
Timber.i("Stop using $this as the audio device handler")
wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(context, it) }
wiredHeadsetStateReceiver = null
bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(context, it) }
bluetoothHeadsetStateReceiver = null
}
override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
Timber.v("onHeadsetEvent $event")
onAudioDeviceChange()
}
override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
Timber.v("onBTHeadsetEvent $event")
onAudioDeviceChange()
}
}

View File

@ -13,16 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("DEPRECATION")
package im.vector.app.features.call.audio
import android.media.AudioAttributes
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.AudioManager.OnAudioFocusChangeListener
import android.os.Build
import androidx.annotation.RequiresApi
import timber.log.Timber
@ -31,7 +26,7 @@ import java.util.HashSet
@RequiresApi(Build.VERSION_CODES.M)
internal class API23AudioDeviceDetector(private val audioManager: AudioManager,
private val callAudioManager: CallAudioManager
) : CallAudioManager.AudioDeviceDetector{
) : CallAudioManager.AudioDeviceDetector {
private val onAudioDeviceChangeRunner = Runnable {
val devices: MutableSet<CallAudioManager.Device> = HashSet()

View File

@ -23,7 +23,7 @@ import timber.log.Timber
import java.util.HashSet
import java.util.concurrent.Executors
class CallAudioManager(context: Context, val configChange: (() -> Unit)?) {
class CallAudioManager(private val context: Context, val configChange: (() -> Unit)?) {
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private var audioDeviceDetector: AudioDeviceDetector? = null
@ -57,11 +57,13 @@ class CallAudioManager(context: Context, val configChange: (() -> Unit)?) {
private fun setup() {
audioDeviceDetector?.stop()
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
audioDeviceDetector = API23AudioDeviceDetector(audioManager, this)
audioDeviceDetector = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
API23AudioDeviceDetector(audioManager, this)
} else {
API21AudioDeviceDetector(context, audioManager, this)
}
audioDeviceRouter = DefaultAudioDeviceRouter(audioManager, this)
audioDeviceDetector?.start()
audioDeviceRouter = DefaultAudioDeviceRouter(audioManager, this)
}
fun runInAudioThread(runnable: Runnable) {
@ -190,8 +192,9 @@ class CallAudioManager(context: Context, val configChange: (() -> Unit)?) {
*
* @param devices The new devices list.
*/
fun replaceDevices(devices: MutableSet<Device>) {
_availableDevices = devices
fun replaceDevices(devices: Set<Device>) {
_availableDevices.clear()
_availableDevices.addAll(devices)
resetSelectedDevice()
}