diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallActivity.kt index 778db8b8dc..c7e3aba5a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallActivity.kt @@ -25,8 +25,10 @@ import androidx.compose.runtime.rxjava3.subscribeAsState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import io.reactivex.rxjava3.disposables.CompositeDisposable import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -94,6 +96,16 @@ class CallActivity : BaseActivity(), CallControlsCallback { observeCallEvents() viewModel.processCallIntent(CallIntent(intent)) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + viewModel.callActions.collect { + when (it) { + CallViewModel.Action.EnableVideo -> onVideoToggleClick(true) + } + } + } + } + setContent { val lifecycleOwner = LocalLifecycleOwner.current val callControlsState by webRtcCallViewModel.getCallControlsState(lifecycleOwner).subscribeAsState(initial = CallControlsState()) @@ -139,6 +151,8 @@ class CallActivity : BaseActivity(), CallControlsCallback { } } + val callScreenDialogType by viewModel.dialog.collectAsState(CallScreenDialogType.NONE) + SignalTheme { Surface { CallScreen( @@ -156,6 +170,7 @@ class CallActivity : BaseActivity(), CallControlsCallback { overflowParticipants = callParticipantsState.listParticipants, localParticipant = callParticipantsState.localParticipant, localRenderState = callParticipantsState.localRenderState, + callScreenDialogType = callScreenDialogType, callInfoView = { CallInfoView.View( webRtcCallViewModel = webRtcCallViewModel, @@ -174,7 +189,8 @@ class CallActivity : BaseActivity(), CallControlsCallback { }, onNavigationClick = { finish() }, onLocalPictureInPictureClicked = webRtcCallViewModel::onLocalPictureInPictureClicked, - onControlsToggled = { areControlsVisible = it } + onControlsToggled = { areControlsVisible = it }, + onCallScreenDialogDismissed = viewModel::onCallScreenDialogDismissed ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt index 179d5f5c58..9f3015e6ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt @@ -86,11 +86,13 @@ fun CallScreen( overflowParticipants: List, localParticipant: CallParticipant, localRenderState: WebRtcLocalRenderState, + callScreenDialogType: CallScreenDialogType, callInfoView: @Composable (Float) -> Unit, raiseHandSnackbar: @Composable (Modifier) -> Unit, onNavigationClick: () -> Unit, onLocalPictureInPictureClicked: () -> Unit, - onControlsToggled: (Boolean) -> Unit + onControlsToggled: (Boolean) -> Unit, + onCallScreenDialogDismissed: () -> Unit = {} ) { var peekPercentage by remember { mutableFloatStateOf(0f) @@ -250,6 +252,8 @@ fun CallScreen( ) } } + + CallScreenDialog(callScreenDialogType, onCallScreenDialogDismissed) } /** @@ -503,6 +507,7 @@ private fun CallScreenPreview() { callParticipantsPagerState = CallParticipantsPagerState(), localParticipant = CallParticipant(), localRenderState = WebRtcLocalRenderState.LARGE, + callScreenDialogType = CallScreenDialogType.NONE, callInfoView = { Text(text = "Call Info View Preview", modifier = Modifier.alpha(it)) }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenDialog.kt new file mode 100644 index 0000000000..1040557cd2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenDialog.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc.v2 + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import org.signal.core.ui.DarkPreview +import org.signal.core.ui.Dialogs +import org.signal.core.ui.Previews +import org.thoughtcrime.securesms.R + +/** + * Displays the current dialog to the user, or nothing. + */ +@Composable +fun CallScreenDialog( + callScreenDialogType: CallScreenDialogType, + onDialogDismissed: () -> Unit +) { + when (callScreenDialogType) { + CallScreenDialogType.NONE -> return + CallScreenDialogType.REMOVED_FROM_CALL_LINK -> RemovedFromCallLinkDialog(onDialogDismissed) + CallScreenDialogType.DENIED_REQUEST_TO_JOIN_CALL_LINK -> DeniedRequestToJoinCallDialog(onDialogDismissed) + } +} + +@Composable +private fun RemovedFromCallLinkDialog(onDialogDismissed: () -> Unit = {}) { + Dialogs.SimpleAlertDialog( + title = stringResource(R.string.WebRtcCallActivity__removed_from_call), + body = stringResource(R.string.WebRtcCallActivity__someone_has_removed_you_from_the_call), + confirm = stringResource(android.R.string.ok), + onConfirm = {}, + onDismiss = onDialogDismissed + ) +} + +@Composable +private fun DeniedRequestToJoinCallDialog(onDialogDismissed: () -> Unit = {}) { + Dialogs.SimpleAlertDialog( + title = stringResource(R.string.WebRtcCallActivity__join_request_denied), + body = stringResource(R.string.WebRtcCallActivity__your_request_to_join_this_call_has_been_denied), + confirm = stringResource(android.R.string.ok), + onConfirm = {}, + onDismiss = onDialogDismissed + ) +} + +@DarkPreview +@Composable +private fun RemovedFromCallLinkDialogPreview() { + Previews.Preview { + RemovedFromCallLinkDialog() + } +} + +@DarkPreview +@Composable +private fun DeniedRequestToJoinCallDialogPreview() { + Previews.Preview { + DeniedRequestToJoinCallDialog() + } +} + +/** + * Enumeration of available call screen dialog types. + */ +enum class CallScreenDialogType { + NONE, + REMOVED_FROM_CALL_LINK, + DENIED_REQUEST_TO_JOIN_CALL_LINK +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallViewModel.kt index 3f2784f941..0f9d5de83a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallViewModel.kt @@ -10,6 +10,8 @@ import androidx.annotation.StringRes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -27,6 +29,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.events.WebRtcViewModel import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.service.webrtc.CallLinkDisconnectReason import org.thoughtcrime.securesms.service.webrtc.SignalCallManager import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager @@ -48,13 +51,23 @@ class CallViewModel( } private var previousEvent: WebRtcViewModel? = null + private var enableVideoIfAvailable = false + private var lastProcessedIntentTimestamp = 0L + private var lastCallLinkDisconnectDialogShowTime = 0L + private var enterPipOnResume = false private val internalCallScreenState = MutableStateFlow(CallScreenState()) val callScreenState: StateFlow = internalCallScreenState + private val internalDialog = MutableStateFlow(CallScreenDialogType.NONE) + val dialog: StateFlow = internalDialog + + private val internalCallActions = MutableSharedFlow() + val callActions: Flow = internalCallActions + fun consumeEnterPipOnResume(): Boolean { val enter = enterPipOnResume enterPipOnResume = false @@ -127,6 +140,10 @@ class CallViewModel( } } + fun onCallScreenDialogDismissed() { + internalDialog.update { CallScreenDialogType.NONE } + } + fun onCallEvent(event: CallEvent) { when (event) { CallEvent.DismissSwitchCameraTooltip -> Unit // TODO @@ -175,12 +192,24 @@ class CallViewModel( WebRtcViewModel.State.CALL_ONGOING_ELSEWHERE -> handleCallTerminated(HangupMessage.Type.BUSY) } - // TODO [alex] -- Call link handling block + if (event.callLinkDisconnectReason != null && event.callLinkDisconnectReason.postedAt > lastCallLinkDisconnectDialogShowTime) { + lastCallLinkDisconnectDialogShowTime = System.currentTimeMillis() + + when (event.callLinkDisconnectReason) { + is CallLinkDisconnectReason.RemovedFromCall -> internalDialog.update { CallScreenDialogType.REMOVED_FROM_CALL_LINK } + is CallLinkDisconnectReason.DeniedRequestToJoinCall -> internalDialog.update { CallScreenDialogType.DENIED_REQUEST_TO_JOIN_CALL_LINK } + } + } val enableVideo = event.localParticipant.cameraState.cameraCount > 0 && enableVideoIfAvailable webRtcCallViewModel.updateFromWebRtcViewModel(event, enableVideo) - // TODO [alex] -- handle enable video + if (enableVideo) { + enableVideoIfAvailable = false + viewModelScope.launch { + internalCallActions.emit(Action.EnableVideo) + } + } // TODO [alex] -- handle denied bluetooth permission } @@ -373,4 +402,15 @@ class CallViewModel( lastProcessedIntentTimestamp = now } + + /** + * Actions that require activity-level context (for example, to request permissions.) + */ + enum class Action { + /** + * Tries to enable local video via the normal toggle callback. Should display permissions + * dialogs as necessary. + */ + EnableVideo + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkPreJoinActionProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkPreJoinActionProcessor.kt index 6f2bc84b2b..d239f5da29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkPreJoinActionProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkPreJoinActionProcessor.kt @@ -33,6 +33,11 @@ class CallLinkPreJoinActionProcessor( private val TAG = Log.tag(CallLinkPreJoinActionProcessor::class.java) } + override fun handleSetRingGroup(currentState: WebRtcServiceState, ringGroup: Boolean): WebRtcServiceState { + Log.i(TAG, "handleSetRingGroup(): Ignoring.") + return currentState + } + override fun handlePreJoinCall(currentState: WebRtcServiceState, remotePeer: RemotePeer): WebRtcServiceState { Log.i(TAG, "handlePreJoinCall():")