From 81f7932cb79116ca432e17d97d6b5c4159d3d884 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 22 Dec 2020 12:26:10 +0100 Subject: [PATCH] VoIP: show duration --- .../app/core/ui/views/CurrentCallsView.kt | 22 ++--- ...lViewHolder.kt => KnownCallsViewHolder.kt} | 46 +++++---- .../im/vector/app/core/utils/CountUpTimer.kt | 97 +++++++++++++++++++ .../app/features/call/VectorCallActivity.kt | 3 +- .../app/features/call/VectorCallViewModel.kt | 11 ++- .../app/features/call/VectorCallViewState.kt | 3 +- .../app/features/call/webrtc/WebRtcCall.kt | 59 +++++++++-- .../app/features/home/HomeDetailFragment.kt | 4 +- .../home/room/detail/RoomDetailFragment.kt | 4 +- 9 files changed, 207 insertions(+), 42 deletions(-) rename vector/src/main/java/im/vector/app/core/ui/views/{ActiveCallViewHolder.kt => KnownCallsViewHolder.kt} (66%) create mode 100644 vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt diff --git a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt index 8fd7bb4c90..16cb7785a7 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt @@ -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) { + fun render(calls: List, 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) } } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt b/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt similarity index 66% rename from vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt rename to vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt index adee15afe3..5de4938cc8 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt @@ -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 = emptyList() private var activeCallPipInitialized = false - fun updateCall(activeCall: WebRtcCall?, calls: List) { - 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) { + 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 } } diff --git a/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt b/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt new file mode 100644 index 0000000000..9d3a6e1b77 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt @@ -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) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 5f7bf1802a..ac814f8444 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -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(), CallContro } } } else { - views.callStatusText.text = null + views.callStatusText.text = state.formattedDuration if (callArgs.isVideoCall) { views.callVideoGroup.isVisible = true views.callInfoGroup.isVisible = false 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 7e3977bc99..c780ec1008 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 @@ -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 ) } 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 aba109091a..15fa2a37fa 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 @@ -38,7 +38,8 @@ data class VectorCallViewState( val availableSoundDevices: List = emptyList(), val callState: Async = Uninitialized, val otherKnownCallInfo: CallInfo? = null, - val callInfo: CallInfo = CallInfo(callId) + val callInfo: CallInfo = CallInfo(callId), + val formattedDuration: String = "" ) : MvRxState { data class CallInfo( 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 1b7775a5d7..f7cd148618 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 @@ -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() @@ -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().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) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 87b561ff93..4c7b7aa991 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 958e956fee..744595ecf0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -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)