mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-26 12:44:38 +00:00
Implement proper in-call status for call links.
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user