VoIP: show duration

This commit is contained in:
ganfra 2020-12-22 12:26:10 +01:00
parent a5736efc75
commit 81f7932cb7
9 changed files with 207 additions and 42 deletions

View file

@ -18,11 +18,13 @@ package im.vector.app.core.ui.views
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.RelativeLayout
import im.vector.app.R
import im.vector.app.databinding.ViewCallControlsBinding
import im.vector.app.databinding.ViewCurrentCallsBinding
import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.themes.ThemeUtils
import kotlinx.android.synthetic.main.view_current_calls.view.*
import org.matrix.android.sdk.api.session.call.CallState
class CurrentCallsView @JvmOverloads constructor(
@ -35,19 +37,17 @@ class CurrentCallsView @JvmOverloads constructor(
fun onTapToReturnToCall()
}
val views: ViewCurrentCallsBinding
var callback: Callback? = null
init {
setupView()
}
private fun setupView() {
inflate(context, R.layout.view_current_calls, this)
views = ViewCurrentCallsBinding.bind(this)
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
setOnClickListener { callback?.onTapToReturnToCall() }
}
fun render(calls: List<WebRtcCall>) {
fun render(calls: List<WebRtcCall>, formattedDuration: String) {
val connectedCalls = calls.filter {
it.mxCall.state is CallState.Connected
}
@ -56,17 +56,17 @@ class CurrentCallsView @JvmOverloads constructor(
}
if (connectedCalls.size == 1) {
if (heldCalls.size == 1) {
currentCallsInfo.setText(R.string.call_only_paused)
views.currentCallsInfo.setText(R.string.call_only_paused)
} else {
currentCallsInfo.setText(R.string.call_only_active)
views.currentCallsInfo.text = resources.getString(R.string.call_only_active, formattedDuration)
}
} else {
if (heldCalls.size > 1) {
currentCallsInfo.text = resources.getString(R.string.call_only_multiple_paused , heldCalls.size)
views.currentCallsInfo.text = resources.getString(R.string.call_only_multiple_paused , heldCalls.size)
} else if (heldCalls.size == 1) {
currentCallsInfo.setText(R.string.call_active_and_single_paused)
views.currentCallsInfo.text = resources.getString(R.string.call_active_and_single_paused, formattedDuration)
} else {
currentCallsInfo.text = resources.getString(R.string.call_active_and_multiple_paused, "00:00", heldCalls.size)
views.currentCallsInfo.text = resources.getString(R.string.call_active_and_multiple_paused, formattedDuration, heldCalls.size)
}
}
}

View file

@ -25,34 +25,44 @@ import im.vector.app.features.call.webrtc.WebRtcCall
import org.webrtc.RendererCommon
import org.webrtc.SurfaceViewRenderer
class ActiveCallViewHolder {
class KnownCallsViewHolder {
private var activeCallPiP: SurfaceViewRenderer? = null
private var activeCallView: CurrentCallsView? = null
private var currentCallsView: CurrentCallsView? = null
private var pipWrapper: CardView? = null
private var activeCall: WebRtcCall? = null
private var currentCall: WebRtcCall? = null
private var calls: List<WebRtcCall> = emptyList()
private var activeCallPipInitialized = false
fun updateCall(activeCall: WebRtcCall?, calls: List<WebRtcCall>) {
this.activeCall = activeCall
val hasActiveCall = activeCall?.mxCall?.state is CallState.Connected
private val tickListener = object : WebRtcCall.Listener {
override fun onTick(formattedDuration: String) {
currentCallsView?.render(calls, formattedDuration)
}
}
fun updateCall(currentCall: WebRtcCall?, calls: List<WebRtcCall>) {
this.currentCall?.removeListener(tickListener)
this.currentCall = currentCall
this.currentCall?.addListener(tickListener)
this.calls = calls
val hasActiveCall = currentCall?.mxCall?.state is CallState.Connected
if (hasActiveCall) {
val isVideoCall = activeCall?.mxCall?.isVideoCall == true
val isVideoCall = currentCall?.mxCall?.isVideoCall == true
if (isVideoCall) initIfNeeded()
activeCallView?.isVisible = !isVideoCall
activeCallView?.render(calls)
currentCallsView?.isVisible = !isVideoCall
currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "")
pipWrapper?.isVisible = isVideoCall
activeCallPiP?.isVisible = isVideoCall
activeCallPiP?.let {
activeCall?.attachViewRenderers(null, it, null)
currentCall?.attachViewRenderers(null, it, null)
}
} else {
activeCallView?.isVisible = false
currentCallsView?.isVisible = false
activeCallPiP?.isVisible = false
pipWrapper?.isVisible = false
activeCallPiP?.let {
activeCall?.detachRenderers(listOf(it))
currentCall?.detachRenderers(listOf(it))
}
}
}
@ -72,27 +82,29 @@ class ActiveCallViewHolder {
fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: CurrentCallsView, pipWrapper: CardView, interactionListener: CurrentCallsView.Callback) {
this.activeCallPiP = activeCallPiP
this.activeCallView = activeCallView
this.currentCallsView = activeCallView
this.pipWrapper = pipWrapper
this.activeCallView?.callback = interactionListener
this.currentCallsView?.callback = interactionListener
pipWrapper.setOnClickListener(
DebouncedClickListener({ _ ->
interactionListener.onTapToReturnToCall()
})
)
this.currentCall?.addListener(tickListener)
}
fun unBind() {
activeCallPiP?.let {
activeCall?.detachRenderers(listOf(it))
currentCall?.detachRenderers(listOf(it))
}
if (activeCallPipInitialized) {
activeCallPiP?.release()
}
this.activeCallView?.callback = null
this.currentCallsView?.callback = null
this.currentCall?.removeListener(tickListener)
pipWrapper?.setOnClickListener(null)
activeCallPiP = null
activeCallView = null
currentCallsView = null
pipWrapper = null
}
}

View file

@ -0,0 +1,97 @@
/*
* Copyright 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.core.utils
import android.os.Handler
import android.os.SystemClock
class CountUpTimer(private val intervalInMs: Long) {
private var startTimestamp: Long = 0
private var delayTime: Long = 0
private var lastPauseTimestamp: Long = 0
private var isRunning: Boolean = false
var tickListener: TickListener? = null
private val tickHandler: Handler = Handler()
private val tickSelector = Runnable {
if (isRunning) {
tickListener?.onTick(time)
startTicking()
}
}
init {
reset()
}
/**
* Reset the timer, also clears all laps information. Running status will not affected
*/
fun reset() {
startTimestamp = SystemClock.elapsedRealtime()
delayTime = 0
lastPauseTimestamp = startTimestamp
}
/**
* Pause the timer
*/
fun pause() {
if (isRunning) {
lastPauseTimestamp = SystemClock.elapsedRealtime()
isRunning = false
stopTicking()
}
}
/**
* Resume the timer
*/
fun resume() {
if (!isRunning) {
val currentTime: Long = SystemClock.elapsedRealtime()
delayTime += currentTime - lastPauseTimestamp
isRunning = true
startTicking()
}
}
val time: Long
get() = if (isRunning) {
SystemClock.elapsedRealtime() - startTimestamp - delayTime
} else {
lastPauseTimestamp - startTimestamp - delayTime
}
private fun startTicking() {
tickHandler.removeCallbacksAndMessages(null)
val time = time
val remainingTimeInInterval = intervalInMs - time % intervalInMs
tickHandler.postDelayed(tickSelector, remainingTimeInInterval)
}
private fun stopTicking() {
tickHandler.removeCallbacksAndMessages(null)
}
interface TickListener {
fun onTick(milliseconds: Long)
}
}

View file

@ -50,6 +50,7 @@ import im.vector.app.features.home.room.detail.RoomDetailActivity
import im.vector.app.features.home.room.detail.RoomDetailArgs
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.parcelize.Parcelize
import okhttp3.internal.concurrent.formatDuration
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCallDetail
@ -205,7 +206,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
}
}
} else {
views.callStatusText.text = null
views.callStatusText.text = state.formattedDuration
if (callArgs.isVideoCall) {
views.callVideoGroup.isVisible = true
views.callInfoGroup.isVisible = false

View file

@ -79,6 +79,12 @@ class VectorCallViewModel @AssistedInject constructor(
}
}
override fun onTick(formattedDuration: String) {
setState {
copy(formattedDuration = formattedDuration)
}
}
override fun onStateUpdate(call: MxCall) {
val callState = call.state
if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
@ -176,8 +182,9 @@ class VectorCallViewModel @AssistedInject constructor(
isLocalOnHold = webRtcCall.isLocalOnHold(),
isRemoteOnHold = webRtcCall.remoteOnHold,
availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(),
isFrontCamera = call?.currentCameraType() == CameraType.FRONT,
canSwitchCamera = call?.canSwitchCamera() ?: false,
isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT,
canSwitchCamera = webRtcCall.canSwitchCamera(),
formattedDuration = webRtcCall.formattedDuration(),
isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD
)
}

View file

@ -38,7 +38,8 @@ data class VectorCallViewState(
val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(),
val callState: Async<CallState> = Uninitialized,
val otherKnownCallInfo: CallInfo? = null,
val callInfo: CallInfo = CallInfo(callId)
val callInfo: CallInfo = CallInfo(callId),
val formattedDuration: String = ""
) : MvRxState {
data class CallInfo(

View file

@ -20,6 +20,7 @@ import android.content.Context
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.CameraEventsHandlerAdapter
import im.vector.app.features.call.CameraProxy
@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.matrix.android.sdk.api.session.call.TurnServerResponse
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
@ -53,6 +55,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
import org.matrix.android.sdk.api.session.room.model.call.SdpType
import org.matrix.android.sdk.internal.util.awaitCallback
import org.threeten.bp.Duration
import org.webrtc.AudioSource
import org.webrtc.AudioTrack
import org.webrtc.Camera1Enumerator
@ -96,6 +99,8 @@ class WebRtcCall(val mxCall: MxCall,
fun onCaptureStateChanged() {}
fun onCameraChanged() {}
fun onHoldUnhold() {}
fun onTick(formattedDuration: String) {}
override fun onStateUpdate(call: MxCall) {}
}
private val listeners = CopyOnWriteArrayList<Listener>()
@ -130,6 +135,18 @@ class WebRtcCall(val mxCall: MxCall,
private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD
private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null
private val timer = CountUpTimer(1000).apply {
tickListener = object : CountUpTimer.TickListener {
override fun onTick(milliseconds: Long) {
val formattedDuration = formatDuration(Duration.ofMillis(milliseconds))
listeners.forEach {
tryOrNull { it.onTick(formattedDuration) }
}
}
}
}
// Mute status
var micMuted = false
private set
@ -209,6 +226,12 @@ class WebRtcCall(val mxCall: MxCall,
}
}
fun formattedDuration(): String {
return formatDuration(
Duration.ofMillis(timer.time)
)
}
private fun createPeerConnection(turnServerResponse: TurnServerResponse?) {
val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return
val iceServers = mutableListOf<PeerConnection.IceServer>().apply {
@ -547,7 +570,7 @@ class WebRtcCall(val mxCall: MxCall,
return callOnHold
}
fun updateRemoteOnHold(onHold: Boolean) = synchronized(this){
fun updateRemoteOnHold(onHold: Boolean) = synchronized(this) {
if (remoteOnHold == onHold) return
remoteOnHold = onHold
if (!onHold) {
@ -574,11 +597,11 @@ class WebRtcCall(val mxCall: MxCall,
updateMuteStatus()
}
fun canSwitchCamera(): Boolean = synchronized(this){
fun canSwitchCamera(): Boolean = synchronized(this) {
return availableCamera.size > 1
}
private fun getOppositeCameraIfAny(): CameraProxy? = synchronized(this){
private fun getOppositeCameraIfAny(): CameraProxy? = synchronized(this) {
val currentCamera = cameraInUse ?: return null
return if (currentCamera.type == CameraType.FRONT) {
availableCamera.firstOrNull { it.type == CameraType.BACK }
@ -587,7 +610,7 @@ class WebRtcCall(val mxCall: MxCall,
}
}
fun switchCamera() = synchronized(this){
fun switchCamera() = synchronized(this) {
Timber.v("## VOIP switchCamera")
if (mxCall.state is CallState.Connected && mxCall.isVideoCall) {
val oppositeCamera = getOppositeCameraIfAny() ?: return
@ -630,7 +653,7 @@ class WebRtcCall(val mxCall: MxCall,
}
}
fun currentCameraType(): CameraType? = synchronized(this){
fun currentCameraType(): CameraType? = synchronized(this) {
return cameraInUse?.type
}
@ -638,8 +661,10 @@ class WebRtcCall(val mxCall: MxCall,
return currentCaptureFormat
}
private fun release() {
private fun release() {
mxCall.removeListener(this)
timer.reset()
timer.tickListener = null
videoCapturer?.stopCapture()
videoCapturer?.dispose()
videoCapturer = null
@ -784,6 +809,11 @@ class WebRtcCall(val mxCall: MxCall,
}
val nowOnHold = isLocalOnHold()
if (prevOnHold != nowOnHold) {
if (nowOnHold) {
timer.pause()
} else {
timer.resume()
}
listeners.forEach {
tryOrNull { it.onHoldUnhold() }
}
@ -791,9 +821,26 @@ class WebRtcCall(val mxCall: MxCall,
}
}
private fun formatDuration(duration: Duration): String {
val hours = duration.seconds / 3600
val minutes = (duration.seconds % 3600) / 60
val seconds = duration.seconds % 60
return if (hours > 0) {
String.format("%d:%02d:%02d", hours, minutes, seconds)
} else {
String.format("%02d:%02d", minutes, seconds)
}
}
// MxCall.StateListener
override fun onStateUpdate(call: MxCall) {
val state = call.state
if (state is CallState.Connected && state.iceConnectionState == MxPeerConnectionState.CONNECTED) {
timer.resume()
} else {
timer.pause()
}
listeners.forEach {
tryOrNull { it.onStateUpdate(call) }
}

View file

@ -33,7 +33,7 @@ import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.ui.views.CurrentCallsView
import im.vector.app.core.ui.views.ActiveCallViewHolder
import im.vector.app.core.ui.views.KnownCallsViewHolder
import im.vector.app.core.ui.views.KeysBackupBanner
import im.vector.app.databinding.FragmentHomeDetailBinding
import im.vector.app.features.call.SharedKnownCallsViewModel
@ -83,7 +83,7 @@ class HomeDetailFragment @Inject constructor(
return FragmentHomeDetailBinding.inflate(inflater, container, false)
}
private val activeCallViewHolder = ActiveCallViewHolder()
private val activeCallViewHolder = KnownCallsViewHolder()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View file

@ -90,7 +90,7 @@ import im.vector.app.core.intent.getMimeTypeFromUri
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.ui.views.CurrentCallsView
import im.vector.app.core.ui.views.ActiveCallViewHolder
import im.vector.app.core.ui.views.KnownCallsViewHolder
import im.vector.app.core.ui.views.ActiveConferenceView
import im.vector.app.core.ui.views.JumpToReadMarkerView
import im.vector.app.core.ui.views.NotificationAreaView
@ -295,7 +295,7 @@ class RoomDetailFragment @Inject constructor(
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
private var lockSendButton = false
private val activeCallViewHolder = ActiveCallViewHolder()
private val activeCallViewHolder = KnownCallsViewHolder()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)