mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 18:35:40 +03:00
VoIP: show duration
This commit is contained in:
parent
a5736efc75
commit
81f7932cb7
9 changed files with 207 additions and 42 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue