diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index 7571b6a205..af1bd734c1 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -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() private val knownCalls = mutableSetOf() @@ -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) - } } diff --git a/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt b/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt new file mode 100644 index 0000000000..8ca6b2bb16 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt @@ -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 { + return HashSet().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() + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt b/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt index 8649c755f4..2305ca7ed5 100644 --- a/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt +++ b/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt @@ -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 = HashSet() diff --git a/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt index 9518ab443f..92b0018b57 100644 --- a/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt @@ -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) { - _availableDevices = devices + fun replaceDevices(devices: Set) { + _availableDevices.clear() + _availableDevices.addAll(devices) resetSelectedDevice() }