Merge pull request #2101 from vector-im/feature/clean_call

Feature/clean call
This commit is contained in:
Benoit Marty 2020-09-15 17:43:49 +02:00 committed by GitHub
commit 23aaa58834
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 149 additions and 84 deletions

View File

@ -0,0 +1,32 @@
/*
* 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 im.vector.app.core.platform.VectorViewModelAction
sealed class VectorCallViewActions : VectorViewModelAction {
object EndCall : VectorCallViewActions()
object AcceptCall : VectorCallViewActions()
object DeclineCall : VectorCallViewActions()
object ToggleMute : VectorCallViewActions()
object ToggleVideo : VectorCallViewActions()
data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions()
object SwitchSoundDevice : VectorCallViewActions()
object HeadSetButtonPressed : VectorCallViewActions()
object ToggleCamera : VectorCallViewActions()
object ToggleHDSD : VectorCallViewActions()
}

View File

@ -0,0 +1,33 @@
/*
* 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 im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.session.call.TurnServerResponse
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
) : VectorCallViewEvents()
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
// object CallAccepted : VectorCallViewEvents()
}

View File

@ -16,10 +16,8 @@
package im.vector.app.features.call
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
@ -27,9 +25,7 @@ import com.airbnb.mvrx.ViewModelContext
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.VectorViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallState
@ -41,48 +37,6 @@ import org.webrtc.PeerConnection
import java.util.Timer
import java.util.TimerTask
data class VectorCallViewState(
val callId: String? = null,
val roomId: String = "",
val isVideoCall: Boolean,
val isAudioMuted: Boolean = false,
val isVideoEnabled: Boolean = true,
val isVideoCaptureInError: Boolean = false,
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 otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
val callState: Async<CallState> = Uninitialized
) : MvRxState
sealed class VectorCallViewActions : VectorViewModelAction {
object EndCall : VectorCallViewActions()
object AcceptCall : VectorCallViewActions()
object DeclineCall : VectorCallViewActions()
object ToggleMute : VectorCallViewActions()
object ToggleVideo : VectorCallViewActions()
data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions()
object SwitchSoundDevice : VectorCallViewActions()
object HeadSetButtonPressed : VectorCallViewActions()
object ToggleCamera : VectorCallViewActions()
object ToggleHDSD : VectorCallViewActions()
}
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
) : VectorCallViewEvents()
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
// object CallAccepted : VectorCallViewEvents()
}
class VectorCallViewModel @AssistedInject constructor(
@Assisted initialState: VectorCallViewState,
@Assisted val args: CallArgs,
@ -91,23 +45,23 @@ class VectorCallViewModel @AssistedInject constructor(
val proximityManager: CallProximityManager
) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) {
var call: MxCall? = null
private var call: MxCall? = null
var connectionTimoutTimer: Timer? = null
var hasBeenConnectedOnce = false
private var connectionTimeoutTimer: Timer? = null
private var hasBeenConnectedOnce = false
private val callStateListener = object : MxCall.StateListener {
override fun onStateUpdate(call: MxCall) {
val callState = call.state
if (callState is CallState.Connected && callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) {
hasBeenConnectedOnce = true
connectionTimoutTimer?.cancel()
connectionTimoutTimer = null
connectionTimeoutTimer?.cancel()
connectionTimeoutTimer = null
} else {
// do we reset as long as it's moving?
connectionTimoutTimer?.cancel()
connectionTimeoutTimer?.cancel()
if (hasBeenConnectedOnce) {
connectionTimoutTimer = Timer().apply {
connectionTimeoutTimer = Timer().apply {
schedule(object : TimerTask() {
override fun run() {
session.callSignalingService().getTurnServer(object : MatrixCallback<TurnServerResponse> {
@ -136,17 +90,17 @@ class VectorCallViewModel @AssistedInject constructor(
override fun onCurrentCallChange(call: MxCall?) {
}
override fun onCaptureStateChanged(mgr: WebRtcPeerConnectionManager) {
override fun onCaptureStateChanged() {
setState {
copy(
isVideoCaptureInError = mgr.capturerIsInError,
isHD = mgr.currentCaptureFormat() is CaptureFormat.HD
isVideoCaptureInError = webRtcPeerConnectionManager.capturerIsInError,
isHD = webRtcPeerConnectionManager.currentCaptureFormat() is CaptureFormat.HD
)
}
}
override fun onAudioDevicesChange(mgr: WebRtcPeerConnectionManager) {
val currentSoundDevice = mgr.audioManager.getCurrentSoundDevice()
override fun onAudioDevicesChange() {
val currentSoundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice()
if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) {
proximityManager.start()
} else {
@ -155,17 +109,17 @@ class VectorCallViewModel @AssistedInject constructor(
setState {
copy(
availableSoundDevices = mgr.audioManager.getAvailableSoundDevices(),
availableSoundDevices = webRtcPeerConnectionManager.callAudioManager.getAvailableSoundDevices(),
soundDevice = currentSoundDevice
)
}
}
override fun onCameraChange(mgr: WebRtcPeerConnectionManager) {
override fun onCameraChange() {
setState {
copy(
canSwitchCamera = mgr.canSwitchCamera(),
isFrontCamera = mgr.currentCameraType() == CameraType.FRONT
canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera(),
isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT
)
}
}
@ -182,7 +136,7 @@ class VectorCallViewModel @AssistedInject constructor(
mxCall.addListener(callStateListener)
val currentSoundDevice = webRtcPeerConnectionManager.audioManager.getCurrentSoundDevice()
val currentSoundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice()
if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) {
proximityManager.start()
}
@ -193,7 +147,7 @@ class VectorCallViewModel @AssistedInject constructor(
callState = Success(mxCall.state),
otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized,
soundDevice = currentSoundDevice,
availableSoundDevices = webRtcPeerConnectionManager.audioManager.getAvailableSoundDevices(),
availableSoundDevices = webRtcPeerConnectionManager.callAudioManager.getAvailableSoundDevices(),
isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT,
canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera(),
isHD = mxCall.isVideoCall && webRtcPeerConnectionManager.currentCaptureFormat() is CaptureFormat.HD
@ -250,10 +204,10 @@ class VectorCallViewModel @AssistedInject constructor(
Unit
}
is VectorCallViewActions.ChangeAudioDevice -> {
webRtcPeerConnectionManager.audioManager.setCurrentSoundDevice(action.device)
webRtcPeerConnectionManager.callAudioManager.setCurrentSoundDevice(action.device)
setState {
copy(
soundDevice = webRtcPeerConnectionManager.audioManager.getCurrentSoundDevice()
soundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice()
)
}
}

View File

@ -0,0 +1,39 @@
/*
* 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 com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.util.MatrixItem
data class VectorCallViewState(
val callId: String? = null,
val roomId: String = "",
val isVideoCall: Boolean,
val isAudioMuted: Boolean = false,
val isVideoEnabled: Boolean = true,
val isVideoCaptureInError: Boolean = false,
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 otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
val callState: Async<CallState> = Uninitialized
) : MvRxState

View File

@ -23,6 +23,9 @@ 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 io.reactivex.disposables.Disposable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.ReplaySubject
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.session.Session
@ -35,9 +38,6 @@ import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import io.reactivex.disposables.Disposable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.ReplaySubject
import org.webrtc.AudioSource
import org.webrtc.AudioTrack
import org.webrtc.Camera1Enumerator
@ -79,9 +79,9 @@ class WebRtcPeerConnectionManager @Inject constructor(
interface CurrentCallListener {
fun onCurrentCallChange(call: MxCall?)
fun onCaptureStateChanged(mgr: WebRtcPeerConnectionManager) {}
fun onAudioDevicesChange(mgr: WebRtcPeerConnectionManager) {}
fun onCameraChange(mgr: WebRtcPeerConnectionManager) {}
fun onCaptureStateChanged() {}
fun onAudioDevicesChange() {}
fun onCameraChange() {}
}
private val currentCallsListeners = emptyList<CurrentCallListener>().toMutableList()
@ -93,9 +93,9 @@ class WebRtcPeerConnectionManager @Inject constructor(
currentCallsListeners.remove(listener)
}
val audioManager = CallAudioManager(context.applicationContext) {
val callAudioManager = CallAudioManager(context.applicationContext) {
currentCallsListeners.forEach {
tryThis { it.onAudioDevicesChange(this) }
tryThis { it.onAudioDevicesChange() }
}
}
@ -174,7 +174,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
set(value) {
field = value
currentCallsListeners.forEach {
tryThis { it.onCaptureStateChanged(this) }
tryThis { it.onCaptureStateChanged() }
}
}
@ -577,7 +577,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
fun close() {
Timber.v("## VOIP WebRtcPeerConnectionManager close() >")
CallService.onNoActiveCall(context)
audioManager.stop()
callAudioManager.stop()
val callToEnd = currentCall
currentCall = null
// This must be done in this thread
@ -631,7 +631,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
val createdCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
val callContext = CallContext(createdCall)
audioManager.startForCall(createdCall)
callAudioManager.startForCall(createdCall)
currentCall = callContext
val name = currentSession?.getUser(createdCall.otherUserId)?.getBestName()
@ -684,7 +684,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
val callContext = CallContext(mxCall)
currentCall = callContext
audioManager.startForCall(mxCall)
callAudioManager.startForCall(mxCall)
executor.execute {
callContext.remoteCandidateSource = ReplaySubject.create()
}
@ -745,7 +745,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
}
currentCallsListeners.forEach {
tryThis { it.onCameraChange(this@WebRtcPeerConnectionManager) }
tryThis { it.onCameraChange() }
}
}
@ -771,7 +771,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
// videoCapturer?.stopCapture()
videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps)
currentCaptureMode = format
currentCallsListeners.forEach { tryThis { it.onCaptureStateChanged(this) } }
currentCallsListeners.forEach { tryThis { it.onCaptureStateChanged() } }
}
}
@ -802,12 +802,12 @@ class WebRtcPeerConnectionManager @Inject constructor(
Timber.v("## VOIP onWiredDeviceEvent $event")
currentCall ?: return
// sometimes we received un-wanted unplugged...
audioManager.wiredStateChange(event)
callAudioManager.wiredStateChange(event)
}
fun onWirelessDeviceEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
Timber.v("## VOIP onWirelessDeviceEvent $event")
audioManager.bluetoothStateChange(event.plugged)
callAudioManager.bluetoothStateChange(event.plugged)
}
override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
@ -862,7 +862,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
*/
PeerConnection.PeerConnectionState.CONNECTED -> {
callContext.mxCall.state = CallState.Connected(newState)
audioManager.onCallConnected(callContext.mxCall)
callAudioManager.onCallConnected(callContext.mxCall)
}
/**
* One or more of the ICE transports on the connection is in the "failed" state.

View File

@ -20,6 +20,7 @@ import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.preference.Preference
import im.vector.app.BuildConfig
import org.matrix.android.sdk.api.Matrix
import im.vector.app.R
import im.vector.app.core.preference.VectorPreference
@ -58,7 +59,13 @@ class VectorSettingsHelpAboutFragment @Inject constructor(
// application version
findPreference<VectorPreference>(VectorPreferences.SETTINGS_VERSION_PREFERENCE_KEY)!!.let {
it.summary = versionProvider.getVersion(longFormat = false, useBuildNumber = true)
it.summary = buildString {
append(versionProvider.getVersion(longFormat = false, useBuildNumber = true))
if (BuildConfig.DEBUG) {
append(" ")
append(BuildConfig.GIT_BRANCH_NAME)
}
}
it.setOnPreferenceClickListener { pref ->
copyToClipboard(requireContext(), pref.summary)