VoIP: start reworking audio device management [WIP]

This commit is contained in:
ganfra 2021-01-21 13:03:33 +01:00
parent dd67e8c5b5
commit d29ab94617
15 changed files with 534 additions and 369 deletions

View File

@ -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"

View File

@ -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)
}
}

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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 -> {

View File

@ -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),

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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 {
}

View File

@ -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 {

View File

@ -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.

View File

@ -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

View File

@ -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,