diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 50c9eabadb..6b0253c5fc 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -14,6 +14,8 @@ + + diff --git a/vector/src/main/java/im/vector/riotx/features/call/CallAudioManager.kt b/vector/src/main/java/im/vector/riotx/features/call/CallAudioManager.kt new file mode 100644 index 0000000000..a5c3069367 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/call/CallAudioManager.kt @@ -0,0 +1,115 @@ +/* + * 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.riotx.features.call + +import android.content.Context +import android.content.pm.PackageManager +import android.media.AudioManager +import im.vector.matrix.android.api.session.call.MxCall +import timber.log.Timber + +class CallAudioManager( + val applicationContext: Context +) { + + private val audioManager: AudioManager = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + private var savedIsSpeakerPhoneOn = false + private var savedIsMicrophoneMute = false + private var savedAudioMode = AudioManager.MODE_INVALID + + 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}") + 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) + + // TODO check if there are headsets? + if (mxCall.isVideoCall) { + setSpeakerphoneOn(true) + } + } + + fun stop() { + Timber.v("## VOIP: AudioManager stopCall") + + // Restore previously stored audio states. + setSpeakerphoneOn(savedIsSpeakerPhoneOn) + setMicrophoneMute(savedIsMicrophoneMute) + audioManager.mode = savedAudioMode + + @Suppress("DEPRECATION") + audioManager.abandonAudioFocus(audioFocusChangeListener) + } + + /** Sets the speaker phone mode. */ + private fun setSpeakerphoneOn(on: Boolean) { + Timber.v("## VOIP: AudioManager setSpeakerphoneOn $on") + val wasOn = audioManager.isSpeakerphoneOn + 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 + if (wasMuted == on) { + return + } + audioManager.isMicrophoneMute = on + + audioManager.isMusicActive + } + + /** 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/riotx/features/call/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt index 693b6168a6..f40d02d2f8 100644 --- a/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt @@ -82,6 +82,8 @@ class WebRtcPeerConnectionManager @Inject constructor( currentCallsListeners.remove(listener) } + val audioManager = CallAudioManager(context.applicationContext) + data class CallContext( val mxCall: MxCall, @@ -457,6 +459,8 @@ class WebRtcPeerConnectionManager @Inject constructor( Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") val createdCall = sessionHolder.getSafeActiveSession()?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return val callContext = CallContext(createdCall) + + audioManager.startForCall(createdCall) currentCall = callContext executor.execute { @@ -489,11 +493,13 @@ class WebRtcPeerConnectionManager @Inject constructor( if (currentCall != null) { Timber.w("## VOIP TODO: Automatically reject incoming call?") mxCall.hangUp() + audioManager.stop() return } val callContext = CallContext(mxCall) currentCall = callContext + audioManager.startForCall(mxCall) executor.execute { callContext.remoteCandidateSource = ReplaySubject.create() } @@ -538,6 +544,7 @@ class WebRtcPeerConnectionManager @Inject constructor( fun endCall() { currentCall?.mxCall?.hangUp() currentCall = null + audioManager.stop() close() } @@ -602,7 +609,6 @@ class WebRtcPeerConnectionManager @Inject constructor( * property until the May 13, 2016 draft of the specification. */ PeerConnection.PeerConnectionState.CLOSED -> { - } /** * At least one of the ICE transports for the connection is in the "disconnected" state and none of