Implement proper in-call status for call links.

This commit is contained in:
Alex Hart
2023-08-10 10:41:09 -03:00
parent ee1291c816
commit 13853c708e
8 changed files with 76 additions and 40 deletions

View File

@@ -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<RecipientId> 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();

View File

@@ -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<CallParticipant> = if (callParticipantsState?.callState == WebRtcViewModel.State.CALL_CONNECTED) {
listOf(CallParticipant(recipient = Recipient.self())) + (callParticipantsState?.allRemoteParticipants?.map { it } ?: emptyList())
} else {

View File

@@ -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
}

View File

@@ -60,7 +60,7 @@ public class WebRtcCallViewModel extends ViewModel {
private final SingleLiveEvent<Event> events = new SingleLiveEvent<>();
private final BehaviorSubject<Long> elapsed = BehaviorSubject.createDefault(-1L);
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
private final DefaultValueLiveData<CallParticipantsState> participantsState = new DefaultValueLiveData<>(CallParticipantsState.STARTING_STATE);
private final BehaviorSubject<CallParticipantsState> participantsState = BehaviorSubject.createDefault(CallParticipantsState.STARTING_STATE);
private final SingleLiveEvent<CallParticipantListUpdate> callParticipantListUpdate = new SingleLiveEvent<>();
private final MutableLiveData<Collection<RecipientId>> identityChangedRecipients = new MutableLiveData<>(Collections.emptyList());
private final LiveData<SafetyNumberChangeEvent> safetyNumberChangeEvent = LiveDataUtil.combineLatest(isInPipMode, identityChangedRecipients, SafetyNumberChangeEvent::new);
@@ -68,11 +68,11 @@ public class WebRtcCallViewModel extends ViewModel {
private final LiveData<List<GroupMemberEntry.FullMember>> groupMembers = Transformations.switchMap(groupRecipient, r -> Transformations.distinctUntilChanged(new LiveGroup(r.requireGroupId()).getFullMembers()));
private final LiveData<List<GroupMemberEntry.FullMember>> groupMembersChanged = LiveDataUtil.skip(groupMembers, 1);
private final LiveData<Integer> groupMemberCount = Transformations.map(groupMembers, List::size);
private final LiveData<Boolean> shouldShowSpeakerHint = Transformations.map(participantsState, this::shouldShowSpeakerHint);
private final Observable<Boolean> shouldShowSpeakerHint = participantsState.map(this::shouldShowSpeakerHint);
private final LiveData<Orientation> orientation;
private final MutableLiveData<Boolean> isLandscapeEnabled = new MutableLiveData<>();
private final LiveData<Integer> controlsRotation;
private final Observer<List<GroupMemberEntry.FullMember>> groupMemberStateUpdater = m -> participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), m));
private final Observer<List<GroupMemberEntry.FullMember>> groupMemberStateUpdater = m -> participantsState.onNext(CallParticipantsState.update(participantsState.getValue(), m));
private final MutableLiveData<WebRtcEphemeralState> ephemeralState = new MutableLiveData<>();
private final BehaviorSubject<PendingParticipantCollection> 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<Event> getEvents() {
@@ -148,22 +148,31 @@ public class WebRtcCallViewModel extends ViewModel {
return Observable.combineLatest(
elapsedTime,
pendingParticipants,
(time, participants) -> {
Set<PendingParticipantCollection.Entry> pending = participants.getUnresolvedPendingParticipants();
if (pending.isEmpty()) {
participantsState,
(time, pendingParticipants, participantsState) -> {
if (!getRecipient().get().isCallLink()) {
return new InCallStatus.ElapsedTime(time);
}
Set<PendingParticipantCollection.Entry> 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<CallParticipantsState> getCallParticipantsState() {
public Observable<CallParticipantsState> getCallParticipantsState() {
return participantsState;
}
public @Nullable CallParticipantsState getCallParticipantsStateSnapshot() {
return participantsState.getValue();
}
public LiveData<CallParticipantListUpdate> getCallParticipantListUpdate() {
return callParticipantListUpdate;
}
@@ -180,7 +189,7 @@ public class WebRtcCallViewModel extends ViewModel {
return groupMemberCount;
}
public LiveData<Boolean> shouldShowSpeakerHint() {
public Observable<Boolean> 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());

View File

@@ -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() {

View File

@@ -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)

View File

@@ -345,17 +345,17 @@ public class WebRtcServiceStateBuilder {
return this;
}
public @NonNull CallInfoStateBuilder setPendingParticipants(@NonNull List<Recipient> pendingParticipants) {
public @NonNull CallInfoStateBuilder setCallLinkPendingParticipants(@NonNull List<Recipient> 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;
}

View File

@@ -1766,6 +1766,11 @@
<item quantity="one">%1$d person waiting</item>
<item quantity="other">%1$d people waiting</item>
</plurals>
<!-- Displayed in call status during call link when no users are pending -->
<plurals name="WebRtcCallActivity__d_people">
<item quantity="one">%1$d person</item>
<item quantity="other">%1$d people</item>
</plurals>
<!-- Title of dialog displayed when a user's join request is denied for call link entry -->
<string name="WebRtcCallActivity__join_request_denied">Join request denied</string>
<!-- Message of dialog displayed when a user's join request is denied for call link entry -->
@@ -1775,7 +1780,6 @@
<!-- Message of dialog displayed when a user is removed from a call link -->
<string name="WebRtcCallActivity__someone_has_removed_you_from_the_call">Someone has removed you from the call.</string>
<!-- WebRtcCallView -->
<string name="WebRtcCallView__signal_call">Signal Call</string>
<string name="WebRtcCallView__signal_video_call">Signal Video Call</string>