From 5bd3eda17d2b09e2cdac1fdbce9fe96d4cf55af0 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Mon, 16 Sep 2024 10:24:09 -0300 Subject: [PATCH] Add snackbar that is displayed if you're currently in a different call. --- .../ContactSelectionListFragment.java | 9 +- .../thoughtcrime/securesms/MainActivity.java | 5 +- .../securesms/NewConversationActivity.java | 9 +- .../calls/YouAreAlreadyInACallSnackbar.kt | 53 +++++ ...CreateCallLinkBottomSheetDialogFragment.kt | 224 ++++++++++++------ .../links/create/CreateCallLinkViewModel.kt | 10 + .../links/details/CallLinkDetailsFragment.kt | 17 +- .../links/details/CallLinkDetailsViewModel.kt | 10 + .../securesms/calls/log/CallLogContextMenu.kt | 9 +- .../securesms/calls/log/CallLogFragment.kt | 9 +- .../securesms/calls/new/NewCallActivity.kt | 9 +- .../ConversationSettingsFragment.kt | 9 +- .../SharedContactDetailsActivity.java | 5 +- .../conversation/v2/ConversationFragment.kt | 21 +- .../RecipientBottomSheetDialogFragment.java | 5 +- .../bottomsheet/RecipientDialogViewModel.java | 10 +- .../ActiveCallActionProcessorDelegate.java | 2 +- .../service/webrtc/ActiveCallData.kt | 41 ++++ .../webrtc/GroupConnectedActionProcessor.java | 2 +- .../webrtc/GroupJoiningActionProcessor.java | 2 +- .../service/webrtc/WebRtcActionProcessor.java | 2 +- .../securesms/util/CommunicationActions.java | 55 +++-- app/src/main/res/values/strings.xml | 2 + .../main/java/org/signal/core/ui/Snackbars.kt | 5 +- 24 files changed, 394 insertions(+), 131 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/calls/YouAreAlreadyInACallSnackbar.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallData.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java index 8e14376469..2a9df86a22 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -53,6 +53,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable; import org.signal.core.util.concurrent.RxExtensions; import org.signal.core.util.concurrent.SimpleTask; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar; import org.thoughtcrime.securesms.components.RecyclerViewFastScroller; import org.thoughtcrime.securesms.contacts.ContactChipViewModel; import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode; @@ -1009,12 +1010,16 @@ public final class ContactSelectionListFragment extends LoggingFragment { private class CallButtonClickCallbacks implements ContactSearchAdapter.CallButtonClickCallbacks { @Override public void onVideoCallButtonClicked(@NonNull Recipient recipient) { - CommunicationActions.startVideoCall(ContactSelectionListFragment.this, recipient); + CommunicationActions.startVideoCall(ContactSelectionListFragment.this, recipient, () -> { + YouAreAlreadyInACallSnackbar.show(requireView()); + }); } @Override public void onAudioCallButtonClicked(@NonNull Recipient recipient) { - CommunicationActions.startVoiceCall(ContactSelectionListFragment.this, recipient); + CommunicationActions.startVoiceCall(ContactSelectionListFragment.this, recipient, () -> { + YouAreAlreadyInACallSnackbar.show(requireView()); + }); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java index d6fb0c6e83..b20699e2bb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java @@ -18,6 +18,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.concurrent.LifecycleDisposable; import org.signal.donations.StripeApi; +import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar; import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment; import org.thoughtcrime.securesms.components.DeviceSpecificNotificationBottomSheet; import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment; @@ -243,7 +244,9 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot private void handleCallLinkInIntent(Intent intent) { Uri data = intent.getData(); if (data != null) { - CommunicationActions.handlePotentialCallLinkUrl(this, data.toString()); + CommunicationActions.handlePotentialCallLinkUrl(this, data.toString(), () -> { + YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content)); + }); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java index f1ec89e7cb..319ca042cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java @@ -39,6 +39,7 @@ import org.signal.core.util.DimensionUnit; import org.signal.core.util.concurrent.LifecycleDisposable; import org.signal.core.util.concurrent.SimpleTask; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar; import org.thoughtcrime.securesms.components.menu.ActionItem; import org.thoughtcrime.securesms.components.menu.SignalContextMenu; import org.thoughtcrime.securesms.contacts.management.ContactsManagementRepository; @@ -305,7 +306,9 @@ public class NewConversationActivity extends ContactSelectionActivity R.drawable.ic_phone_right_24, getString(R.string.NewConversationActivity__audio_call), R.color.signal_colorOnSurface, - () -> CommunicationActions.startVoiceCall(this, recipient) + () -> CommunicationActions.startVoiceCall(this, recipient, () -> { + YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content)); + }) ); } else { return null; @@ -321,7 +324,9 @@ public class NewConversationActivity extends ContactSelectionActivity R.drawable.ic_video_call_24, getString(R.string.NewConversationActivity__video_call), R.color.signal_colorOnSurface, - () -> CommunicationActions.startVideoCall(this, recipient) + () -> CommunicationActions.startVideoCall(this, recipient, () -> { + YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content)); + }) ); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/YouAreAlreadyInACallSnackbar.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/YouAreAlreadyInACallSnackbar.kt new file mode 100644 index 0000000000..d54f80847d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/YouAreAlreadyInACallSnackbar.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.calls + +import android.view.View +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.google.android.material.snackbar.Snackbar +import org.signal.core.ui.Snackbars +import org.thoughtcrime.securesms.R + +/** + * Snackbar which can be displayed whenever the user tries to join a call but is already in another. + */ +object YouAreAlreadyInACallSnackbar { + /** + * Composable component + */ + @Composable + fun YouAreAlreadyInACallSnackbar( + displaySnackbar: Boolean, + modifier: Modifier = Modifier + ) { + val message = stringResource(R.string.CommunicationActions__you_are_already_in_a_call) + val hostState = remember { SnackbarHostState() } + Snackbars.Host(hostState, modifier = modifier) + + LaunchedEffect(displaySnackbar) { + if (displaySnackbar) { + hostState.showSnackbar(message) + } + } + } + + /** + * View system component + */ + @JvmStatic + fun show(view: View) { + Snackbar.make( + view, + view.context.getString(R.string.CommunicationActions__you_are_already_in_a_call), + Snackbar.LENGTH_LONG + ).show() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkBottomSheetDialogFragment.kt index edc1ed1cb0..dad88db257 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkBottomSheetDialogFragment.kt @@ -11,6 +11,7 @@ import android.os.Bundle import android.view.View import android.widget.Toast import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -21,6 +22,7 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -36,21 +38,28 @@ import io.reactivex.rxjava3.kotlin.subscribeBy import org.signal.core.ui.BottomSheets import org.signal.core.ui.Buttons import org.signal.core.ui.Dividers +import org.signal.core.ui.Previews import org.signal.core.ui.Rows +import org.signal.core.ui.SignalPreview import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.logging.Log import org.signal.ringrtc.CallLinkState import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.YouAreAlreadyInACallSnackbar import org.thoughtcrime.securesms.calls.links.CallLinks import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment import org.thoughtcrime.securesms.calls.links.SignalCallRow import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment import org.thoughtcrime.securesms.database.CallLinkTable +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId import org.thoughtcrime.securesms.service.webrtc.links.CreateCallLinkResult +import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult import org.thoughtcrime.securesms.sharing.v2.ShareActivity import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.Util +import java.time.Instant /** * Bottom sheet for creating call links @@ -77,84 +86,21 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment @Composable override fun SheetContent() { - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentSize(Alignment.Center) - ) { - val callLink: CallLinkTable.CallLink by viewModel.callLink + val callLink: CallLinkTable.CallLink by viewModel.callLink + val displayAlreadyInACallSnackbar: Boolean by viewModel.showAlreadyInACall.collectAsState(false) - BottomSheets.Handle(modifier = Modifier.align(Alignment.CenterHorizontally)) - - Spacer(modifier = Modifier.height(20.dp)) - - Text( - text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__create_call_link), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - - Spacer(modifier = Modifier.height(24.dp)) - - SignalCallRow( - callLink = callLink, - callLinkPeekInfo = null, - onJoinClicked = this@CreateCallLinkBottomSheetDialogFragment::onJoinClicked - ) - - Spacer(modifier = Modifier.height(12.dp)) - - Rows.TextRow( - text = stringResource( - id = if (callLink.state.name.isEmpty()) { - R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name - } else { - R.string.CreateCallLinkBottomSheetDialogFragment__edit_call_name - } - ), - onClick = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked - ) - - Rows.ToggleRow( - checked = callLink.state.restrictions == CallLinkState.Restrictions.ADMIN_APPROVAL, - text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__require_admin_approval), - onCheckChanged = this@CreateCallLinkBottomSheetDialogFragment::setApproveAllMembers, - modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::toggleApproveAllMembers) - ) - - Dividers.Default() - - Rows.TextRow( - text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link_via_signal), - icon = painterResource(id = R.drawable.symbol_forward_24), - onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked - ) - - Rows.TextRow( - text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__copy_link), - icon = painterResource(id = R.drawable.symbol_copy_android_24), - onClick = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked - ) - - Rows.TextRow( - text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link), - icon = painterResource(id = R.drawable.symbol_share_android_24), - onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked - ) - - Buttons.MediumTonal( - onClick = this@CreateCallLinkBottomSheetDialogFragment::onDoneClicked, - modifier = Modifier - .padding(end = dimensionResource(id = R.dimen.core_ui__gutter)) - .align(Alignment.End) - ) { - Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__done)) - } - - Spacer(modifier = Modifier.size(16.dp)) - } + CreateCallLinkBottomSheetContent( + callLink = callLink, + onJoinClicked = this@CreateCallLinkBottomSheetDialogFragment::onJoinClicked, + onAddACallNameClicked = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked, + onApproveAllMembersChanged = this@CreateCallLinkBottomSheetDialogFragment::setApproveAllMembers, + onToggleApproveAllMembersClicked = this@CreateCallLinkBottomSheetDialogFragment::toggleApproveAllMembers, + onShareViaSignalClicked = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked, + onCopyLinkClicked = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked, + onShareLinkClicked = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked, + onDoneClicked = this@CreateCallLinkBottomSheetDialogFragment::onDoneClicked, + displayAlreadyInACallSnackbar = displayAlreadyInACallSnackbar + ) } private fun setCallName(callName: String) { @@ -195,7 +141,9 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment lifecycleDisposable += viewModel.commitCallLink().subscribeBy(onSuccess = { when (it) { is EnsureCallLinkCreatedResult.Success -> { - CommunicationActions.startVideoCall(requireActivity(), it.recipient) + CommunicationActions.startVideoCall(requireActivity(), it.recipient) { + viewModel.setShowAlreadyInACall(true) + } dismissAllowingStateLoss() } @@ -282,3 +230,123 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment Toast.makeText(requireContext(), R.string.CallLinkDetailsFragment__couldnt_save_changes, Toast.LENGTH_LONG).show() } } + +@Composable +private fun CreateCallLinkBottomSheetContent( + callLink: CallLinkTable.CallLink, + displayAlreadyInACallSnackbar: Boolean, + onJoinClicked: () -> Unit = {}, + onAddACallNameClicked: () -> Unit = {}, + onApproveAllMembersChanged: (Boolean) -> Unit = {}, + onToggleApproveAllMembersClicked: () -> Unit = {}, + onShareViaSignalClicked: () -> Unit = {}, + onCopyLinkClicked: () -> Unit = {}, + onShareLinkClicked: () -> Unit = {}, + onDoneClicked: () -> Unit = {} +) { + Box { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentSize(Alignment.Center) + ) { + BottomSheets.Handle(modifier = Modifier.align(Alignment.CenterHorizontally)) + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__create_call_link), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(24.dp)) + + SignalCallRow( + callLink = callLink, + callLinkPeekInfo = null, + onJoinClicked = onJoinClicked + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Rows.TextRow( + text = stringResource( + id = if (callLink.state.name.isEmpty()) { + R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name + } else { + R.string.CreateCallLinkBottomSheetDialogFragment__edit_call_name + } + ), + onClick = onAddACallNameClicked + ) + + Rows.ToggleRow( + checked = callLink.state.restrictions == CallLinkState.Restrictions.ADMIN_APPROVAL, + text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__require_admin_approval), + onCheckChanged = onApproveAllMembersChanged, + modifier = Modifier.clickable(onClick = onToggleApproveAllMembersClicked) + ) + + Dividers.Default() + + Rows.TextRow( + text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link_via_signal), + icon = painterResource(id = R.drawable.symbol_forward_24), + onClick = onShareViaSignalClicked + ) + + Rows.TextRow( + text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__copy_link), + icon = painterResource(id = R.drawable.symbol_copy_android_24), + onClick = onCopyLinkClicked + ) + + Rows.TextRow( + text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link), + icon = painterResource(id = R.drawable.symbol_share_android_24), + onClick = onShareLinkClicked + ) + + Buttons.MediumTonal( + onClick = onDoneClicked, + modifier = Modifier + .padding(end = dimensionResource(id = R.dimen.core_ui__gutter)) + .align(Alignment.End) + ) { + Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__done)) + } + + Spacer(modifier = Modifier.size(16.dp)) + } + + YouAreAlreadyInACallSnackbar( + displaySnackbar = displayAlreadyInACallSnackbar, + modifier = Modifier.align(Alignment.BottomCenter) + ) + } +} + +@SignalPreview +@Composable +private fun CreateCallLinkBottomSheetContentPreview() { + Previews.BottomSheetPreview { + CreateCallLinkBottomSheetContent( + callLink = CallLinkTable.CallLink( + recipientId = RecipientId.UNKNOWN, + roomId = CallLinkRoomId.fromBytes(byteArrayOf(1, 2, 3, 4)), + credentials = null, + state = SignalCallLinkState( + name = "Test Call", + restrictions = CallLinkState.Restrictions.ADMIN_APPROVAL, + revoked = false, + expiration = Instant.MAX + ), + deletionTimestamp = 0L + ), + displayAlreadyInACallSnackbar = true + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkViewModel.kt index 95d6fd2e37..8a89bcce51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkViewModel.kt @@ -14,6 +14,9 @@ import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.subscribeBy +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import org.signal.ringrtc.CallLinkState.Restrictions import org.thoughtcrime.securesms.calls.links.CallLinks import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository @@ -47,6 +50,9 @@ class CreateCallLinkViewModel( val callLink: State = _callLink val linkKeyBytes: ByteArray = credentials.linkKeyBytes + private val internalShowAlreadyInACall = MutableStateFlow(false) + val showAlreadyInACall: StateFlow = internalShowAlreadyInACall + private val disposables = CompositeDisposable() init { @@ -61,6 +67,10 @@ class CreateCallLinkViewModel( disposables.dispose() } + fun setShowAlreadyInACall(showAlreadyInACall: Boolean) { + internalShowAlreadyInACall.update { showAlreadyInACall } + } + fun commitCallLink(): Single { return repository.ensureCallLinkCreated(credentials) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsFragment.kt index 08306a0105..d3a88eff79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsFragment.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -37,6 +38,8 @@ import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.logging.Log import org.signal.ringrtc.CallLinkState.Restrictions import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar +import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.YouAreAlreadyInACallSnackbar import org.thoughtcrime.securesms.calls.links.CallLinks import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment import org.thoughtcrime.securesms.calls.links.SignalCallRow @@ -44,6 +47,7 @@ import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.database.CallLinkTable import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials +import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult import org.thoughtcrime.securesms.sharing.v2.ShareActivity @@ -79,9 +83,11 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback { @Composable override fun FragmentContent() { val state by viewModel.state + val showAlreadyInACall by viewModel.showAlreadyInACall.collectAsState(false) CallLinkDetails( state, + showAlreadyInACall, this ) } @@ -93,7 +99,9 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback { override fun onJoinClicked() { val recipientSnapshot = viewModel.recipientSnapshot if (recipientSnapshot != null) { - CommunicationActions.startVideoCall(this, recipientSnapshot) + CommunicationActions.startVideoCall(this, recipientSnapshot) { + viewModel.showAlreadyInACall(true) + } } } @@ -206,7 +214,7 @@ private fun CallLinkDetailsPreview() { ) CallLinkTable.CallLink( recipientId = RecipientId.UNKNOWN, - roomId = credentials.roomId, + roomId = CallLinkRoomId.fromBytes(byteArrayOf(1, 2, 3, 4)), credentials = credentials, state = SignalCallLinkState( name = "Call Name", @@ -224,6 +232,7 @@ private fun CallLinkDetailsPreview() { false, callLink ), + true, object : CallLinkDetailsCallback { override fun onDeleteConfirmed() = Unit override fun onDeleteCanceled() = Unit @@ -243,10 +252,14 @@ private fun CallLinkDetailsPreview() { @Composable private fun CallLinkDetails( state: CallLinkDetailsState, + showAlreadyInACall: Boolean, callback: CallLinkDetailsCallback ) { Scaffolds.Settings( title = stringResource(id = R.string.CallLinkDetailsFragment__call_details), + snackbarHost = { + YouAreAlreadyInACallSnackbar(showAlreadyInACall) + }, onNavigationClick = callback::onNavigationClicked, navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24) ) { paddingValues -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsViewModel.kt index 11db2d97e2..e841892a28 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsViewModel.kt @@ -17,6 +17,9 @@ import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.subjects.BehaviorSubject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import org.signal.ringrtc.CallLinkState import org.thoughtcrime.securesms.calls.links.CallLinks import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository @@ -44,6 +47,9 @@ class CallLinkDetailsViewModel( val recipientSnapshot: Recipient? get() = recipientSubject.value + private val internalShowAlreadyInACall = MutableStateFlow(false) + val showAlreadyInACall: StateFlow = internalShowAlreadyInACall + init { disposables += repository.refreshCallLinkState(callLinkRoomId) disposables += CallLinks.watchCallLink(callLinkRoomId) @@ -80,6 +86,10 @@ class CallLinkDetailsViewModel( disposables.dispose() } + fun showAlreadyInACall(showAlreadyInACall: Boolean) { + internalShowAlreadyInACall.update { showAlreadyInACall } + } + fun setDisplayRevocationDialog(displayRevocationDialog: Boolean) { _state.value = _state.value.copy(displayRevocationDialog = displayRevocationDialog) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogContextMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogContextMenu.kt index abbba4181c..3407b02919 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogContextMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogContextMenu.kt @@ -7,6 +7,7 @@ import androidx.recyclerview.widget.RecyclerView import io.reactivex.rxjava3.kotlin.subscribeBy import org.signal.core.util.concurrent.LifecycleDisposable import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity import org.thoughtcrime.securesms.components.menu.ActionItem import org.thoughtcrime.securesms.components.menu.SignalContextMenu @@ -72,7 +73,9 @@ class CallLogContextMenu( iconRes = R.drawable.symbol_video_24, title = fragment.getString(R.string.CallContextMenu__video_call) ) { - CommunicationActions.startVideoCall(fragment, peer) + CommunicationActions.startVideoCall(fragment, peer) { + YouAreAlreadyInACallSnackbar.show(fragment.requireView()) + } } } @@ -85,7 +88,9 @@ class CallLogContextMenu( iconRes = R.drawable.symbol_phone_24, title = fragment.getString(R.string.CallContextMenu__audio_call) ) { - CommunicationActions.startVoiceCall(fragment, call.peer) + CommunicationActions.startVoiceCall(fragment, call.peer) { + YouAreAlreadyInACallSnackbar.show(fragment.requireView()) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt index ab715ee79c..9589422351 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt @@ -35,6 +35,7 @@ import org.signal.core.util.concurrent.addTo import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity import org.thoughtcrime.securesms.calls.new.NewCallActivity import org.thoughtcrime.securesms.components.Material3SearchToolbar @@ -391,12 +392,16 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal } override fun onStartAudioCallClicked(recipient: Recipient) { - CommunicationActions.startVoiceCall(this, recipient) + CommunicationActions.startVoiceCall(this, recipient) { + YouAreAlreadyInACallSnackbar.show(requireView()) + } } override fun onStartVideoCallClicked(recipient: Recipient, canUserBeginCall: Boolean) { if (canUserBeginCall) { - CommunicationActions.startVideoCall(this, recipient) + CommunicationActions.startVideoCall(this, recipient) { + YouAreAlreadyInACallSnackbar.show(requireView()) + } } else { ConversationDialogs.displayCannotStartGroupCallDueToPermissionsDialog(requireContext()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallActivity.kt index 61d06369d0..860709564c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallActivity.kt @@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.ContactSelectionActivity import org.thoughtcrime.securesms.ContactSelectionListFragment import org.thoughtcrime.securesms.InviteActivity import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient @@ -81,9 +82,13 @@ class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment private fun launch(recipient: Recipient) { if (recipient.isGroup) { - CommunicationActions.startVideoCall(this, recipient) + CommunicationActions.startVideoCall(this, recipient) { + YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content)) + } } else { - CommunicationActions.startVoiceCall(this, recipient) + CommunicationActions.startVoiceCall(this, recipient) { + YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content)) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt index 2ba69ba46b..bb683793b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.badges.Badges import org.thoughtcrime.securesms.badges.Badges.displayBadges import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment +import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar import org.thoughtcrime.securesms.components.AvatarImageView import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper import org.thoughtcrime.securesms.components.settings.DSLConfiguration @@ -442,11 +443,15 @@ class ConversationSettingsFragment : DSLSettingsFragment( .setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() } .show() } else { - CommunicationActions.startVideoCall(requireActivity(), state.recipient) + CommunicationActions.startVideoCall(requireActivity(), state.recipient) { + YouAreAlreadyInACallSnackbar.show(requireView()) + } } }, onAudioClick = { - CommunicationActions.startVoiceCall(requireActivity(), state.recipient) + CommunicationActions.startVoiceCall(requireActivity(), state.recipient) { + YouAreAlreadyInACallSnackbar.show(requireView()) + } }, onMuteClick = { if (!state.buttonStripState.isMuted) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java index 5b2c32f5f9..cbf399eedf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/SharedContactDetailsActivity.java @@ -24,6 +24,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.thoughtcrime.securesms.PassphraseRequiredActivity; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar; import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; @@ -214,7 +215,9 @@ public class SharedContactDetailsActivity extends PassphraseRequiredActivity { }); callButtonView.setOnClickListener(v -> { - ContactUtil.selectRecipientThroughDialog(this, pushUsers, dynamicLanguage.getCurrentLocale(), recipient -> CommunicationActions.startVoiceCall(this, recipient)); + ContactUtil.selectRecipientThroughDialog(this, pushUsers, dynamicLanguage.getCurrentLocale(), recipient -> CommunicationActions.startVoiceCall(this, recipient, () -> { + YouAreAlreadyInACallSnackbar.show(callButtonView); + })); }); } else if (!systemUsers.isEmpty()) { inviteButtonView.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 23b4d31c5b..80c62c258d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -108,6 +108,7 @@ import org.thoughtcrime.securesms.badges.gifts.viewgift.received.ViewReceivedGif import org.thoughtcrime.securesms.badges.gifts.viewgift.sent.ViewSentGiftBottomSheet import org.thoughtcrime.securesms.banner.banners.ServiceOutageBanner import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner +import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar import org.thoughtcrime.securesms.components.AnimatingToggle import org.thoughtcrime.securesms.components.ComposeText import org.thoughtcrime.securesms.components.ConversationSearchBottomBar @@ -1494,7 +1495,9 @@ class ConversationFragment : private fun handleVideoCall() { val recipient = viewModel.recipientSnapshot ?: return if (!recipient.isGroup) { - CommunicationActions.startVideoCall(this, recipient) + CommunicationActions.startVideoCall(this, recipient) { + YouAreAlreadyInACallSnackbar.show(requireView()) + } return } @@ -1510,7 +1513,9 @@ class ConversationFragment : if (notAllowed) { ConversationDialogs.displayCannotStartGroupCallDueToPermissionsDialog(requireContext()) } else { - CommunicationActions.startVideoCall(this, recipient) + CommunicationActions.startVideoCall(this, recipient) { + YouAreAlreadyInACallSnackbar.show(requireView()) + } } } } @@ -2962,7 +2967,9 @@ class ConversationFragment : override fun onJoinGroupCallClicked() { val activity = activity ?: return val recipient = viewModel.recipientSnapshot ?: return - CommunicationActions.startVideoCall(activity, recipient) + CommunicationActions.startVideoCall(activity, recipient) { + YouAreAlreadyInACallSnackbar.show(requireView()) + } } override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) { @@ -3280,7 +3287,9 @@ class ConversationFragment : } override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) { - CommunicationActions.startVideoCall(this@ConversationFragment, callLinkRootKey) + CommunicationActions.startVideoCall(this@ConversationFragment, callLinkRootKey) { + YouAreAlreadyInACallSnackbar.show(requireView()) + } } override fun onShowSafetyTips(forGroup: Boolean) { @@ -3420,7 +3429,9 @@ class ConversationFragment : override fun handleDial() { val recipient: Recipient = viewModel.recipientSnapshot ?: return - CommunicationActions.startVoiceCall(this@ConversationFragment, recipient) + CommunicationActions.startVoiceCall(this@ConversationFragment, recipient) { + YouAreAlreadyInACallSnackbar.show(requireView()) + } } override fun handleViewMedia() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java index fda8f9f410..9d72f057f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java @@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.avatar.view.AvatarView; import org.thoughtcrime.securesms.badges.BadgeImageView; import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment; +import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar; import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon; import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference; import org.thoughtcrime.securesms.fonts.SignalSymbols; @@ -270,12 +271,12 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF return Unit.INSTANCE; }, () -> { - viewModel.onSecureVideoCallClicked(requireActivity()); + viewModel.onSecureVideoCallClicked(requireActivity(), () -> YouAreAlreadyInACallSnackbar.show(requireView())); return Unit.INSTANCE; }, () -> { if (buttonStripState.isAudioSecure()) { - viewModel.onSecureCallClicked(requireActivity()); + viewModel.onSecureCallClicked(requireActivity(), () -> YouAreAlreadyInACallSnackbar.show(requireView())); } else { viewModel.onInsecureCallClicked(requireActivity()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java index eac7aaa0f9..9e9ada41a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java @@ -7,9 +7,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; -import androidx.core.app.ActivityCompat; import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; @@ -162,16 +160,16 @@ final class RecipientDialogViewModel extends ViewModel { recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startConversation(activity, recipient, null)); } - void onSecureCallClicked(@NonNull FragmentActivity activity) { - recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startVoiceCall(activity, recipient)); + void onSecureCallClicked(@NonNull FragmentActivity activity, @NonNull CommunicationActions.OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) { + recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startVoiceCall(activity, recipient, onUserAlreadyInAnotherCall)); } void onInsecureCallClicked(@NonNull FragmentActivity activity) { recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startInsecureCall(activity, recipient)); } - void onSecureVideoCallClicked(@NonNull FragmentActivity activity) { - recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startVideoCall(activity, recipient)); + void onSecureVideoCallClicked(@NonNull FragmentActivity activity, @NonNull CommunicationActions.OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) { + recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startVideoCall(activity, recipient, onUserAlreadyInAnotherCall)); } void onBlockClicked(@NonNull FragmentActivity activity) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java index 1ea8000a3c..846d4f9b2b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallActionProcessorDelegate.java @@ -51,7 +51,7 @@ public class ActiveCallActionProcessorDelegate extends WebRtcActionProcessor { @Override protected @NonNull WebRtcServiceState handleIsInCallQuery(@NonNull WebRtcServiceState currentState, @Nullable ResultReceiver resultReceiver) { if (resultReceiver != null) { - resultReceiver.send(1, null); + resultReceiver.send(1, ActiveCallData.fromCallState(currentState).toBundle()); } return currentState; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallData.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallData.kt new file mode 100644 index 0000000000..dd73399084 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallData.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.service.webrtc + +import android.os.Bundle +import android.os.Parcelable +import androidx.core.os.bundleOf +import kotlinx.parcelize.Parcelize +import org.signal.core.util.getParcelableCompat +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState + +/** + * Active call data to be returned from calls to isInCallQuery. + */ +@Parcelize +data class ActiveCallData( + val recipientId: RecipientId +) : Parcelable { + + companion object { + private const val KEY = "ACTIVE_CALL_DATA" + + @JvmStatic + fun fromCallState(webRtcServiceState: WebRtcServiceState): ActiveCallData { + return ActiveCallData( + webRtcServiceState.callInfoState.callRecipient.id + ) + } + + @JvmStatic + fun fromBundle(bundle: Bundle): ActiveCallData { + return bundle.getParcelableCompat(KEY, ActiveCallData::class.java)!! + } + } + + fun toBundle(): Bundle = bundleOf(KEY to this) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java index 773f4243d4..73ee3a8590 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java @@ -49,7 +49,7 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor { @Override protected @NonNull WebRtcServiceState handleIsInCallQuery(@NonNull WebRtcServiceState currentState, @Nullable ResultReceiver resultReceiver) { if (resultReceiver != null) { - resultReceiver.send(1, null); + resultReceiver.send(1, ActiveCallData.fromCallState(currentState).toBundle()); } return currentState; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java index 7467d1e7f7..cd61ac9470 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java @@ -36,7 +36,7 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor { @Override protected @NonNull WebRtcServiceState handleIsInCallQuery(@NonNull WebRtcServiceState currentState, @Nullable ResultReceiver resultReceiver) { if (resultReceiver != null) { - resultReceiver.send(1, null); + resultReceiver.send(1, ActiveCallData.fromCallState(currentState).toBundle()); } return currentState; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java index 39308e9907..18ee543951 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -100,7 +100,7 @@ public abstract class WebRtcActionProcessor { protected @NonNull WebRtcServiceState handleIsInCallQuery(@NonNull WebRtcServiceState currentState, @Nullable ResultReceiver resultReceiver) { if (resultReceiver != null) { - resultReceiver.send(0, null); + resultReceiver.send(0, ActiveCallData.fromCallState(currentState).toBundle()); } return currentState; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java index b2bc5f1992..e98c6eb48d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java @@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.profiles.manage.UsernameRepository; import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameLinkConversionResult; import org.thoughtcrime.securesms.proxy.ProxyBottomSheetFragment; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.service.webrtc.ActiveCallData; import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; @@ -65,18 +66,18 @@ public class CommunicationActions { /** * Start a voice call. Assumes that permission request results will be routed to a handler on the Fragment. */ - public static void startVoiceCall(@NonNull Fragment fragment, @NonNull Recipient recipient) { - startVoiceCall(new FragmentCallContext(fragment), recipient); + public static void startVoiceCall(@NonNull Fragment fragment, @NonNull Recipient recipient, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) { + startVoiceCall(new FragmentCallContext(fragment), recipient, onUserAlreadyInAnotherCall); } /** * Start a voice call. Assumes that permission request results will be routed to a handler on the Activity. */ - public static void startVoiceCall(@NonNull FragmentActivity activity, @NonNull Recipient recipient) { - startVoiceCall(new ActivityCallContext(activity), recipient); + public static void startVoiceCall(@NonNull FragmentActivity activity, @NonNull Recipient recipient, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) { + startVoiceCall(new ActivityCallContext(activity), recipient, onUserAlreadyInAnotherCall); } - private static void startVoiceCall(@NonNull CallContext callContext, @NonNull Recipient recipient) { + private static void startVoiceCall(@NonNull CallContext callContext, @NonNull Recipient recipient, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) { if (TelephonyUtil.isAnyPstnLineBusy(callContext.getContext())) { Toast.makeText(callContext.getContext(), R.string.CommunicationActions_a_cellular_call_is_already_in_progress, @@ -90,7 +91,12 @@ public class CommunicationActions { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { if (resultCode == 1) { - startCallInternal(callContext, recipient, false, false); + ActiveCallData activeCallData = ActiveCallData.fromBundle(resultData); + if (Objects.equals(activeCallData.getRecipientId(), recipient.getId())) { + startCallInternal(callContext, recipient, false, false); + } else { + onUserAlreadyInAnotherCall.onUserAlreadyInAnotherCall(); + } } else { new MaterialAlertDialogBuilder(callContext.getContext()) .setMessage(R.string.CommunicationActions_start_voice_call) @@ -109,18 +115,18 @@ public class CommunicationActions { /** * Start a video call. Assumes that permission request results will be routed to a handler on the Fragment. */ - public static void startVideoCall(@NonNull Fragment fragment, @NonNull Recipient recipient) { - startVideoCall(new FragmentCallContext(fragment), recipient, false); + public static void startVideoCall(@NonNull Fragment fragment, @NonNull Recipient recipient, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) { + startVideoCall(new FragmentCallContext(fragment), recipient, false, onUserAlreadyInAnotherCall); } /** * Start a video call. Assumes that permission request results will be routed to a handler on the Activity. */ - public static void startVideoCall(@NonNull FragmentActivity activity, @NonNull Recipient recipient) { - startVideoCall(new ActivityCallContext(activity), recipient, false); + public static void startVideoCall(@NonNull FragmentActivity activity, @NonNull Recipient recipient, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) { + startVideoCall(new ActivityCallContext(activity), recipient, false, onUserAlreadyInAnotherCall); } - private static void startVideoCall(@NonNull CallContext callContext, @NonNull Recipient recipient, boolean fromCallLink) { + private static void startVideoCall(@NonNull CallContext callContext, @NonNull Recipient recipient, boolean fromCallLink, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) { if (TelephonyUtil.isAnyPstnLineBusy(callContext.getContext())) { Toast.makeText(callContext.getContext(), R.string.CommunicationActions_a_cellular_call_is_already_in_progress, @@ -132,7 +138,16 @@ public class CommunicationActions { AppDependencies.getSignalCallManager().isCallActive(new ResultReceiver(new Handler(Looper.getMainLooper())) { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { - startCallInternal(callContext, recipient, resultCode != 1, fromCallLink); + if (resultCode == 1) { + ActiveCallData activeCallData = ActiveCallData.fromBundle(resultData); + if (Objects.equals(activeCallData.getRecipientId(), recipient.getId())) { + startCallInternal(callContext, recipient, false, fromCallLink); + } else { + onUserAlreadyInAnotherCall.onUserAlreadyInAnotherCall(); + } + } else { + startCallInternal(callContext, recipient, true, fromCallLink); + } } }); } @@ -307,7 +322,7 @@ public class CommunicationActions { } } - public static void handlePotentialCallLinkUrl(@NonNull FragmentActivity activity, @NonNull String potentialUrl) { + public static void handlePotentialCallLinkUrl(@NonNull FragmentActivity activity, @NonNull String potentialUrl, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) { if (!CallLinks.isCallLink(potentialUrl)) { return; } @@ -323,7 +338,7 @@ public class CommunicationActions { return; } - startVideoCall(new ActivityCallContext(activity), rootKey); + startVideoCall(new ActivityCallContext(activity), rootKey, onUserAlreadyInAnotherCall); } /** @@ -332,11 +347,11 @@ public class CommunicationActions { * * @param fragment The fragment, which will be used for context and permissions routing. */ - public static void startVideoCall(@NonNull Fragment fragment, @NonNull CallLinkRootKey rootKey) { - startVideoCall(new FragmentCallContext(fragment), rootKey); + public static void startVideoCall(@NonNull Fragment fragment, @NonNull CallLinkRootKey rootKey, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) { + startVideoCall(new FragmentCallContext(fragment), rootKey, onUserAlreadyInAnotherCall); } - private static void startVideoCall(@NonNull CallContext callContext, @NonNull CallLinkRootKey rootKey) { + private static void startVideoCall(@NonNull CallContext callContext, @NonNull CallLinkRootKey rootKey, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) { SimpleTask.run(() -> { CallLinkRoomId roomId = CallLinkRoomId.fromBytes(rootKey.deriveRoomId()); CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getOrCreateCallLinkByRootKey(rootKey); @@ -354,7 +369,7 @@ public class CommunicationActions { .setPositiveButton(android.R.string.ok, null) .show(); } else { - startVideoCall(callContext, callLinkRecipient.get(), true); + startVideoCall(callContext, callLinkRecipient.get(), true, onUserAlreadyInAnotherCall); } }); } @@ -532,4 +547,8 @@ public class CommunicationActions { return fragment.getParentFragmentManager(); } } + + public interface OnUserAlreadyInAnotherCall { + void onUserAlreadyInAnotherCall(); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d2de317616..0dacd880bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -346,6 +346,8 @@ Invalid link This is not a valid call link. Make sure the entire link is intact and correct before attempting to join. + + You are already in a call diff --git a/core-ui/src/main/java/org/signal/core/ui/Snackbars.kt b/core-ui/src/main/java/org/signal/core/ui/Snackbars.kt index ddc32dc21f..5a5ad104a8 100644 --- a/core-ui/src/main/java/org/signal/core/ui/Snackbars.kt +++ b/core-ui/src/main/java/org/signal/core/ui/Snackbars.kt @@ -13,6 +13,7 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarVisuals import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import org.signal.core.ui.theme.LocalSnackbarColors import org.signal.core.ui.theme.SignalTheme @@ -24,8 +25,8 @@ import org.signal.core.ui.theme.SignalTheme */ object Snackbars { @Composable - fun Host(snackbarHostState: SnackbarHostState) { - SnackbarHost(hostState = snackbarHostState) { + fun Host(snackbarHostState: SnackbarHostState, modifier: Modifier = Modifier) { + SnackbarHost(hostState = snackbarHostState, modifier = modifier) { Default(snackbarData = it) } }