From 13853c708e2ff3b3e692ef3dcc460cc37514042f Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 10 Aug 2023 10:41:09 -0300 Subject: [PATCH] Implement proper in-call status for call links. --- .../securesms/WebRtcCallActivity.java | 30 +++++++++----- .../components/webrtc/CallLinkInfoSheet.kt | 8 +++- .../components/webrtc/InCallStatus.kt | 9 +++- .../webrtc/WebRtcCallViewModel.java | 41 +++++++++++-------- .../CallParticipantsListDialog.java | 10 +++-- .../CallLinkConnectedActionProcessor.kt | 6 +-- .../state/WebRtcServiceStateBuilder.java | 6 +-- app/src/main/res/values/strings.xml | 6 ++- 8 files changed, 76 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index 95ab93eb5e..300fbeb9df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -37,6 +37,7 @@ import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.content.ContextCompat; import androidx.core.util.Consumer; +import androidx.lifecycle.LiveDataReactiveStreams; import androidx.lifecycle.ViewModelProvider; import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter; import androidx.window.layout.DisplayFeature; @@ -103,6 +104,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.BackpressureStrategy; import io.reactivex.rxjava3.disposables.Disposable; import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE; @@ -256,7 +258,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan super.onPause(); if (!viewModel.isCallStarting()) { - CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); + CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) { finish(); } @@ -276,7 +278,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan } if (!viewModel.isCallStarting()) { - CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); + CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) { ApplicationDependencies.getSignalCallManager().cancelPreJoin(); } @@ -432,7 +434,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan lifecycleDisposable.add(viewModel.getInCallstatus().subscribe(this::handleInCallStatus)); boolean isStartedFromCallLink = getIntent().getBooleanExtra(WebRtcCallActivity.EXTRA_STARTED_FROM_CALL_LINK, false); - LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(), + LiveDataUtil.combineLatest(LiveDataReactiveStreams.fromPublisher(viewModel.getCallParticipantsState().toFlowable(BackpressureStrategy.LATEST)), viewModel.getOrientationAndLandscapeEnabled(), viewModel.getEphemeralState(), (s, o, e) -> new CallParticipantsViewState(s, e, o.first == PORTRAIT_BOTTOM_EDGE, o.second, isStartedFromCallLink)) @@ -441,10 +443,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent); viewModel.getGroupMembersChanged().observe(this, unused -> updateGroupMembersForGroupCall()); viewModel.getGroupMemberCount().observe(this, this::handleGroupMemberCountChange); - viewModel.shouldShowSpeakerHint().observe(this, this::updateSpeakerHint); + lifecycleDisposable.add(viewModel.shouldShowSpeakerHint().subscribe(this::updateSpeakerHint)); callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> { - CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); + CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); if (state != null) { if (state.needsNewRequestSizes()) { requestNewSizesThrottle.publish(() -> ApplicationDependencies.getSignalCallManager().updateRenderedResolutions()); @@ -541,15 +543,23 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan } callScreen.setStatus(getString(R.string.WebRtcCallActivity__signal_s, ellapsedTimeFormatter.toString())); - } else if (inCallStatus instanceof InCallStatus.PendingUsers) { - int waiting = ((InCallStatus.PendingUsers) inCallStatus).getPendingUserCount(); + } else if (inCallStatus instanceof InCallStatus.PendingCallLinkUsers) { + int waiting = ((InCallStatus.PendingCallLinkUsers) inCallStatus).getPendingUserCount(); callScreen.setStatus(getResources().getQuantityString( R.plurals.WebRtcCallActivity__d_people_waiting, waiting, waiting )); - } else { + } else if (inCallStatus instanceof InCallStatus.JoinedCallLinkUsers) { + int joined = ((InCallStatus.JoinedCallLinkUsers) inCallStatus).getJoinedUserCount(); + + callScreen.setStatus(getResources().getQuantityString( + R.plurals.WebRtcCallActivity__d_people, + joined, + joined + )); + }else { throw new AssertionError(); } } @@ -758,7 +768,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan @Override public void onSendAnywayAfterSafetyNumberChange(@NonNull List changedRecipients) { - CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); + CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); if (state == null) { return; @@ -776,7 +786,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan @Override public void onCanceled() { - CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); + CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); if (state != null && state.getGroupCallState().isNotIdle()) { if (state.getCallState().isPreJoinOrNetworkUnavailable()) { ApplicationDependencies.getSignalCallManager().cancelPreJoin(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallLinkInfoSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallLinkInfoSheet.kt index df114964fd..b39f1a6b43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallLinkInfoSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallLinkInfoSheet.kt @@ -47,8 +47,10 @@ import androidx.core.os.bundleOf import androidx.fragment.app.FragmentManager import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels +import androidx.lifecycle.toLiveData import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.BackpressureStrategy import io.reactivex.rxjava3.kotlin.subscribeBy import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -119,7 +121,11 @@ class CallLinkInfoSheet : ComposeBottomSheetDialogFragment() { @Composable override fun SheetContent() { val callLinkDetailsState by callLinkDetailsViewModel.state - val callParticipantsState by webRtcCallViewModel.callParticipantsState.observeAsState() + val callParticipantsState by webRtcCallViewModel.callParticipantsState + .toFlowable(BackpressureStrategy.LATEST) + .toLiveData() + .observeAsState() + val participants: ImmutableList = if (callParticipantsState?.callState == WebRtcViewModel.State.CALL_CONNECTED) { listOf(CallParticipant(recipient = Recipient.self())) + (callParticipantsState?.allRemoteParticipants?.map { it } ?: emptyList()) } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/InCallStatus.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/InCallStatus.kt index 2b5abd98e8..430ca88d9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/InCallStatus.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/InCallStatus.kt @@ -16,7 +16,12 @@ sealed interface InCallStatus { data class ElapsedTime(val elapsedTime: Long) : InCallStatus /** - * The number of users requesting to join a call. + * The number of users requesting to join a call link. */ - data class PendingUsers(val pendingUserCount: Int) : InCallStatus + data class PendingCallLinkUsers(val pendingUserCount: Int) : InCallStatus + + /** + * The number of users in a call link. + */ + data class JoinedCallLinkUsers(val joinedUserCount: Int) : InCallStatus } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java index fe93cf769c..aaa7f93c85 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java @@ -60,7 +60,7 @@ public class WebRtcCallViewModel extends ViewModel { private final SingleLiveEvent events = new SingleLiveEvent<>(); private final BehaviorSubject elapsed = BehaviorSubject.createDefault(-1L); private final MutableLiveData liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live()); - private final DefaultValueLiveData participantsState = new DefaultValueLiveData<>(CallParticipantsState.STARTING_STATE); + private final BehaviorSubject participantsState = BehaviorSubject.createDefault(CallParticipantsState.STARTING_STATE); private final SingleLiveEvent callParticipantListUpdate = new SingleLiveEvent<>(); private final MutableLiveData> identityChangedRecipients = new MutableLiveData<>(Collections.emptyList()); private final LiveData safetyNumberChangeEvent = LiveDataUtil.combineLatest(isInPipMode, identityChangedRecipients, SafetyNumberChangeEvent::new); @@ -68,11 +68,11 @@ public class WebRtcCallViewModel extends ViewModel { private final LiveData> groupMembers = Transformations.switchMap(groupRecipient, r -> Transformations.distinctUntilChanged(new LiveGroup(r.requireGroupId()).getFullMembers())); private final LiveData> groupMembersChanged = LiveDataUtil.skip(groupMembers, 1); private final LiveData groupMemberCount = Transformations.map(groupMembers, List::size); - private final LiveData shouldShowSpeakerHint = Transformations.map(participantsState, this::shouldShowSpeakerHint); + private final Observable shouldShowSpeakerHint = participantsState.map(this::shouldShowSpeakerHint); private final LiveData orientation; private final MutableLiveData isLandscapeEnabled = new MutableLiveData<>(); private final LiveData controlsRotation; - private final Observer> groupMemberStateUpdater = m -> participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), m)); + private final Observer> groupMemberStateUpdater = m -> participantsState.onNext(CallParticipantsState.update(participantsState.getValue(), m)); private final MutableLiveData ephemeralState = new MutableLiveData<>(); private final BehaviorSubject pendingParticipants = BehaviorSubject.create(); @@ -135,7 +135,7 @@ public class WebRtcCallViewModel extends ViewModel { public void setFoldableState(@NonNull WebRtcControls.FoldableState foldableState) { this.foldableState.postValue(foldableState); - ThreadUtil.runOnMain(() -> participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), foldableState))); + ThreadUtil.runOnMain(() -> participantsState.onNext(CallParticipantsState.update(participantsState.getValue(), foldableState))); } public LiveData getEvents() { @@ -148,22 +148,31 @@ public class WebRtcCallViewModel extends ViewModel { return Observable.combineLatest( elapsedTime, pendingParticipants, - (time, participants) -> { - Set pending = participants.getUnresolvedPendingParticipants(); - - if (pending.isEmpty()) { + participantsState, + (time, pendingParticipants, participantsState) -> { + if (!getRecipient().get().isCallLink()) { return new InCallStatus.ElapsedTime(time); + } + + Set pending = pendingParticipants.getUnresolvedPendingParticipants(); + + if (!pending.isEmpty()) { + return new InCallStatus.PendingCallLinkUsers(pending.size()); } else { - return new InCallStatus.PendingUsers(pending.size()); + return new InCallStatus.JoinedCallLinkUsers((int) participantsState.getParticipantCount().orElse(0)); } } ).distinctUntilChanged(); } - public LiveData getCallParticipantsState() { + public Observable getCallParticipantsState() { return participantsState; } + public @Nullable CallParticipantsState getCallParticipantsStateSnapshot() { + return participantsState.getValue(); + } + public LiveData getCallParticipantListUpdate() { return callParticipantListUpdate; } @@ -180,7 +189,7 @@ public class WebRtcCallViewModel extends ViewModel { return groupMemberCount; } - public LiveData shouldShowSpeakerHint() { + public Observable shouldShowSpeakerHint() { return shouldShowSpeakerHint; } @@ -216,7 +225,7 @@ public class WebRtcCallViewModel extends ViewModel { public void setIsInPipMode(boolean isInPipMode) { this.isInPipMode.setValue(isInPipMode); - participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), isInPipMode)); + participantsState.onNext(CallParticipantsState.update(participantsState.getValue(), isInPipMode)); } public void setIsLandscapeEnabled(boolean isLandscapeEnabled) { @@ -239,7 +248,7 @@ public class WebRtcCallViewModel extends ViewModel { events.setValue(new Event.ShowSwipeToSpeakerHint()); } - participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), page)); + participantsState.onNext(CallParticipantsState.update(participantsState.getValue(), page)); } public void onLocalPictureInPictureClicked() { @@ -248,8 +257,8 @@ public class WebRtcCallViewModel extends ViewModel { return; } - participantsState.setValue(CallParticipantsState.setExpanded(participantsState.getValue(), - state.getLocalRenderState() != WebRtcLocalRenderState.EXPANDED)); + participantsState.onNext(CallParticipantsState.setExpanded(participantsState.getValue(), + state.getLocalRenderState() != WebRtcLocalRenderState.EXPANDED)); } public void onDismissedVideoTooltip() { @@ -271,7 +280,7 @@ public class WebRtcCallViewModel extends ViewModel { boolean wasScreenSharing = state.getFocusedParticipant().isScreenSharing(); CallParticipantsState newState = CallParticipantsState.update(state, webRtcViewModel, enableVideo); - participantsState.setValue(newState); + participantsState.onNext(newState); if (switchOnFirstScreenShare && !wasScreenSharing && newState.getFocusedParticipant().isScreenSharing()) { switchOnFirstScreenShare = false; events.setValue(new Event.SwitchToSpeaker()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListDialog.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListDialog.java index 1fb9bb7eee..961ce980c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/participantslist/CallParticipantsListDialog.java @@ -18,6 +18,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.annimon.stream.OptionalLong; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import org.signal.core.util.concurrent.LifecycleDisposable; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel; @@ -34,6 +35,8 @@ public class CallParticipantsListDialog extends BottomSheetDialogFragment { private RecyclerView participantList; private CallParticipantsListAdapter adapter; + private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable(); + public static void show(@NonNull FragmentManager manager) { CallParticipantsListDialog fragment = new CallParticipantsListDialog(); @@ -69,14 +72,13 @@ public class CallParticipantsListDialog extends BottomSheetDialogFragment { } @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { final WebRtcCallViewModel viewModel = new ViewModelProvider(requireActivity()).get(WebRtcCallViewModel.class); initializeList(); - viewModel.getCallParticipantsState().observe(getViewLifecycleOwner(), this::updateList); + lifecycleDisposable.bindTo(getViewLifecycleOwner()); + lifecycleDisposable.add(viewModel.getCallParticipantsState().subscribe(this::updateList)); } private void initializeList() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkConnectedActionProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkConnectedActionProcessor.kt index ce23fc873c..d046ec73dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkConnectedActionProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkConnectedActionProcessor.kt @@ -56,7 +56,7 @@ class CallLinkConnectedActionProcessor( return superState.builder() .changeCallInfoState() - .setPendingParticipants(pendingParticipants) + .setCallLinkPendingParticipants(pendingParticipants) .build() } @@ -72,7 +72,7 @@ class CallLinkConnectedActionProcessor( currentState .builder() .changeCallInfoState() - .setPendingParticipantApproved(recipient) + .setCallLinkPendingParticipantApproved(recipient) .build() } catch (e: CallException) { Log.w(tag, "Failed to approve user.", e) @@ -93,7 +93,7 @@ class CallLinkConnectedActionProcessor( currentState .builder() .changeCallInfoState() - .setPendingParticipantRejected(recipient) + .setCallLinkPendingParticipantRejected(recipient) .build() } catch (e: CallException) { Log.w(tag, "Failed to deny user.", e) diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java index 41f5feecfb..8a32ec5378 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java @@ -345,17 +345,17 @@ public class WebRtcServiceStateBuilder { return this; } - public @NonNull CallInfoStateBuilder setPendingParticipants(@NonNull List pendingParticipants) { + public @NonNull CallInfoStateBuilder setCallLinkPendingParticipants(@NonNull List pendingParticipants) { toBuild.setPendingParticipants(toBuild.getPendingParticipants().withRecipients(pendingParticipants)); return this; } - public @NonNull CallInfoStateBuilder setPendingParticipantApproved(@NonNull Recipient participant) { + public @NonNull CallInfoStateBuilder setCallLinkPendingParticipantApproved(@NonNull Recipient participant) { toBuild.setPendingParticipants(toBuild.getPendingParticipants().withApproval(participant)); return this; } - public @NonNull CallInfoStateBuilder setPendingParticipantRejected(@NonNull Recipient participant) { + public @NonNull CallInfoStateBuilder setCallLinkPendingParticipantRejected(@NonNull Recipient participant) { toBuild.setPendingParticipants(toBuild.getPendingParticipants().withDenial(participant)); return this; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e17a8a49aa..dd69fb5890 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1766,6 +1766,11 @@ %1$d person waiting %1$d people waiting + + + %1$d person + %1$d people + Join request denied @@ -1775,7 +1780,6 @@ Someone has removed you from the call. - Signal Call Signal Video Call