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.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.RelativeLayout import android.widget.RelativeLayout
import im.vector.app.R 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.call.webrtc.WebRtcCall
import im.vector.app.features.themes.ThemeUtils 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 import org.matrix.android.sdk.api.session.call.CallState
class CurrentCallsView @JvmOverloads constructor( class CurrentCallsView @JvmOverloads constructor(
@ -35,19 +37,17 @@ class CurrentCallsView @JvmOverloads constructor(
fun onTapToReturnToCall() fun onTapToReturnToCall()
} }
val views: ViewCurrentCallsBinding
var callback: Callback? = null var callback: Callback? = null
init { init {
setupView()
}
private fun setupView() {
inflate(context, R.layout.view_current_calls, this) inflate(context, R.layout.view_current_calls, this)
views = ViewCurrentCallsBinding.bind(this)
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary)) setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
setOnClickListener { callback?.onTapToReturnToCall() } setOnClickListener { callback?.onTapToReturnToCall() }
} }
fun render(calls: List<WebRtcCall>) { fun render(calls: List<WebRtcCall>, formattedDuration: String) {
val connectedCalls = calls.filter { val connectedCalls = calls.filter {
it.mxCall.state is CallState.Connected it.mxCall.state is CallState.Connected
} }
@ -56,17 +56,17 @@ class CurrentCallsView @JvmOverloads constructor(
} }
if (connectedCalls.size == 1) { if (connectedCalls.size == 1) {
if (heldCalls.size == 1) { if (heldCalls.size == 1) {
currentCallsInfo.setText(R.string.call_only_paused) views.currentCallsInfo.setText(R.string.call_only_paused)
} else { } else {
currentCallsInfo.setText(R.string.call_only_active) views.currentCallsInfo.text = resources.getString(R.string.call_only_active, formattedDuration)
} }
} else { } else {
if (heldCalls.size > 1) { 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) { } 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 { } 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.RendererCommon
import org.webrtc.SurfaceViewRenderer import org.webrtc.SurfaceViewRenderer
class ActiveCallViewHolder { class KnownCallsViewHolder {
private var activeCallPiP: SurfaceViewRenderer? = null private var activeCallPiP: SurfaceViewRenderer? = null
private var activeCallView: CurrentCallsView? = null private var currentCallsView: CurrentCallsView? = null
private var pipWrapper: CardView? = 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 private var activeCallPipInitialized = false
fun updateCall(activeCall: WebRtcCall?, calls: List<WebRtcCall>) { private val tickListener = object : WebRtcCall.Listener {
this.activeCall = activeCall override fun onTick(formattedDuration: String) {
val hasActiveCall = activeCall?.mxCall?.state is CallState.Connected 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) { if (hasActiveCall) {
val isVideoCall = activeCall?.mxCall?.isVideoCall == true val isVideoCall = currentCall?.mxCall?.isVideoCall == true
if (isVideoCall) initIfNeeded() if (isVideoCall) initIfNeeded()
activeCallView?.isVisible = !isVideoCall currentCallsView?.isVisible = !isVideoCall
activeCallView?.render(calls) currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "")
pipWrapper?.isVisible = isVideoCall pipWrapper?.isVisible = isVideoCall
activeCallPiP?.isVisible = isVideoCall activeCallPiP?.isVisible = isVideoCall
activeCallPiP?.let { activeCallPiP?.let {
activeCall?.attachViewRenderers(null, it, null) currentCall?.attachViewRenderers(null, it, null)
} }
} else { } else {
activeCallView?.isVisible = false currentCallsView?.isVisible = false
activeCallPiP?.isVisible = false activeCallPiP?.isVisible = false
pipWrapper?.isVisible = false pipWrapper?.isVisible = false
activeCallPiP?.let { 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) { fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: CurrentCallsView, pipWrapper: CardView, interactionListener: CurrentCallsView.Callback) {
this.activeCallPiP = activeCallPiP this.activeCallPiP = activeCallPiP
this.activeCallView = activeCallView this.currentCallsView = activeCallView
this.pipWrapper = pipWrapper this.pipWrapper = pipWrapper
this.activeCallView?.callback = interactionListener this.currentCallsView?.callback = interactionListener
pipWrapper.setOnClickListener( pipWrapper.setOnClickListener(
DebouncedClickListener({ _ -> DebouncedClickListener({ _ ->
interactionListener.onTapToReturnToCall() interactionListener.onTapToReturnToCall()
}) })
) )
this.currentCall?.addListener(tickListener)
} }
fun unBind() { fun unBind() {
activeCallPiP?.let { activeCallPiP?.let {
activeCall?.detachRenderers(listOf(it)) currentCall?.detachRenderers(listOf(it))
} }
if (activeCallPipInitialized) { if (activeCallPipInitialized) {
activeCallPiP?.release() activeCallPiP?.release()
} }
this.activeCallView?.callback = null this.currentCallsView?.callback = null
this.currentCall?.removeListener(tickListener)
pipWrapper?.setOnClickListener(null) pipWrapper?.setOnClickListener(null)
activeCallPiP = null activeCallPiP = null
activeCallView = null currentCallsView = null
pipWrapper = 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 im.vector.app.features.home.room.detail.RoomDetailArgs
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import okhttp3.internal.concurrent.formatDuration
import org.matrix.android.sdk.api.extensions.orFalse 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.CallState
import org.matrix.android.sdk.api.session.call.MxCallDetail import org.matrix.android.sdk.api.session.call.MxCallDetail
@ -205,7 +206,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
} }
} }
} else { } else {
views.callStatusText.text = null views.callStatusText.text = state.formattedDuration
if (callArgs.isVideoCall) { if (callArgs.isVideoCall) {
views.callVideoGroup.isVisible = true views.callVideoGroup.isVisible = true
views.callInfoGroup.isVisible = false 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) { override fun onStateUpdate(call: MxCall) {
val callState = call.state val callState = call.state
if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
@ -176,8 +182,9 @@ class VectorCallViewModel @AssistedInject constructor(
isLocalOnHold = webRtcCall.isLocalOnHold(), isLocalOnHold = webRtcCall.isLocalOnHold(),
isRemoteOnHold = webRtcCall.remoteOnHold, isRemoteOnHold = webRtcCall.remoteOnHold,
availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(), availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(),
isFrontCamera = call?.currentCameraType() == CameraType.FRONT, isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT,
canSwitchCamera = call?.canSwitchCamera() ?: false, canSwitchCamera = webRtcCall.canSwitchCamera(),
formattedDuration = webRtcCall.formattedDuration(),
isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD 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 availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(),
val callState: Async<CallState> = Uninitialized, val callState: Async<CallState> = Uninitialized,
val otherKnownCallInfo: CallInfo? = null, val otherKnownCallInfo: CallInfo? = null,
val callInfo: CallInfo = CallInfo(callId) val callInfo: CallInfo = CallInfo(callId),
val formattedDuration: String = ""
) : MvRxState { ) : MvRxState {
data class CallInfo( data class CallInfo(

View file

@ -20,6 +20,7 @@ import android.content.Context
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import im.vector.app.core.services.CallService 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.CallAudioManager
import im.vector.app.features.call.CameraEventsHandlerAdapter import im.vector.app.features.call.CameraEventsHandlerAdapter
import im.vector.app.features.call.CameraProxy 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.Session
import org.matrix.android.sdk.api.session.call.CallState 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.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.call.TurnServerResponse
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent 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.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.CallNegotiateContent
import org.matrix.android.sdk.api.session.room.model.call.SdpType import org.matrix.android.sdk.api.session.room.model.call.SdpType
import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.internal.util.awaitCallback
import org.threeten.bp.Duration
import org.webrtc.AudioSource import org.webrtc.AudioSource
import org.webrtc.AudioTrack import org.webrtc.AudioTrack
import org.webrtc.Camera1Enumerator import org.webrtc.Camera1Enumerator
@ -96,6 +99,8 @@ class WebRtcCall(val mxCall: MxCall,
fun onCaptureStateChanged() {} fun onCaptureStateChanged() {}
fun onCameraChanged() {} fun onCameraChanged() {}
fun onHoldUnhold() {} fun onHoldUnhold() {}
fun onTick(formattedDuration: String) {}
override fun onStateUpdate(call: MxCall) {}
} }
private val listeners = CopyOnWriteArrayList<Listener>() private val listeners = CopyOnWriteArrayList<Listener>()
@ -130,6 +135,18 @@ class WebRtcCall(val mxCall: MxCall,
private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD
private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null 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 // Mute status
var micMuted = false var micMuted = false
private set 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?) { private fun createPeerConnection(turnServerResponse: TurnServerResponse?) {
val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return
val iceServers = mutableListOf<PeerConnection.IceServer>().apply { val iceServers = mutableListOf<PeerConnection.IceServer>().apply {
@ -547,7 +570,7 @@ class WebRtcCall(val mxCall: MxCall,
return callOnHold return callOnHold
} }
fun updateRemoteOnHold(onHold: Boolean) = synchronized(this){ fun updateRemoteOnHold(onHold: Boolean) = synchronized(this) {
if (remoteOnHold == onHold) return if (remoteOnHold == onHold) return
remoteOnHold = onHold remoteOnHold = onHold
if (!onHold) { if (!onHold) {
@ -574,11 +597,11 @@ class WebRtcCall(val mxCall: MxCall,
updateMuteStatus() updateMuteStatus()
} }
fun canSwitchCamera(): Boolean = synchronized(this){ fun canSwitchCamera(): Boolean = synchronized(this) {
return availableCamera.size > 1 return availableCamera.size > 1
} }
private fun getOppositeCameraIfAny(): CameraProxy? = synchronized(this){ private fun getOppositeCameraIfAny(): CameraProxy? = synchronized(this) {
val currentCamera = cameraInUse ?: return null val currentCamera = cameraInUse ?: return null
return if (currentCamera.type == CameraType.FRONT) { return if (currentCamera.type == CameraType.FRONT) {
availableCamera.firstOrNull { it.type == CameraType.BACK } 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") Timber.v("## VOIP switchCamera")
if (mxCall.state is CallState.Connected && mxCall.isVideoCall) { if (mxCall.state is CallState.Connected && mxCall.isVideoCall) {
val oppositeCamera = getOppositeCameraIfAny() ?: return 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 return cameraInUse?.type
} }
@ -638,8 +661,10 @@ class WebRtcCall(val mxCall: MxCall,
return currentCaptureFormat return currentCaptureFormat
} }
private fun release() { private fun release() {
mxCall.removeListener(this) mxCall.removeListener(this)
timer.reset()
timer.tickListener = null
videoCapturer?.stopCapture() videoCapturer?.stopCapture()
videoCapturer?.dispose() videoCapturer?.dispose()
videoCapturer = null videoCapturer = null
@ -784,6 +809,11 @@ class WebRtcCall(val mxCall: MxCall,
} }
val nowOnHold = isLocalOnHold() val nowOnHold = isLocalOnHold()
if (prevOnHold != nowOnHold) { if (prevOnHold != nowOnHold) {
if (nowOnHold) {
timer.pause()
} else {
timer.resume()
}
listeners.forEach { listeners.forEach {
tryOrNull { it.onHoldUnhold() } 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 // MxCall.StateListener
override fun onStateUpdate(call: MxCall) { 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 { listeners.forEach {
tryOrNull { it.onStateUpdate(call) } 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.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.ui.views.CurrentCallsView 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.core.ui.views.KeysBackupBanner
import im.vector.app.databinding.FragmentHomeDetailBinding import im.vector.app.databinding.FragmentHomeDetailBinding
import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.SharedKnownCallsViewModel
@ -83,7 +83,7 @@ class HomeDetailFragment @Inject constructor(
return FragmentHomeDetailBinding.inflate(inflater, container, false) return FragmentHomeDetailBinding.inflate(inflater, container, false)
} }
private val activeCallViewHolder = ActiveCallViewHolder() private val activeCallViewHolder = KnownCallsViewHolder()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) 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.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.ui.views.CurrentCallsView 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.ActiveConferenceView
import im.vector.app.core.ui.views.JumpToReadMarkerView import im.vector.app.core.ui.views.JumpToReadMarkerView
import im.vector.app.core.ui.views.NotificationAreaView import im.vector.app.core.ui.views.NotificationAreaView
@ -295,7 +295,7 @@ class RoomDetailFragment @Inject constructor(
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
private var lockSendButton = false private var lockSendButton = false
private val activeCallViewHolder = ActiveCallViewHolder() private val activeCallViewHolder = KnownCallsViewHolder()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)