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 16cb7785a7..dfcd4629c6 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 @@ -52,7 +52,7 @@ class CurrentCallsView @JvmOverloads constructor( it.mxCall.state is CallState.Connected } val heldCalls = connectedCalls.filter { - it.isLocalOnHold() || it.remoteOnHold + it.isLocalOnHold || it.remoteOnHold } if (connectedCalls.size == 1) { if (heldCalls.size == 1) { diff --git a/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt b/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt index 5de4938cc8..3bd9ce713d 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt @@ -42,6 +42,9 @@ class KnownCallsViewHolder { } fun updateCall(currentCall: WebRtcCall?, calls: List) { + activeCallPiP?.let { + this.currentCall?.detachRenderers(listOf(it)) + } this.currentCall?.removeListener(tickListener) this.currentCall = currentCall this.currentCall?.addListener(tickListener) 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 index 9d3a6e1b77..5b5a406194 100644 --- a/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt +++ b/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt @@ -16,82 +16,45 @@ package im.vector.app.core.utils -import android.os.Handler -import android.os.SystemClock +import io.reactivex.Flowable +import io.reactivex.Observable +import io.reactivex.disposables.Disposable +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong 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 + private val elapsedTime: AtomicLong = AtomicLong() + private val resumed: AtomicBoolean = AtomicBoolean(false) + + private val disposable = Observable.interval(intervalInMs, TimeUnit.MILLISECONDS) + .filter { _ -> resumed.get() } + .doOnNext { _ -> elapsedTime.addAndGet(intervalInMs) } + .subscribe { + tickListener?.onTick(elapsedTime.get()) + } var tickListener: TickListener? = null - private val tickHandler: Handler = Handler() - private val tickSelector = Runnable { - if (isRunning) { - tickListener?.onTick(time) - startTicking() - } + fun elapsedTime(): Long{ + return elapsedTime.get() } - 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() - } + resumed.set(false) } - /** - * 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) + resumed.set(true) } - private fun stopTicking() { - tickHandler.removeCallbacksAndMessages(null) + fun stop() { + disposable.dispose() } - 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 ac814f8444..9de0dbb7ed 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,7 +50,6 @@ 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 @@ -166,7 +165,8 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.smallIsHeldIcon.isVisible = false when (callState) { is CallState.Idle, - is CallState.Dialing -> { + is CallState.CreateOffer, + is CallState.Dialing -> { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true views.callStatusText.setText(R.string.call_ring) @@ -187,9 +187,9 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callConnectingProgress.isVisible = true configureCallInfo(state) } - is CallState.Connected -> { + is CallState.Connected -> { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { - if (state.isLocalOnHold) { + if (state.isLocalOnHold || state.isRemoteOnHold) { views.smallIsHeldIcon.isVisible = true views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true @@ -226,13 +226,11 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callStatusText.setText(R.string.call_connecting) views.callConnectingProgress.isVisible = true } - // ensure all attached? - callManager.getCallById(callArgs.callId)?.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, null) } - is CallState.Terminated -> { + is CallState.Terminated -> { finish() } - null -> { + null -> { } } } @@ -255,7 +253,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) avatarRenderer.renderBlur(state.otherKnownCallInfo.otherUserItem, views.otherKnownCallAvatarView, sampling = 20, rounded = false, colorFilter = colorFilter) views.otherKnownCallLayout.isVisible = true - views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold() || it.remoteOnHold }.orFalse() + views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.remoteOnHold }.orFalse() } } 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 c780ec1008..ac45cdab5a 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 @@ -55,7 +55,7 @@ class VectorCallViewModel @AssistedInject constructor( override fun onHoldUnhold() { setState { copy( - isLocalOnHold = call?.isLocalOnHold() ?: false, + isLocalOnHold = call?.isLocalOnHold ?: false, isRemoteOnHold = call?.remoteOnHold ?: false ) } @@ -179,7 +179,7 @@ class VectorCallViewModel @AssistedInject constructor( callState = Success(webRtcCall.mxCall.state), callInfo = VectorCallViewState.CallInfo(callId, item), soundDevice = currentSoundDevice, - isLocalOnHold = webRtcCall.isLocalOnHold(), + isLocalOnHold = webRtcCall.isLocalOnHold, isRemoteOnHold = webRtcCall.remoteOnHold, availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(), isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT, 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 e6ebdaf572..41a43dd924 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 @@ -135,7 +135,7 @@ class WebRtcCall(val mxCall: MxCall, private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null - private val timer = CountUpTimer(1000).apply { + private val timer = CountUpTimer(Duration.ofSeconds(1).toMillis()).apply { tickListener = object : CountUpTimer.TickListener { override fun onTick(milliseconds: Long) { val formattedDuration = formatDuration(Duration.ofMillis(milliseconds)) @@ -146,7 +146,6 @@ class WebRtcCall(val mxCall: MxCall, } } - // Mute status var micMuted = false private set @@ -154,6 +153,11 @@ class WebRtcCall(val mxCall: MxCall, private set var remoteOnHold = false private set + var isLocalOnHold = false + private set + + // This value is used to track localOnHold when changing remoteOnHold value + private var wasLocalOnHold = false var offerSdp: CallInviteContent.Offer? = null @@ -228,7 +232,7 @@ class WebRtcCall(val mxCall: MxCall, fun formattedDuration(): String { return formatDuration( - Duration.ofMillis(timer.time) + Duration.ofMillis(timer.elapsedTime()) ) } @@ -257,21 +261,9 @@ class WebRtcCall(val mxCall: MxCall, fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) { Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer") -// this.localSurfaceRenderer = WeakReference(localViewRenderer) -// this.remoteSurfaceRenderer = WeakReference(remoteViewRenderer) localSurfaceRenderers.addIfNeeded(localViewRenderer) remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer) - // The call is going to resume from background, we can reduce notif - mxCall - .takeIf { it.state is CallState.Connected } - ?.let { mxCall -> - // Start background service with notification - CallService.onPendingCall( - context = context, - callId = mxCall.callId) - } - GlobalScope.launch(dispatcher) { when (mode) { VectorCallActivity.INCOMING_ACCEPT -> { @@ -301,9 +293,8 @@ class WebRtcCall(val mxCall: MxCall, } } - fun detachRenderers(renderers: List?) = synchronized(this) { + fun detachRenderers(renderers: List?) { Timber.v("## VOIP detachRenderers") - // currentCall?.localMediaStream?.let { currentCall?.peerConnection?.removeStream(it) } if (renderers.isNullOrEmpty()) { // remove all sinks localSurfaceRenderers.forEach { @@ -545,7 +536,7 @@ class WebRtcCall(val mxCall: MxCall, * rather than 'sendonly') * @returns true if the other party has put us on hold */ - fun isLocalOnHold(): Boolean = synchronized(this) { + private fun computeIsLocalOnHold(): Boolean { if (mxCall.state !is CallState.Connected) return false var callOnHold = true // We consider a call to be on hold only if *all* the tracks are on hold @@ -558,38 +549,46 @@ class WebRtcCall(val mxCall: MxCall, return callOnHold } - fun updateRemoteOnHold(onHold: Boolean) = synchronized(this) { - if (remoteOnHold == onHold) return - remoteOnHold = onHold - if (!onHold) { - onCallBecomeActive(this) + fun updateRemoteOnHold(onHold: Boolean) { + GlobalScope.launch(dispatcher) { + if (remoteOnHold == onHold) return@launch + val direction: RtpTransceiver.RtpTransceiverDirection + if (onHold) { + wasLocalOnHold = isLocalOnHold + remoteOnHold = true + isLocalOnHold = true + direction = RtpTransceiver.RtpTransceiverDirection.INACTIVE + } else { + remoteOnHold = false + isLocalOnHold = wasLocalOnHold + onCallBecomeActive(this@WebRtcCall) + direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV + } + for (transceiver in peerConnection?.transceivers ?: emptyList()) { + transceiver.direction = direction + } + updateMuteStatus() + listeners.forEach { + tryOrNull { it.onHoldUnhold() } + } } - val direction = if (onHold) { - RtpTransceiver.RtpTransceiverDirection.INACTIVE - } else { - RtpTransceiver.RtpTransceiverDirection.SEND_RECV - } - for (transceiver in peerConnection?.transceivers ?: emptyList()) { - transceiver.direction = direction - } - updateMuteStatus() } - fun muteCall(muted: Boolean) = synchronized(this) { + fun muteCall(muted: Boolean) { micMuted = muted updateMuteStatus() } - fun enableVideo(enabled: Boolean) = synchronized(this) { + fun enableVideo(enabled: Boolean) { videoMuted = !enabled updateMuteStatus() } - fun canSwitchCamera(): Boolean = synchronized(this) { + fun canSwitchCamera(): Boolean { return availableCamera.size > 1 } - private fun getOppositeCameraIfAny(): CameraProxy? = synchronized(this) { + private fun getOppositeCameraIfAny(): CameraProxy? { val currentCamera = cameraInUse ?: return null return if (currentCamera.type == CameraType.FRONT) { availableCamera.firstOrNull { it.type == CameraType.BACK } @@ -598,7 +597,7 @@ class WebRtcCall(val mxCall: MxCall, } } - fun switchCamera() = synchronized(this) { + fun switchCamera() { Timber.v("## VOIP switchCamera") if (mxCall.state is CallState.Connected && mxCall.isVideoCall) { val oppositeCamera = getOppositeCameraIfAny() ?: return @@ -641,17 +640,18 @@ class WebRtcCall(val mxCall: MxCall, } } - fun currentCameraType(): CameraType? = synchronized(this) { + fun currentCameraType(): CameraType? { return cameraInUse?.type } - fun currentCaptureFormat(): CaptureFormat = synchronized(this) { + fun currentCaptureFormat(): CaptureFormat { return currentCaptureFormat } private fun release() { + listeners.clear() mxCall.removeListener(this) - timer.reset() + timer.stop() timer.tickListener = null videoCapturer?.stopCapture() videoCapturer?.dispose() @@ -703,10 +703,11 @@ class WebRtcCall(val mxCall: MxCall, } } - fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) = synchronized(this) { + fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) { if (mxCall.state == CallState.Terminated) { return } + val wasConnected = mxCall.state is CallState.Connected mxCall.state = CallState.Terminated // Close tracks ASAP localVideoTrack?.setEnabled(false) @@ -715,12 +716,13 @@ class WebRtcCall(val mxCall: MxCall, val cameraManager = context.getSystemService()!! cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) } - release() - listeners.clear() + GlobalScope.launch(dispatcher) { + release() + } onCallEnded(this) if (originatedByMe) { // send hang up event - if (mxCall.state is CallState.Connected) { + if (wasConnected) { mxCall.hangUp(reason) } else { mxCall.reject() @@ -783,7 +785,7 @@ class WebRtcCall(val mxCall: MxCall, Timber.i("Ignoring colliding negotiate event because we're impolite") return@launch } - val prevOnHold = isLocalOnHold() + val prevOnHold = computeIsLocalOnHold() try { val sdp = SessionDescription(type.asWebRTC(), sdpText) peerConnection.awaitSetRemoteDescription(sdp) @@ -795,8 +797,10 @@ class WebRtcCall(val mxCall: MxCall, } catch (failure: Throwable) { Timber.e(failure, "Failed to complete negotiation") } - val nowOnHold = isLocalOnHold() + val nowOnHold = computeIsLocalOnHold() + wasLocalOnHold = nowOnHold if (prevOnHold != nowOnHold) { + isLocalOnHold = nowOnHold if (nowOnHold) { timer.pause() } else { diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 8850507c6a..48f0c54e76 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -297,11 +297,6 @@ class NotificationUtils @Inject constructor(private val context: Context, .setLights(accentColor, 500, 500) .setOngoing(true) - // Compat: Display the incoming call notification on the lock screen - builder.priority = NotificationCompat.PRIORITY_HIGH - - // -// val pendingIntent = stackBuilder.getPendingIntent(requestId, PendingIntent.FLAG_UPDATE_CURRENT) val contentIntent = VectorCallActivity.newIntent( context = context, @@ -340,9 +335,11 @@ class NotificationUtils @Inject constructor(private val context: Context, answerCallPendingIntent ) ) - - builder.setFullScreenIntent(contentPendingIntent, true) - + if (fromBg) { + // Compat: Display the incoming call notification on the lock screen + builder.priority = NotificationCompat.PRIORITY_HIGH + builder.setFullScreenIntent(contentPendingIntent, true) + } return builder.build() }