diff --git a/vector/build.gradle b/vector/build.gradle index 496582e41c..a59b41d910 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -322,6 +322,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "androidx.sharetarget:sharetarget:1.0.0" implementation 'androidx.core:core-ktx:1.3.2' + implementation "androidx.media:media:1.2.1" implementation "org.threeten:threetenbp:1.4.0:no-tzdb" implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0" diff --git a/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt b/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt deleted file mode 100644 index 3a24cf6d48..0000000000 --- a/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Copyright (c) 2020 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. - */ - -package im.vector.app.features.call - -import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothManager -import android.bluetooth.BluetoothProfile -import android.content.Context -import android.content.pm.PackageManager -import android.media.AudioManager -import androidx.core.content.getSystemService -import im.vector.app.core.services.WiredHeadsetStateReceiver -import org.matrix.android.sdk.api.session.call.CallState -import org.matrix.android.sdk.api.session.call.MxCall -import timber.log.Timber -import java.util.concurrent.Executors - -class CallAudioManager( - val applicationContext: Context, - val configChange: (() -> Unit)? -) { - - enum class SoundDevice { - PHONE, - SPEAKER, - HEADSET, - WIRELESS_HEADSET - } - - // if all calls to audio manager not in the same thread it's not working well. - private val executor = Executors.newSingleThreadExecutor() - - private var audioManager: AudioManager? = null - - private var savedIsSpeakerPhoneOn = false - private var savedIsMicrophoneMute = false - private var savedAudioMode = AudioManager.MODE_INVALID - - private var connectedBlueToothHeadset: BluetoothProfile? = null - private var wantsBluetoothConnection = false - - private var bluetoothAdapter: BluetoothAdapter? = null - - init { - executor.execute { - audioManager = applicationContext.getSystemService() - } - val bm = applicationContext.getSystemService<BluetoothManager>() - val adapter = bm?.adapter - Timber.d("## VOIP Bluetooth adapter $adapter") - bluetoothAdapter = adapter - adapter?.getProfileProxy(applicationContext, object : BluetoothProfile.ServiceListener { - override fun onServiceDisconnected(profile: Int) { - Timber.d("## VOIP onServiceDisconnected $profile") - if (profile == BluetoothProfile.HEADSET) { - connectedBlueToothHeadset = null - configChange?.invoke() - } - } - - override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) { - Timber.d("## VOIP onServiceConnected $profile , proxy:$proxy") - if (profile == BluetoothProfile.HEADSET) { - connectedBlueToothHeadset = proxy - configChange?.invoke() - } - } - }, BluetoothProfile.HEADSET) - } - - private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> - - // Called on the listener to notify if the audio focus for this listener has been changed. - // The |focusChange| value indicates whether the focus was gained, whether the focus was lost, - // and whether that loss is transient, or whether the new focus holder will hold it for an - // unknown amount of time. - Timber.v("## VOIP: Audio focus change $focusChange") - } - - fun startForCall(mxCall: MxCall) { - Timber.v("## VOIP: AudioManager startForCall ${mxCall.callId}") - } - - private fun setupAudioManager(mxCall: MxCall) { - Timber.v("## VOIP: AudioManager setupAudioManager ${mxCall.callId}") - val audioManager = audioManager ?: return - savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn - savedIsMicrophoneMute = audioManager.isMicrophoneMute - savedAudioMode = audioManager.mode - - // Request audio playout focus (without ducking) and install listener for changes in focus. - - // Remove the deprecation forces us to use 2 different method depending on API level - @Suppress("DEPRECATION") val result = audioManager.requestAudioFocus(audioFocusChangeListener, - AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) - if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - Timber.d("## VOIP Audio focus request granted for VOICE_CALL streams") - } else { - Timber.d("## VOIP Audio focus request failed") - } - - // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is - // required to be in this mode when playout and/or recording starts for - // best possible VoIP performance. - audioManager.mode = AudioManager.MODE_IN_COMMUNICATION - - // Always disable microphone mute during a WebRTC call. - setMicrophoneMute(false) - - adjustCurrentSoundDevice(mxCall) - } - - private fun adjustCurrentSoundDevice(mxCall: MxCall) { - val audioManager = audioManager ?: return - executor.execute { - if (mxCall.state == CallState.LocalRinging && !isHeadsetOn()) { - // Always use speaker if incoming call is in ringing state and a headset is not connected - Timber.v("##VOIP: AudioManager default to SPEAKER (it is ringing)") - setCurrentSoundDevice(SoundDevice.SPEAKER) - } else if (mxCall.isVideoCall && !isHeadsetOn()) { - // If there are no headset, start video output in speaker - // (you can't watch the video and have the phone close to your ear) - Timber.v("##VOIP: AudioManager default to speaker ") - setCurrentSoundDevice(SoundDevice.SPEAKER) - } else { - // if a wired headset is plugged, sound will be directed to it - // (can't really force earpiece when headset is plugged) - if (isBluetoothHeadsetConnected(audioManager)) { - Timber.v("##VOIP: AudioManager default to WIRELESS_HEADSET ") - setCurrentSoundDevice(SoundDevice.WIRELESS_HEADSET) - // try now in case already connected? - audioManager.isBluetoothScoOn = true - } else { - Timber.v("##VOIP: AudioManager default to PHONE/HEADSET ") - setCurrentSoundDevice(if (isWiredHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE) - } - } - } - } - - fun onCallConnected(mxCall: MxCall) { - Timber.v("##VOIP: AudioManager call answered, adjusting current sound device") - setupAudioManager(mxCall) - } - - fun getAvailableSoundDevices(): List<SoundDevice> { - return ArrayList<SoundDevice>().apply { - if (isBluetoothHeadsetOn()) add(SoundDevice.WIRELESS_HEADSET) - add(if (isWiredHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE) - add(SoundDevice.SPEAKER) - } - } - - fun stop() { - Timber.v("## VOIP: AudioManager stopCall") - executor.execute { - // Restore previously stored audio states. - setSpeakerphoneOn(savedIsSpeakerPhoneOn) - setMicrophoneMute(savedIsMicrophoneMute) - audioManager?.mode = savedAudioMode - - connectedBlueToothHeadset?.let { - if (audioManager != null && isBluetoothHeadsetConnected(audioManager!!)) { - audioManager?.stopBluetoothSco() - audioManager?.isBluetoothScoOn = false - audioManager?.isSpeakerphoneOn = false - } - bluetoothAdapter?.closeProfileProxy(BluetoothProfile.HEADSET, it) - } - - audioManager?.mode = AudioManager.MODE_NORMAL - - @Suppress("DEPRECATION") - audioManager?.abandonAudioFocus(audioFocusChangeListener) - } - } - - fun getCurrentSoundDevice(): SoundDevice { - val audioManager = audioManager ?: return SoundDevice.PHONE - if (audioManager.isSpeakerphoneOn) { - return SoundDevice.SPEAKER - } else { - if (isBluetoothHeadsetConnected(audioManager)) return SoundDevice.WIRELESS_HEADSET - return if (isHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE - } - } - - private fun isBluetoothHeadsetConnected(audioManager: AudioManager) = - isBluetoothHeadsetOn() - && !connectedBlueToothHeadset?.connectedDevices.isNullOrEmpty() - && (wantsBluetoothConnection || audioManager.isBluetoothScoOn) - - fun setCurrentSoundDevice(device: SoundDevice) { - executor.execute { - Timber.v("## VOIP setCurrentSoundDevice $device") - when (device) { - SoundDevice.HEADSET, - SoundDevice.PHONE -> { - wantsBluetoothConnection = false - if (isBluetoothHeadsetOn()) { - audioManager?.stopBluetoothSco() - audioManager?.isBluetoothScoOn = false - } - setSpeakerphoneOn(false) - } - SoundDevice.SPEAKER -> { - setSpeakerphoneOn(true) - wantsBluetoothConnection = false - audioManager?.stopBluetoothSco() - audioManager?.isBluetoothScoOn = false - } - SoundDevice.WIRELESS_HEADSET -> { - setSpeakerphoneOn(false) - // I cannot directly do it, i have to start then wait that it's connected - // to route to bt - audioManager?.startBluetoothSco() - wantsBluetoothConnection = true - } - } - - configChange?.invoke() - } - } - - fun bluetoothStateChange(plugged: Boolean) { - executor.execute { - if (plugged && wantsBluetoothConnection) { - audioManager?.isBluetoothScoOn = true - } else if (!plugged && !wantsBluetoothConnection) { - audioManager?.stopBluetoothSco() - } - - configChange?.invoke() - } - } - - fun wiredStateChange(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { - executor.execute { - // if it's plugged and speaker is on we should route to headset - if (event.plugged && getCurrentSoundDevice() == SoundDevice.SPEAKER) { - setCurrentSoundDevice(CallAudioManager.SoundDevice.HEADSET) - } else if (!event.plugged) { - // if it's unplugged ? always route to speaker? - // this is questionable? - if (!wantsBluetoothConnection) { - setCurrentSoundDevice(SoundDevice.SPEAKER) - } - } - configChange?.invoke() - } - } - - private fun isHeadsetOn(): Boolean { - return isWiredHeadsetOn() || (audioManager?.let { isBluetoothHeadsetConnected(it) } ?: false) - } - - private fun isWiredHeadsetOn(): Boolean { - @Suppress("DEPRECATION") - return audioManager?.isWiredHeadsetOn ?: false - } - - 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 == false) return false.also { - Timber.v("## VOIP: AudioManager isBluetoothScoAvailableOffCall false") - } - return true - } catch (failure: Throwable) { - Timber.e("## VOIP: AudioManager isBluetoothHeadsetOn failure ${failure.localizedMessage}") - return false - } - } - - /** Sets the speaker phone mode. */ - private fun setSpeakerphoneOn(on: Boolean) { - Timber.v("## VOIP: AudioManager setSpeakerphoneOn $on") - val wasOn = audioManager?.isSpeakerphoneOn ?: false - if (wasOn == on) { - return - } - audioManager?.isSpeakerphoneOn = on - } - - /** Sets the microphone mute state. */ - private fun setMicrophoneMute(on: Boolean) { - Timber.v("## VOIP: AudioManager setMicrophoneMute $on") - val wasMuted = audioManager?.isMicrophoneMute ?: false - if (wasMuted == on) { - return - } - audioManager?.isMicrophoneMute = on - } - - /** true if the device has a telephony radio with data - * communication support. */ - private fun isThisPhone(): Boolean { - return applicationContext.packageManager.hasSystemFeature( - PackageManager.FEATURE_TELEPHONY) - } -} diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt index 7c8c4eb465..735ce2fd22 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt @@ -27,6 +27,7 @@ import com.airbnb.mvrx.activityViewModel import im.vector.app.R import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetCallControlsBinding +import im.vector.app.features.call.audio.CallAudioManager import me.gujun.android.span.span @@ -79,22 +80,22 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC } } - private fun showSoundDeviceChooser(available: List<CallAudioManager.SoundDevice>, current: CallAudioManager.SoundDevice) { + private fun showSoundDeviceChooser(available: Set<CallAudioManager.Device>, current: CallAudioManager.Device) { val soundDevices = available.map { when (it) { - CallAudioManager.SoundDevice.WIRELESS_HEADSET -> span { + CallAudioManager.Device.WIRELESS_HEADSET -> span { text = getString(R.string.sound_device_wireless_headset) textStyle = if (current == it) "bold" else "normal" } - CallAudioManager.SoundDevice.PHONE -> span { + CallAudioManager.Device.PHONE -> span { text = getString(R.string.sound_device_phone) textStyle = if (current == it) "bold" else "normal" } - CallAudioManager.SoundDevice.SPEAKER -> span { + CallAudioManager.Device.SPEAKER -> span { text = getString(R.string.sound_device_speaker) textStyle = if (current == it) "bold" else "normal" } - CallAudioManager.SoundDevice.HEADSET -> span { + CallAudioManager.Device.HEADSET -> span { text = getString(R.string.sound_device_headset) textStyle = if (current == it) "bold" else "normal" } @@ -106,16 +107,16 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC when (soundDevices[n].toString()) { // TODO Make an adapter and handle multiple Bluetooth headsets. Also do not use translations. getString(R.string.sound_device_phone) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.PHONE)) + callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.PHONE)) } getString(R.string.sound_device_speaker) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.SPEAKER)) + callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.SPEAKER)) } getString(R.string.sound_device_headset) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.HEADSET)) + callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.HEADSET)) } getString(R.string.sound_device_wireless_headset) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.WIRELESS_HEADSET)) + callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.WIRELESS_HEADSET)) } } } @@ -125,11 +126,11 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC private fun renderState(state: VectorCallViewState) { views.callControlsSoundDevice.title = getString(R.string.call_select_sound_device) - views.callControlsSoundDevice.subTitle = when (state.soundDevice) { - CallAudioManager.SoundDevice.PHONE -> getString(R.string.sound_device_phone) - CallAudioManager.SoundDevice.SPEAKER -> getString(R.string.sound_device_speaker) - CallAudioManager.SoundDevice.HEADSET -> getString(R.string.sound_device_headset) - CallAudioManager.SoundDevice.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset) + views.callControlsSoundDevice.subTitle = when (state.device) { + CallAudioManager.Device.PHONE -> getString(R.string.sound_device_phone) + CallAudioManager.Device.SPEAKER -> getString(R.string.sound_device_speaker) + CallAudioManager.Device.HEADSET -> getString(R.string.sound_device_headset) + CallAudioManager.Device.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset) } views.callControlsSwitchCamera.isVisible = state.isVideoCall && state.canSwitchCamera diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt index adb8897a51..32a1deb266 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt @@ -17,6 +17,7 @@ package im.vector.app.features.call import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.call.audio.CallAudioManager sealed class VectorCallViewActions : VectorViewModelAction { object EndCall : VectorCallViewActions() @@ -25,7 +26,7 @@ sealed class VectorCallViewActions : VectorViewModelAction { object ToggleMute : VectorCallViewActions() object ToggleVideo : VectorCallViewActions() object ToggleHoldResume: VectorCallViewActions() - data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions() + data class ChangeAudioDevice(val device: CallAudioManager.Device) : VectorCallViewActions() object SwitchSoundDevice : VectorCallViewActions() object HeadSetButtonPressed : VectorCallViewActions() object ToggleCamera : VectorCallViewActions() diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt index 832c4fe944..5ffeca6f66 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt @@ -17,6 +17,7 @@ package im.vector.app.features.call import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.features.call.audio.CallAudioManager import org.matrix.android.sdk.api.session.call.TurnServerResponse sealed class VectorCallViewEvents : VectorViewEvents { @@ -24,8 +25,8 @@ sealed class VectorCallViewEvents : VectorViewEvents { object DismissNoCall : VectorCallViewEvents() data class ConnectionTimeout(val turn: TurnServerResponse?) : VectorCallViewEvents() data class ShowSoundDeviceChooser( - val available: List<CallAudioManager.SoundDevice>, - val current: CallAudioManager.SoundDevice + val available: Set<CallAudioManager.Device>, + val current: CallAudioManager.Device ) : VectorCallViewEvents() object ShowCallTransferScreen: VectorCallViewEvents() // data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 0c621dcfe4..d12add5014 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -25,6 +25,7 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCallManager import org.matrix.android.sdk.api.MatrixCallback @@ -133,17 +134,16 @@ class VectorCallViewModel @AssistedInject constructor( } override fun onAudioDevicesChange() { - val currentSoundDevice = callManager.callAudioManager.getCurrentSoundDevice() - if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { + val currentSoundDevice = callManager.audioManager.selectedDevice ?: return + if (currentSoundDevice == CallAudioManager.Device.PHONE) { proximityManager.start() } else { proximityManager.stop() } - setState { copy( - availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(), - soundDevice = currentSoundDevice + availableDevices = callManager.audioManager.availableDevices, + device = currentSoundDevice ) } } @@ -174,8 +174,8 @@ class VectorCallViewModel @AssistedInject constructor( callManager.addCurrentCallListener(currentCallListener) val item: MatrixItem? = session.getUser(webRtcCall.mxCall.opponentUserId)?.toMatrixItem() webRtcCall.addListener(callListener) - val currentSoundDevice = callManager.callAudioManager.getCurrentSoundDevice() - if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { + val currentSoundDevice = callManager.audioManager.selectedDevice + if (currentSoundDevice == CallAudioManager.Device.PHONE) { proximityManager.start() } setState { @@ -183,10 +183,10 @@ class VectorCallViewModel @AssistedInject constructor( isVideoCall = webRtcCall.mxCall.isVideoCall, callState = Success(webRtcCall.mxCall.state), callInfo = VectorCallViewState.CallInfo(callId, item), - soundDevice = currentSoundDevice, + device = currentSoundDevice ?: CallAudioManager.Device.PHONE, isLocalOnHold = webRtcCall.isLocalOnHold, isRemoteOnHold = webRtcCall.remoteOnHold, - availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(), + availableDevices = callManager.audioManager.availableDevices, isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT, canSwitchCamera = webRtcCall.canSwitchCamera(), formattedDuration = webRtcCall.formattedDuration(), @@ -242,16 +242,11 @@ class VectorCallViewModel @AssistedInject constructor( call?.updateRemoteOnHold(!isRemoteOnHold) } is VectorCallViewActions.ChangeAudioDevice -> { - callManager.callAudioManager.setCurrentSoundDevice(action.device) - setState { - copy( - soundDevice = callManager.callAudioManager.getCurrentSoundDevice() - ) - } + callManager.audioManager.setAudioDevice(action.device) } VectorCallViewActions.SwitchSoundDevice -> { _viewEvents.post( - VectorCallViewEvents.ShowSoundDeviceChooser(state.availableSoundDevices, state.soundDevice) + VectorCallViewEvents.ShowSoundDeviceChooser(state.availableDevices, state.device) ) } VectorCallViewActions.HeadSetButtonPressed -> { diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt index 1d12b780b1..cdd002114a 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt @@ -19,6 +19,7 @@ package im.vector.app.features.call import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.call.audio.CallAudioManager import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.util.MatrixItem @@ -34,8 +35,8 @@ data class VectorCallViewState( val isHD: Boolean = false, val isFrontCamera: Boolean = true, val canSwitchCamera: Boolean = true, - val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE, - val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(), + val device: CallAudioManager.Device = CallAudioManager.Device.PHONE, + val availableDevices: Set<CallAudioManager.Device> = emptySet(), val callState: Async<CallState> = Uninitialized, val otherKnownCallInfo: CallInfo? = null, val callInfo: CallInfo = CallInfo(callId), 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 new file mode 100644 index 0000000000..8649c755f4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt @@ -0,0 +1,90 @@ +/* + * 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.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 +import java.util.HashSet + +@RequiresApi(Build.VERSION_CODES.M) +internal class API23AudioDeviceDetector(private val audioManager: AudioManager, + private val callAudioManager: CallAudioManager +) : CallAudioManager.AudioDeviceDetector{ + + private val onAudioDeviceChangeRunner = Runnable { + val devices: MutableSet<CallAudioManager.Device> = HashSet() + val deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_ALL) + for (info in deviceInfos) { + when (info.type) { + AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add(CallAudioManager.Device.WIRELESS_HEADSET) + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> devices.add(CallAudioManager.Device.PHONE) + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> devices.add(CallAudioManager.Device.SPEAKER) + AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_USB_HEADSET -> devices.add(CallAudioManager.Device.HEADSET) + } + } + callAudioManager.replaceDevices(devices) + Timber.i(" Available audio devices: $devices") + callAudioManager.updateAudioRoute() + } + private val audioDeviceCallback: AudioDeviceCallback = object : AudioDeviceCallback() { + override fun onAudioDevicesAdded( + addedDevices: Array<AudioDeviceInfo>) { + Timber.d(" Audio devices added") + onAudioDeviceChange() + } + + override fun onAudioDevicesRemoved( + removedDevices: Array<AudioDeviceInfo>) { + Timber.d(" Audio devices removed") + onAudioDeviceChange() + } + } + + /** + * 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("Using $this as the audio device handler") + audioManager.registerAudioDeviceCallback(audioDeviceCallback, null) + onAudioDeviceChange() + } + + override fun stop() { + audioManager.unregisterAudioDeviceCallback(audioDeviceCallback) + } + + companion object { + /** + * Constant defining a USB headset. Only available on API level >= 26. + * The value of: AudioDeviceInfo.TYPE_USB_HEADSET + */ + private const val TYPE_USB_HEADSET = 22 + } +} 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 new file mode 100644 index 0000000000..9518ab443f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt @@ -0,0 +1,252 @@ +/* + * 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. + */ + +package im.vector.app.features.call.audio + +import android.content.Context +import android.media.AudioManager +import org.matrix.android.sdk.api.extensions.orFalse +import timber.log.Timber +import java.util.HashSet +import java.util.concurrent.Executors + +class CallAudioManager(context: Context, val configChange: (() -> Unit)?) { + + private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + private var audioDeviceDetector: AudioDeviceDetector? = null + private var audioDeviceRouter: AudioDeviceRouter? = null + + enum class Device { + PHONE, + SPEAKER, + HEADSET, + WIRELESS_HEADSET + } + + enum class Mode { + DEFAULT, + AUDIO_CALL, + VIDEO_CALL + } + + private var mode = Mode.DEFAULT + private var _availableDevices: MutableSet<Device> = HashSet() + val availableDevices: Set<Device> + get() = _availableDevices + + var selectedDevice: Device? = null + private set + private var userSelectedDevice: Device? = null + + init { + runInAudioThread { setup() } + } + + private fun setup() { + audioDeviceDetector?.stop() + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + audioDeviceDetector = API23AudioDeviceDetector(audioManager, this) + } + audioDeviceRouter = DefaultAudioDeviceRouter(audioManager, this) + audioDeviceDetector?.start() + } + + fun runInAudioThread(runnable: Runnable) { + executor.execute(runnable) + } + + /** + * Sets the user selected audio device as the active audio device. + * + * @param device the desired device which will become active. + */ + fun setAudioDevice(device: Device) { + runInAudioThread(Runnable { + if (!_availableDevices.contains(device)) { + Timber.w(" Audio device not available: $device") + userSelectedDevice = null + return@Runnable + } + if (mode != Mode.DEFAULT) { + Timber.i(" User selected device set to: $device") + userSelectedDevice = device + updateAudioRoute(mode, false) + } + }) + } + + /** + * Public method to set the current audio mode. + * + * @param mode the desired audio mode. + * could be updated successfully, and it will be rejected otherwise. + */ + fun setMode(mode: Mode) { + runInAudioThread { + var success: Boolean + try { + success = updateAudioRoute(mode, false) + } catch (e: Throwable) { + success = false + Timber.e(e, " Failed to update audio route for mode: " + mode) + } + if (success) { + this@CallAudioManager.mode = mode + } + } + } + + /** + * Updates the audio route for the given mode. + * + * @param mode the audio mode to be used when computing the audio route. + * @return `true` if the audio route was updated successfully; + * `false`, otherwise. + */ + private fun updateAudioRoute(mode: Mode, force: Boolean): Boolean { + Timber.i(" Update audio route for mode: " + mode) + if (!audioDeviceRouter?.setMode(mode).orFalse()) { + return false + } + if (mode == Mode.DEFAULT) { + selectedDevice = null + userSelectedDevice = null + return true + } + val bluetoothAvailable = _availableDevices.contains(Device.WIRELESS_HEADSET) + val headsetAvailable = _availableDevices.contains(Device.HEADSET) + + // Pick the desired device based on what's available and the mode. + var audioDevice: Device + audioDevice = if (bluetoothAvailable) { + Device.WIRELESS_HEADSET + } else if (headsetAvailable) { + Device.HEADSET + } else if (mode == Mode.VIDEO_CALL) { + Device.SPEAKER + } else { + Device.PHONE + } + // Consider the user's selection + if (userSelectedDevice != null && _availableDevices.contains(userSelectedDevice)) { + audioDevice = userSelectedDevice!! + } + + // If the previously selected device and the current default one + // match, do nothing. + if (!force && selectedDevice != null && selectedDevice == audioDevice) { + return true + } + selectedDevice = audioDevice + Timber.i(" Selected audio device: " + audioDevice) + audioDeviceRouter?.setAudioRoute(audioDevice) + configChange?.invoke() + return true + } + + /** + * Resets the current device selection. + */ + fun resetSelectedDevice() { + selectedDevice = null + userSelectedDevice = null + } + + /** + * Adds a new device to the list of available devices. + * + * @param device The new device. + */ + fun addDevice(device: Device) { + _availableDevices.add(device) + resetSelectedDevice() + } + + /** + * Removes a device from the list of available devices. + * + * @param device The old device to the removed. + */ + fun removeDevice(device: Device) { + _availableDevices.remove(device) + resetSelectedDevice() + } + + /** + * Replaces the current list of available devices with a new one. + * + * @param devices The new devices list. + */ + fun replaceDevices(devices: MutableSet<Device>) { + _availableDevices = devices + resetSelectedDevice() + } + + /** + * Re-sets the current audio route. Needed when devices changes have happened. + */ + fun updateAudioRoute() { + if (mode != Mode.DEFAULT) { + updateAudioRoute(mode, false) + } + } + + /** + * Re-sets the current audio route. Needed when focus is lost and regained. + */ + fun resetAudioRoute() { + if (mode != Mode.DEFAULT) { + updateAudioRoute(mode, true) + } + } + + /** + * Interface for the modules implementing the actual audio device management. + */ + interface AudioDeviceDetector { + /** + * Start detecting audio device changes. + */ + fun start() + + /** + * Stop audio device detection. + */ + fun stop() + } + + interface AudioDeviceRouter { + /** + * Set the appropriate route for the given audio device. + * + * @param device Audio device for which the route must be set. + */ + fun setAudioRoute(device: Device) + + /** + * Set the given audio mode. + * + * @param mode The new audio mode to be used. + * @return Whether the operation was successful or not. + */ + fun setMode(mode: Mode): Boolean + } + + companion object { + // Every audio operations should be launched on single thread + private val executor = Executors.newSingleThreadExecutor() + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt b/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt new file mode 100644 index 0000000000..c252cc9f89 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt @@ -0,0 +1,112 @@ +/* + * 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. + */ + +package im.vector.app.features.call.audio + +import android.media.AudioManager +import androidx.media.AudioAttributesCompat +import androidx.media.AudioFocusRequestCompat +import androidx.media.AudioManagerCompat +import timber.log.Timber + +class DefaultAudioDeviceRouter(private val audioManager: AudioManager, + private val callAudioManager: CallAudioManager +) : CallAudioManager.AudioDeviceRouter, AudioManager.OnAudioFocusChangeListener { + + private var audioFocusLost = false + + private var focusRequestCompat: AudioFocusRequestCompat? = null + + override fun setAudioRoute(device: CallAudioManager.Device) { + audioManager.isSpeakerphoneOn = device === CallAudioManager.Device.SPEAKER + setBluetoothAudioRoute(device === CallAudioManager.Device.WIRELESS_HEADSET) + } + + override fun setMode(mode: CallAudioManager.Mode): Boolean { + if (mode === CallAudioManager.Mode.DEFAULT) { + audioFocusLost = false + audioManager.mode = AudioManager.MODE_NORMAL + focusRequestCompat?.also { + AudioManagerCompat.abandonAudioFocusRequest(audioManager, it) + } + focusRequestCompat = null + audioManager.isSpeakerphoneOn = false + setBluetoothAudioRoute(false) + return true + } + audioManager.mode = AudioManager.MODE_IN_COMMUNICATION + audioManager.isMicrophoneMute = false + + val audioFocusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) + .setAudioAttributes( + AudioAttributesCompat.Builder() + .setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION) + .setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH) + .build() + ) + .setOnAudioFocusChangeListener(this) + .build() + .also { + focusRequestCompat = it + } + + val gotFocus = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest) + if (gotFocus == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { + Timber.w(" Audio focus request failed") + return false + } + return true + } + + /** + * Helper method to set the output route to a Bluetooth device. + * + * @param enabled true if Bluetooth should use used, false otherwise. + */ + private fun setBluetoothAudioRoute(enabled: Boolean) { + if (enabled) { + audioManager.startBluetoothSco() + audioManager.isBluetoothScoOn = true + } else { + audioManager.isBluetoothScoOn = false + audioManager.stopBluetoothSco() + } + } + + /** + * [AudioManager.OnAudioFocusChangeListener] interface method. Called + * when the audio focus of the system is updated. + * + * @param focusChange - The type of focus change. + */ + override fun onAudioFocusChange(focusChange: Int) { + callAudioManager.runInAudioThread { + when (focusChange) { + AudioManager.AUDIOFOCUS_GAIN -> { + Timber.d(" Audio focus gained") + if (audioFocusLost) { + callAudioManager.resetAudioRoute() + } + audioFocusLost = false + } + AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + Timber.d(" Audio focus lost") + audioFocusLost = true + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/telecom/CallConnectionService.java b/vector/src/main/java/im/vector/app/features/call/telecom/CallConnectionService.java new file mode 100644 index 0000000000..f3fbfc9ac4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/telecom/CallConnectionService.java @@ -0,0 +1,27 @@ +/* + * 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. + */ + +package im.vector.app.features.call.telecom; + +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import org.jitsi.meet.sdk.ConnectionService; + +@RequiresApi(api = Build.VERSION_CODES.O) +public class CallConnectionService extends ConnectionService { +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionService.kt b/vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionService.kt index 410a4621e8..e289537177 100644 --- a/vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionService.kt +++ b/vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionService.kt @@ -71,7 +71,7 @@ import im.vector.app.core.services.CallService bindService(Intent(applicationContext, CallService::class.java), CallServiceConnection(connection), 0) connection.setInitializing() - return CallConnection(applicationContext, roomId, callId) + return connection } inner class CallServiceConnection(private val callConnection: CallConnection) : ServiceConnection { diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt index f6e2caf72c..b476d7dbe6 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt @@ -16,7 +16,7 @@ package im.vector.app.features.call.webrtc -import im.vector.app.features.call.CallAudioManager +import im.vector.app.features.call.audio.CallAudioManager import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.webrtc.DataChannel @@ -26,8 +26,7 @@ import org.webrtc.PeerConnection import org.webrtc.RtpReceiver import timber.log.Timber -class PeerConnectionObserver(private val webRtcCall: WebRtcCall, - private val callAudioManager: CallAudioManager) : PeerConnection.Observer { +class PeerConnectionObserver(private val webRtcCall: WebRtcCall) : PeerConnection.Observer { override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) { Timber.v("## VOIP StreamObserver onConnectionChange: $newState") @@ -38,7 +37,6 @@ class PeerConnectionObserver(private val webRtcCall: WebRtcCall, */ PeerConnection.PeerConnectionState.CONNECTED -> { webRtcCall.mxCall.state = CallState.Connected(MxPeerConnectionState.CONNECTED) - callAudioManager.onCallConnected(webRtcCall.mxCall) } /** * One or more of the ICE transports on the connection is in the "failed" state. diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 28ca503507..95e7e92646 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -21,7 +21,7 @@ import android.hardware.camera2.CameraManager import androidx.core.content.getSystemService import im.vector.app.core.services.CallService import im.vector.app.core.utils.CountUpTimer -import im.vector.app.features.call.CallAudioManager +import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.CameraEventsHandlerAdapter import im.vector.app.features.call.CameraProxy import im.vector.app.features.call.CameraType @@ -86,7 +86,6 @@ private const val VIDEO_TRACK_ID = "ARDAMSv0" private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints() class WebRtcCall(val mxCall: MxCall, - private val callAudioManager: CallAudioManager, private val rootEglBase: EglBase?, private val context: Context, private val dispatcher: CoroutineContext, @@ -256,7 +255,7 @@ class WebRtcCall(val mxCall: MxCall, val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply { sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN } - peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, PeerConnectionObserver(this, callAudioManager)) + peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, PeerConnectionObserver(this)) } fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) { @@ -317,6 +316,9 @@ class WebRtcCall(val mxCall: MxCall, } private suspend fun setupOutgoingCall() = withContext(dispatcher) { + tryOrNull { + onCallBecomeActive(this@WebRtcCall) + } val turnServer = getTurnServer() mxCall.state = CallState.CreateOffer // 1. Create RTCPeerConnection diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 18720a6c19..f7f50b4681 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -24,8 +24,8 @@ import im.vector.app.ActiveSessionDataSource import im.vector.app.core.services.BluetoothHeadsetReceiver import im.vector.app.core.services.CallService import im.vector.app.core.services.WiredHeadsetStateReceiver -import im.vector.app.features.call.CallAudioManager import im.vector.app.features.call.VectorCallActivity +import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.utils.EglUtils import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.asCoroutineDispatcher @@ -79,10 +79,12 @@ class WebRtcCallManager @Inject constructor( currentCallsListeners.remove(listener) } - val callAudioManager = CallAudioManager(context) { + val audioManager = CallAudioManager(context) { currentCallsListeners.forEach { tryOrNull { it.onAudioDevicesChange() } } + }.apply { + setMode(CallAudioManager.Mode.DEFAULT) } private var peerConnectionFactory: PeerConnectionFactory? = null @@ -180,13 +182,13 @@ class WebRtcCallManager @Inject constructor( Timber.v("## VOIP WebRtcPeerConnectionManager onCall active: ${call.mxCall.callId}") val currentCall = getCurrentCall().takeIf { it != call } currentCall?.updateRemoteOnHold(onHold = true) + audioManager.setMode(if (call.mxCall.isVideoCall) CallAudioManager.Mode.VIDEO_CALL else CallAudioManager.Mode.AUDIO_CALL) this.currentCall.setAndNotify(call) } private fun onCallEnded(call: WebRtcCall) { Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: ${call.mxCall.callId}") CallService.onCallTerminated(context, call.callId) - callAudioManager.stop() callsByCallId.remove(call.mxCall.callId) callsByRoomId[call.mxCall.roomId]?.remove(call) if (getCurrentCall() == call) { @@ -199,6 +201,7 @@ class WebRtcCallManager @Inject constructor( Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one") peerConnectionFactory?.dispose() peerConnectionFactory = null + audioManager.setMode(CallAudioManager.Mode.DEFAULT) } Timber.v("## VOIP WebRtcPeerConnectionManager close() executor done") } @@ -222,7 +225,7 @@ class WebRtcCallManager @Inject constructor( val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return val webRtcCall = createWebRtcCall(mxCall) currentCall.setAndNotify(webRtcCall) - callAudioManager.startForCall(mxCall) + //callAudioManager.startForCall(mxCall) CallService.onOutgoingCallRinging( context = context.applicationContext, @@ -244,7 +247,6 @@ class WebRtcCallManager @Inject constructor( private fun createWebRtcCall(mxCall: MxCall): WebRtcCall { val webRtcCall = WebRtcCall( mxCall = mxCall, - callAudioManager = callAudioManager, rootEglBase = rootEglBase, context = context, dispatcher = dispatcher, @@ -270,12 +272,12 @@ class WebRtcCallManager @Inject constructor( Timber.v("## VOIP onWiredDeviceEvent $event") getCurrentCall() ?: return // sometimes we received un-wanted unplugged... - callAudioManager.wiredStateChange(event) + //callAudioManager.wiredStateChange(event) } fun onWirelessDeviceEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) { Timber.v("## VOIP onWirelessDeviceEvent $event") - callAudioManager.bluetoothStateChange(event.plugged) + //callAudioManager.bluetoothStateChange(event.plugged) } override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) { @@ -292,7 +294,7 @@ class WebRtcCallManager @Inject constructor( createWebRtcCall(mxCall).apply { offerSdp = callInviteContent.offer } - callAudioManager.startForCall(mxCall) + //callAudioManager.startForCall(mxCall) // Start background service with notification CallService.onIncomingCallRinging( context = context,