mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-27 21:24:42 +00:00
Convert WebRtcCallViewModel into Kotlin.
This commit is contained in:
committed by
Greyson Parrelli
parent
27a3cc0305
commit
eac44de527
@@ -45,6 +45,7 @@ import org.signal.core.ui.DarkPreview
|
||||
import org.signal.core.ui.Previews
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.WebRtcCallViewModel
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -80,7 +81,7 @@ class PendingParticipantsBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
System.currentTimeMillis().milliseconds
|
||||
}
|
||||
|
||||
val participants = viewModel.pendingParticipants
|
||||
val participants = viewModel.getPendingParticipants()
|
||||
.map { it.pendingParticipantCollection.getAllPendingParticipants(launchTime).toList() }
|
||||
.subscribeAsState(initial = emptyList())
|
||||
|
||||
|
||||
@@ -16,16 +16,12 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
class WebRtcCallRepository {
|
||||
public final class WebRtcCallRepository {
|
||||
|
||||
private final Context context;
|
||||
|
||||
WebRtcCallRepository(@NonNull Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
private WebRtcCallRepository() {}
|
||||
|
||||
@WorkerThread
|
||||
void getIdentityRecords(@NonNull Recipient recipient, @NonNull Consumer<IdentityRecordList> consumer) {
|
||||
public static void getIdentityRecords(@NonNull Recipient recipient, @NonNull Consumer<IdentityRecordList> consumer) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
List<Recipient> recipients;
|
||||
|
||||
|
||||
@@ -1,583 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.LiveDataReactiveStreams;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.CallControlsState;
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.CallEvent;
|
||||
import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.CallParticipantId;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.webrtc.PendingParticipantCollection;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.PendingParticipantsState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState;
|
||||
import org.thoughtcrime.securesms.util.NetworkUtil;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor;
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject;
|
||||
|
||||
public class WebRtcCallViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
|
||||
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
|
||||
private final MutableLiveData<WebRtcControls.FoldableState> foldableState = new MutableLiveData<>(WebRtcControls.FoldableState.flat());
|
||||
private final LiveData<WebRtcControls> controlsWithFoldableState = LiveDataUtil.combineLatest(foldableState, webRtcControls, this::updateControlsFoldableState);
|
||||
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, controlsWithFoldableState, this::getRealWebRtcControls);
|
||||
private final SingleLiveEvent<CallEvent> events = new SingleLiveEvent<>();
|
||||
private final BehaviorSubject<Long> elapsed = BehaviorSubject.createDefault(-1L);
|
||||
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
|
||||
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);
|
||||
private final LiveData<Recipient> groupRecipient = LiveDataUtil.filter(Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData), Recipient::isActiveGroup);
|
||||
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 Observable<Boolean> shouldShowSpeakerHint = participantsState.map(this::shouldShowSpeakerHint);
|
||||
private final MutableLiveData<Boolean> isLandscapeEnabled = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> canEnterPipMode = new MutableLiveData<>(false);
|
||||
private final Observer<List<GroupMemberEntry.FullMember>> groupMemberStateUpdater = m -> participantsState.onNext(CallParticipantsState.update(participantsState.getValue(), m));
|
||||
private final MutableLiveData<WebRtcEphemeralState> ephemeralState = new MutableLiveData<>();
|
||||
private final BehaviorProcessor<RecipientId> recipientId = BehaviorProcessor.createDefault(RecipientId.UNKNOWN);
|
||||
|
||||
private final BehaviorSubject<PendingParticipantCollection> pendingParticipants = BehaviorSubject.create();
|
||||
|
||||
private final Handler elapsedTimeHandler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable elapsedTimeRunnable = this::handleTick;
|
||||
private final Runnable stopOutgoingRingingMode = this::stopOutgoingRingingMode;
|
||||
|
||||
private boolean canDisplayTooltipIfNeeded = true;
|
||||
private boolean canDisplaySwitchCameraTooltipIfNeeded = true;
|
||||
private boolean canDisplayPopupIfNeeded = true;
|
||||
private boolean hasEnabledLocalVideo = false;
|
||||
private boolean wasInOutgoingRingingMode = false;
|
||||
private long callConnectedTime = -1;
|
||||
private boolean answerWithVideoAvailable = false;
|
||||
private List<CallParticipant> previousParticipantsList = Collections.emptyList();
|
||||
private boolean callStarting = false;
|
||||
private boolean switchOnFirstScreenShare = true;
|
||||
private boolean showScreenShareTip = true;
|
||||
|
||||
private final WebRtcCallRepository repository = new WebRtcCallRepository(AppDependencies.getApplication());
|
||||
|
||||
public WebRtcCallViewModel() {
|
||||
groupMembers.observeForever(groupMemberStateUpdater);
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getMicrophoneEnabled() {
|
||||
return Transformations.distinctUntilChanged(microphoneEnabled);
|
||||
}
|
||||
|
||||
public LiveData<WebRtcControls> getWebRtcControls() {
|
||||
return realWebRtcControls;
|
||||
}
|
||||
|
||||
public LiveRecipient getRecipient() {
|
||||
return liveRecipient.getValue();
|
||||
}
|
||||
|
||||
public Flowable<Recipient> getRecipientFlowable() {
|
||||
return recipientId.switchMap(id -> Recipient.observable(id).toFlowable(BackpressureStrategy.LATEST)).observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
public void setRecipient(@NonNull Recipient recipient) {
|
||||
recipientId.onNext(recipient.getId());
|
||||
liveRecipient.setValue(recipient.live());
|
||||
}
|
||||
|
||||
public void setFoldableState(@NonNull WebRtcControls.FoldableState foldableState) {
|
||||
this.foldableState.postValue(foldableState);
|
||||
|
||||
ThreadUtil.runOnMain(() -> participantsState.onNext(CallParticipantsState.update(participantsState.getValue(), foldableState)));
|
||||
}
|
||||
|
||||
public LiveData<CallEvent> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
public Observable<InCallStatus> getInCallstatus() {
|
||||
Observable<Long> elapsedTime = elapsed.map(timeInCall -> callConnectedTime == -1 ? -1 : timeInCall);
|
||||
|
||||
return Observable.combineLatest(
|
||||
elapsedTime,
|
||||
pendingParticipants,
|
||||
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.JoinedCallLinkUsers((int) participantsState.getParticipantCount().orElse(0));
|
||||
}
|
||||
}
|
||||
).distinctUntilChanged().observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
public Flowable<CallControlsState> getCallControlsState(@NonNull LifecycleOwner lifecycleOwner) {
|
||||
// Calculate this separately so we have a value when the recipient is not a group.
|
||||
Flowable<Integer> groupSize = recipientId.filter(id -> id != RecipientId.UNKNOWN)
|
||||
.switchMap(id -> Recipient.observable(id).toFlowable(BackpressureStrategy.LATEST))
|
||||
.map(recipient -> {
|
||||
if (recipient.isActiveGroup()) {
|
||||
return SignalDatabase.groups().getGroupMemberIds(recipient.requireGroupId(), GroupTable.MemberSet.FULL_MEMBERS_INCLUDING_SELF).size();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return Flowable.combineLatest(
|
||||
getCallParticipantsState().toFlowable(BackpressureStrategy.LATEST),
|
||||
LiveDataReactiveStreams.toPublisher(getWebRtcControls(), lifecycleOwner),
|
||||
groupSize,
|
||||
CallControlsState::fromViewModelData
|
||||
);
|
||||
}
|
||||
|
||||
public Observable<CallParticipantsState> getCallParticipantsState() {
|
||||
return participantsState;
|
||||
}
|
||||
|
||||
public @Nullable CallParticipantsState getCallParticipantsStateSnapshot() {
|
||||
return participantsState.getValue();
|
||||
}
|
||||
|
||||
public LiveData<CallParticipantListUpdate> getCallParticipantListUpdate() {
|
||||
return callParticipantListUpdate;
|
||||
}
|
||||
|
||||
public LiveData<SafetyNumberChangeEvent> getSafetyNumberChangeEvent() {
|
||||
return safetyNumberChangeEvent;
|
||||
}
|
||||
|
||||
public LiveData<List<GroupMemberEntry.FullMember>> getGroupMembersChanged() {
|
||||
return groupMembersChanged;
|
||||
}
|
||||
|
||||
public LiveData<Integer> getGroupMemberCount() {
|
||||
return groupMemberCount;
|
||||
}
|
||||
|
||||
public Observable<Boolean> shouldShowSpeakerHint() {
|
||||
return shouldShowSpeakerHint.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
public WebRtcAudioOutput getCurrentAudioOutput() {
|
||||
return getWebRtcControls().getValue().getAudioOutput();
|
||||
}
|
||||
|
||||
public LiveData<WebRtcEphemeralState> getEphemeralState() {
|
||||
return ephemeralState;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> canEnterPipMode() {
|
||||
return canEnterPipMode;
|
||||
}
|
||||
|
||||
public boolean isAnswerWithVideoAvailable() {
|
||||
return answerWithVideoAvailable;
|
||||
}
|
||||
|
||||
public boolean isCallStarting() {
|
||||
return callStarting;
|
||||
}
|
||||
|
||||
public @NonNull Observable<PendingParticipantsState> getPendingParticipants() {
|
||||
Observable<Boolean> isInPipMode = participantsState
|
||||
.map(CallParticipantsState::isInPipMode)
|
||||
.distinctUntilChanged();
|
||||
|
||||
return Observable.combineLatest(pendingParticipants, isInPipMode, PendingParticipantsState::new);
|
||||
}
|
||||
|
||||
public @NonNull PendingParticipantCollection getPendingParticipantsSnapshot() {
|
||||
return pendingParticipants.getValue();
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setIsInPipMode(boolean isInPipMode) {
|
||||
this.isInPipMode.setValue(isInPipMode);
|
||||
|
||||
participantsState.onNext(CallParticipantsState.update(participantsState.getValue(), isInPipMode));
|
||||
}
|
||||
|
||||
public void setIsLandscapeEnabled(boolean isLandscapeEnabled) {
|
||||
this.isLandscapeEnabled.postValue(isLandscapeEnabled);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setIsViewingFocusedParticipant(@NonNull CallParticipantsState.SelectedPage page) {
|
||||
if (page == CallParticipantsState.SelectedPage.FOCUSED) {
|
||||
SignalStore.tooltips().markGroupCallSpeakerViewSeen();
|
||||
}
|
||||
|
||||
CallParticipantsState state = participantsState.getValue();
|
||||
if (showScreenShareTip &&
|
||||
state.getFocusedParticipant().isScreenSharing() &&
|
||||
state.isViewingFocusedParticipant() &&
|
||||
page == CallParticipantsState.SelectedPage.GRID)
|
||||
{
|
||||
showScreenShareTip = false;
|
||||
events.setValue(CallEvent.ShowSwipeToSpeakerHint.INSTANCE);
|
||||
}
|
||||
|
||||
participantsState.onNext(CallParticipantsState.update(participantsState.getValue(), page));
|
||||
}
|
||||
|
||||
public void onLocalPictureInPictureClicked() {
|
||||
CallParticipantsState state = participantsState.getValue();
|
||||
participantsState.onNext(CallParticipantsState.setExpanded(participantsState.getValue(),
|
||||
state.getLocalRenderState() != WebRtcLocalRenderState.EXPANDED));
|
||||
}
|
||||
|
||||
public void onDismissedVideoTooltip() {
|
||||
canDisplayTooltipIfNeeded = false;
|
||||
}
|
||||
|
||||
public void onDismissedSwitchCameraTooltip() {
|
||||
canDisplaySwitchCameraTooltipIfNeeded = false;
|
||||
SignalStore.tooltips().markCallingSwitchCameraTooltipSeen();
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) {
|
||||
canEnterPipMode.setValue(!webRtcViewModel.getState().isPreJoinOrNetworkUnavailable());
|
||||
if (callStarting && webRtcViewModel.getState().isPassedPreJoin()) {
|
||||
callStarting = false;
|
||||
}
|
||||
|
||||
CallParticipant localParticipant = webRtcViewModel.getLocalParticipant();
|
||||
|
||||
microphoneEnabled.setValue(localParticipant.isMicrophoneEnabled());
|
||||
|
||||
CallParticipantsState state = participantsState.getValue();
|
||||
boolean wasScreenSharing = state.getFocusedParticipant().isScreenSharing();
|
||||
CallParticipantsState newState = CallParticipantsState.update(state, webRtcViewModel, enableVideo);
|
||||
|
||||
participantsState.onNext(newState);
|
||||
if (switchOnFirstScreenShare && !wasScreenSharing && newState.getFocusedParticipant().isScreenSharing()) {
|
||||
switchOnFirstScreenShare = false;
|
||||
events.setValue(CallEvent.SwitchToSpeaker.INSTANCE);
|
||||
}
|
||||
|
||||
if (webRtcViewModel.getGroupState().isConnected()) {
|
||||
if (!containsPlaceholders(previousParticipantsList)) {
|
||||
CallParticipantListUpdate update = CallParticipantListUpdate.computeDeltaUpdate(previousParticipantsList, webRtcViewModel.getRemoteParticipants());
|
||||
callParticipantListUpdate.setValue(update);
|
||||
}
|
||||
|
||||
previousParticipantsList = webRtcViewModel.getRemoteParticipants();
|
||||
|
||||
identityChangedRecipients.setValue(webRtcViewModel.getIdentityChangedParticipants());
|
||||
}
|
||||
|
||||
updateWebRtcControls(webRtcViewModel.getState(),
|
||||
webRtcViewModel.getGroupState(),
|
||||
localParticipant.getCameraState().isEnabled(),
|
||||
webRtcViewModel.isRemoteVideoEnabled(),
|
||||
webRtcViewModel.isRemoteVideoOffer(),
|
||||
localParticipant.isMoreThanOneCameraAvailable(),
|
||||
webRtcViewModel.hasAtLeastOneRemote(),
|
||||
webRtcViewModel.getActiveDevice(),
|
||||
webRtcViewModel.getAvailableDevices(),
|
||||
webRtcViewModel.getRemoteDevicesCount().orElse(0),
|
||||
webRtcViewModel.getParticipantLimit(),
|
||||
webRtcViewModel.getRecipient().isCallLink(),
|
||||
webRtcViewModel.getRemoteParticipants().size() > CallParticipantsState.SMALL_GROUP_MAX);
|
||||
|
||||
pendingParticipants.onNext(webRtcViewModel.getPendingParticipants());
|
||||
|
||||
if (newState.isInOutgoingRingingMode()) {
|
||||
cancelTimer();
|
||||
if (!wasInOutgoingRingingMode) {
|
||||
elapsedTimeHandler.postDelayed(stopOutgoingRingingMode, CallParticipantsState.MAX_OUTGOING_GROUP_RING_DURATION);
|
||||
}
|
||||
wasInOutgoingRingingMode = true;
|
||||
} else {
|
||||
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
|
||||
callConnectedTime = wasInOutgoingRingingMode ? System.currentTimeMillis() : webRtcViewModel.getCallConnectedTime();
|
||||
startTimer();
|
||||
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_CONNECTED || webRtcViewModel.getGroupState().isNotIdleOrConnected()) {
|
||||
cancelTimer();
|
||||
callConnectedTime = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
if (event.getGroupState().isNotIdle()) {
|
||||
callScreen.setRingGroup(event.shouldRingGroup());
|
||||
|
||||
if (event.shouldRingGroup() && event.areRemoteDevicesInCall()) {
|
||||
AppDependencies.getSignalCallManager().setRingGroup(false);
|
||||
}
|
||||
}
|
||||
*/
|
||||
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_PRE_JOIN && webRtcViewModel.getGroupState().isNotIdle()) {
|
||||
// Set flag
|
||||
|
||||
if (webRtcViewModel.shouldRingGroup() && webRtcViewModel.areRemoteDevicesInCall()) {
|
||||
AppDependencies.getSignalCallManager().setRingGroup(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (localParticipant.getCameraState().isEnabled()) {
|
||||
canDisplayTooltipIfNeeded = false;
|
||||
hasEnabledLocalVideo = true;
|
||||
events.setValue(CallEvent.DismissVideoTooltip.INSTANCE);
|
||||
}
|
||||
|
||||
// If remote video is enabled and we a) haven't shown our video and b) have not dismissed the popup
|
||||
if (canDisplayTooltipIfNeeded && webRtcViewModel.isRemoteVideoEnabled() && !hasEnabledLocalVideo) {
|
||||
canDisplayTooltipIfNeeded = false;
|
||||
events.setValue(CallEvent.ShowVideoTooltip.INSTANCE);
|
||||
}
|
||||
|
||||
if (canDisplayPopupIfNeeded && webRtcViewModel.isCellularConnection() && NetworkUtil.isConnectedWifi(AppDependencies.getApplication())) {
|
||||
canDisplayPopupIfNeeded = false;
|
||||
events.setValue(CallEvent.ShowWifiToCellularPopup.INSTANCE);
|
||||
} else if (!webRtcViewModel.isCellularConnection()) {
|
||||
canDisplayPopupIfNeeded = true;
|
||||
}
|
||||
|
||||
if (SignalStore.tooltips().showCallingSwitchCameraTooltip() &&
|
||||
canDisplaySwitchCameraTooltipIfNeeded &&
|
||||
localParticipant.getCameraState().isEnabled() &&
|
||||
webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED &&
|
||||
!newState.getAllRemoteParticipants().isEmpty()
|
||||
)
|
||||
{
|
||||
canDisplaySwitchCameraTooltipIfNeeded = false;
|
||||
events.setValue(CallEvent.ShowSwitchCameraTooltip.INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void updateFromEphemeralState(@NonNull WebRtcEphemeralState state) {
|
||||
ephemeralState.setValue(state);
|
||||
}
|
||||
|
||||
private boolean containsPlaceholders(@NonNull List<CallParticipant> callParticipants) {
|
||||
return Stream.of(callParticipants).anyMatch(p -> p.getCallParticipantId().getDemuxId() == CallParticipantId.DEFAULT_ID);
|
||||
}
|
||||
|
||||
private void updateWebRtcControls(@NonNull WebRtcViewModel.State state,
|
||||
@NonNull WebRtcViewModel.GroupCallState groupState,
|
||||
boolean isLocalVideoEnabled,
|
||||
boolean isRemoteVideoEnabled,
|
||||
boolean isRemoteVideoOffer,
|
||||
boolean isMoreThanOneCameraAvailable,
|
||||
boolean hasAtLeastOneRemote,
|
||||
@NonNull SignalAudioManager.AudioDevice activeDevice,
|
||||
@NonNull Set<SignalAudioManager.AudioDevice> availableDevices,
|
||||
long remoteDevicesCount,
|
||||
@Nullable Long participantLimit,
|
||||
boolean isCallLink,
|
||||
boolean hasParticipantOverflow)
|
||||
{
|
||||
final WebRtcControls.CallState callState;
|
||||
|
||||
switch (state) {
|
||||
case CALL_PRE_JOIN:
|
||||
callState = WebRtcControls.CallState.PRE_JOIN;
|
||||
break;
|
||||
case CALL_INCOMING:
|
||||
callState = WebRtcControls.CallState.INCOMING;
|
||||
answerWithVideoAvailable = isRemoteVideoOffer;
|
||||
break;
|
||||
case CALL_OUTGOING:
|
||||
case CALL_RINGING:
|
||||
callState = WebRtcControls.CallState.OUTGOING;
|
||||
break;
|
||||
case CALL_ACCEPTED_ELSEWHERE:
|
||||
case CALL_DECLINED_ELSEWHERE:
|
||||
case CALL_ONGOING_ELSEWHERE:
|
||||
callState = WebRtcControls.CallState.HANDLED_ELSEWHERE;
|
||||
break;
|
||||
case CALL_NEEDS_PERMISSION:
|
||||
case CALL_BUSY:
|
||||
case CALL_DISCONNECTED:
|
||||
callState = WebRtcControls.CallState.ENDING;
|
||||
break;
|
||||
case CALL_DISCONNECTED_GLARE:
|
||||
callState = WebRtcControls.CallState.INCOMING;
|
||||
break;
|
||||
case NETWORK_FAILURE:
|
||||
callState = WebRtcControls.CallState.ERROR;
|
||||
break;
|
||||
case CALL_RECONNECTING:
|
||||
callState = WebRtcControls.CallState.RECONNECTING;
|
||||
break;
|
||||
default:
|
||||
callState = WebRtcControls.CallState.ONGOING;
|
||||
}
|
||||
|
||||
final WebRtcControls.GroupCallState groupCallState;
|
||||
|
||||
switch (groupState) {
|
||||
case DISCONNECTED:
|
||||
groupCallState = WebRtcControls.GroupCallState.DISCONNECTED;
|
||||
break;
|
||||
case CONNECTING:
|
||||
case RECONNECTING:
|
||||
groupCallState = (participantLimit == null || remoteDevicesCount < participantLimit) ? WebRtcControls.GroupCallState.CONNECTING
|
||||
: WebRtcControls.GroupCallState.FULL;
|
||||
break;
|
||||
case CONNECTED_AND_PENDING:
|
||||
groupCallState = WebRtcControls.GroupCallState.PENDING;
|
||||
break;
|
||||
case CONNECTED:
|
||||
case CONNECTED_AND_JOINING:
|
||||
case CONNECTED_AND_JOINED:
|
||||
groupCallState = WebRtcControls.GroupCallState.CONNECTED;
|
||||
break;
|
||||
default:
|
||||
groupCallState = WebRtcControls.GroupCallState.NONE;
|
||||
break;
|
||||
}
|
||||
|
||||
webRtcControls.setValue(new WebRtcControls(isLocalVideoEnabled,
|
||||
isRemoteVideoEnabled || isRemoteVideoOffer,
|
||||
isMoreThanOneCameraAvailable,
|
||||
Boolean.TRUE.equals(isInPipMode.getValue()),
|
||||
hasAtLeastOneRemote,
|
||||
callState,
|
||||
groupCallState,
|
||||
participantLimit,
|
||||
WebRtcControls.FoldableState.flat(),
|
||||
activeDevice,
|
||||
availableDevices,
|
||||
isCallLink,
|
||||
hasParticipantOverflow));
|
||||
}
|
||||
|
||||
private @NonNull WebRtcControls updateControlsFoldableState(@NonNull WebRtcControls.FoldableState foldableState, @NonNull WebRtcControls controls) {
|
||||
return controls.withFoldableState(foldableState);
|
||||
}
|
||||
|
||||
private @NonNull WebRtcControls getRealWebRtcControls(boolean isInPipMode, @NonNull WebRtcControls controls) {
|
||||
return isInPipMode ? WebRtcControls.PIP : controls;
|
||||
}
|
||||
|
||||
private boolean shouldShowSpeakerHint(@NonNull CallParticipantsState state) {
|
||||
return !state.isInPipMode() &&
|
||||
state.getRemoteDevicesCount().orElse(0) > 1 &&
|
||||
state.getGroupCallState().isConnected() &&
|
||||
!SignalStore.tooltips().hasSeenGroupCallSpeakerView();
|
||||
}
|
||||
|
||||
private void startTimer() {
|
||||
cancelTimer();
|
||||
elapsedTimeHandler.removeCallbacks(stopOutgoingRingingMode);
|
||||
|
||||
elapsedTimeHandler.post(elapsedTimeRunnable);
|
||||
}
|
||||
|
||||
private void stopOutgoingRingingMode() {
|
||||
if (callConnectedTime == -1) {
|
||||
callConnectedTime = System.currentTimeMillis();
|
||||
startTimer();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTick() {
|
||||
if (callConnectedTime == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
long newValue = (System.currentTimeMillis() - callConnectedTime) / 1000;
|
||||
|
||||
elapsed.onNext(newValue);
|
||||
|
||||
elapsedTimeHandler.postDelayed(elapsedTimeRunnable, 1000);
|
||||
}
|
||||
|
||||
private void cancelTimer() {
|
||||
elapsedTimeHandler.removeCallbacks(elapsedTimeRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
cancelTimer();
|
||||
groupMembers.removeObserver(groupMemberStateUpdater);
|
||||
}
|
||||
|
||||
public void startCall(boolean isVideoCall) {
|
||||
callStarting = true;
|
||||
Recipient recipient = getRecipient().get();
|
||||
if (recipient.isGroup()) {
|
||||
repository.getIdentityRecords(recipient, identityRecords -> {
|
||||
if (identityRecords.isUntrusted(false) || identityRecords.isUnverified(false)) {
|
||||
List<IdentityRecord> records = identityRecords.getUnverifiedRecords();
|
||||
records.addAll(identityRecords.getUntrustedRecords());
|
||||
events.postValue(new CallEvent.ShowGroupCallSafetyNumberChange(records));
|
||||
} else {
|
||||
events.postValue(new CallEvent.StartCall(isVideoCall));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
events.postValue(new CallEvent.StartCall(isVideoCall));
|
||||
}
|
||||
}
|
||||
|
||||
public static class SafetyNumberChangeEvent {
|
||||
private final boolean isInPipMode;
|
||||
private final Collection<RecipientId> recipientIds;
|
||||
|
||||
private SafetyNumberChangeEvent(boolean isInPipMode, @NonNull Collection<RecipientId> recipientIds) {
|
||||
this.isInPipMode = isInPipMode;
|
||||
this.recipientIds = recipientIds;
|
||||
}
|
||||
|
||||
public boolean isInPipMode() {
|
||||
return isInPipMode;
|
||||
}
|
||||
|
||||
public @NonNull Collection<RecipientId> getRecipientIds() {
|
||||
return recipientIds;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,6 @@ public final class WebRtcControls {
|
||||
false);
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public WebRtcControls(boolean isLocalVideoEnabled,
|
||||
boolean isRemoteVideoEnabled,
|
||||
boolean isMoreThanOneCameraAvailable,
|
||||
|
||||
@@ -61,7 +61,7 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatar
|
||||
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarImage
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.WebRtcCallViewModel
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.events.CallParticipant
|
||||
|
||||
@@ -52,9 +52,9 @@ import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
|
||||
import org.thoughtcrime.securesms.components.InsetAwareConstraintLayout
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallOverflowPopupWindow
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.CallInfoCallbacks
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.WebRtcCallViewModel
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
import org.thoughtcrime.securesms.util.padding
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
@@ -47,7 +47,7 @@ import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||
import kotlinx.coroutines.delay
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.WebRtcCallViewModel
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.events.CallParticipant
|
||||
import org.thoughtcrime.securesms.events.GroupCallRaiseHandEvent
|
||||
|
||||
@@ -21,7 +21,7 @@ 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;
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.WebRtcCallViewModel;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rxjava3.subscribeAsState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel
|
||||
import org.thoughtcrime.securesms.components.webrtc.controls.CallInfoView
|
||||
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel
|
||||
import org.thoughtcrime.securesms.components.webrtc.controls.RaiseHandSnackbar
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper
|
||||
import org.thoughtcrime.securesms.util.VibrateUtil
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Entry-point for receiving and making Signal calls.
|
||||
*/
|
||||
class CallActivity : BaseActivity(), CallControlsCallback {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(CallActivity::class.java)
|
||||
|
||||
private const val VIBRATE_DURATION = 50
|
||||
}
|
||||
|
||||
private val callPermissionsDialogController = CallPermissionsDialogController()
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private val webRtcCallViewModel: WebRtcCallViewModel by viewModels()
|
||||
private val controlsAndInfoViewModel: ControlsAndInfoViewModel by viewModels()
|
||||
private val viewModel: CallViewModel by viewModel {
|
||||
CallViewModel(
|
||||
webRtcCallViewModel,
|
||||
controlsAndInfoViewModel
|
||||
)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
|
||||
super.attachBaseContext(newBase)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val fullscreenHelper = FullscreenHelper(this)
|
||||
|
||||
lifecycleDisposable.bindTo(this)
|
||||
|
||||
val callInfoCallbacks = CallInfoCallbacks(this, controlsAndInfoViewModel)
|
||||
|
||||
observeCallEvents()
|
||||
viewModel.processCallIntent(CallIntent(intent))
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
viewModel.callActions.collect {
|
||||
when (it) {
|
||||
CallViewModel.Action.EnableVideo -> onVideoToggleClick(true)
|
||||
is CallViewModel.Action.ShowGroupCallSafetyNumberChangeDialog -> SafetyNumberBottomSheet.forGroupCall(it.untrustedIdentities).show(supportFragmentManager)
|
||||
CallViewModel.Action.SwitchToSpeaker -> Unit // TODO - Switch user to speaker view.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val callControlsState by webRtcCallViewModel.getCallControlsState(lifecycleOwner).subscribeAsState(initial = CallControlsState())
|
||||
val callParticipantsState by webRtcCallViewModel.callParticipantsState.subscribeAsState(initial = CallParticipantsState())
|
||||
val callScreenState by viewModel.callScreenState.collectAsStateWithLifecycle()
|
||||
val recipient by remember(callScreenState.callRecipientId) {
|
||||
Recipient.observable(callScreenState.callRecipientId)
|
||||
}.subscribeAsState(Recipient.UNKNOWN)
|
||||
|
||||
LaunchedEffect(callControlsState.isGroupRingingAllowed) {
|
||||
viewModel.onGroupRingAllowedChanged(callControlsState.isGroupRingingAllowed)
|
||||
}
|
||||
|
||||
LaunchedEffect(callParticipantsState.callState) {
|
||||
if (callParticipantsState.callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES)
|
||||
}
|
||||
|
||||
if (callParticipantsState.callState == WebRtcViewModel.State.CALL_RECONNECTING) {
|
||||
VibrateUtil.vibrate(this@CallActivity, VIBRATE_DURATION)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(callScreenState.hangup) {
|
||||
val hangup = callScreenState.hangup
|
||||
if (hangup != null) {
|
||||
if (hangup.hangupMessageType == HangupMessage.Type.NEED_PERMISSION) {
|
||||
startActivity(CalleeMustAcceptMessageRequestActivity.createIntent(this@CallActivity, callParticipantsState.recipient.id))
|
||||
} else {
|
||||
delay(hangup.delay)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
var areControlsVisible by remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(areControlsVisible) {
|
||||
if (areControlsVisible) {
|
||||
fullscreenHelper.showSystemUI()
|
||||
} else {
|
||||
fullscreenHelper.hideSystemUI()
|
||||
}
|
||||
}
|
||||
|
||||
val callScreenDialogType by viewModel.dialog.collectAsStateWithLifecycle(CallScreenDialogType.NONE)
|
||||
|
||||
SignalTheme {
|
||||
Surface {
|
||||
CallScreen(
|
||||
callRecipient = recipient,
|
||||
webRtcCallState = callParticipantsState.callState,
|
||||
callScreenState = callScreenState,
|
||||
callControlsState = callControlsState,
|
||||
callControlsCallback = this,
|
||||
callParticipantsPagerState = CallParticipantsPagerState(
|
||||
callParticipants = callParticipantsState.gridParticipants,
|
||||
focusedParticipant = callParticipantsState.focusedParticipant,
|
||||
isRenderInPip = callParticipantsState.isInPipMode,
|
||||
hideAvatar = callParticipantsState.hideAvatar
|
||||
),
|
||||
overflowParticipants = callParticipantsState.listParticipants,
|
||||
localParticipant = callParticipantsState.localParticipant,
|
||||
localRenderState = callParticipantsState.localRenderState,
|
||||
callScreenDialogType = callScreenDialogType,
|
||||
callInfoView = {
|
||||
CallInfoView.View(
|
||||
webRtcCallViewModel = webRtcCallViewModel,
|
||||
controlsAndInfoViewModel = controlsAndInfoViewModel,
|
||||
callbacks = callInfoCallbacks,
|
||||
modifier = Modifier
|
||||
.alpha(it)
|
||||
)
|
||||
},
|
||||
raiseHandSnackbar = {
|
||||
RaiseHandSnackbar.View(
|
||||
webRtcCallViewModel = webRtcCallViewModel,
|
||||
showCallInfoListener = { /*TODO*/ },
|
||||
modifier = it
|
||||
)
|
||||
},
|
||||
onNavigationClick = { finish() },
|
||||
onLocalPictureInPictureClicked = webRtcCallViewModel::onLocalPictureInPictureClicked,
|
||||
onControlsToggled = { areControlsVisible = it },
|
||||
onCallScreenDialogDismissed = viewModel::onCallScreenDialogDismissed
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
viewModel.processCallIntent(CallIntent(intent))
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
Log.i(TAG, "onResume")
|
||||
super.onResume()
|
||||
|
||||
if (!EventBus.getDefault().isRegistered(viewModel)) {
|
||||
EventBus.getDefault().register(viewModel)
|
||||
}
|
||||
|
||||
val stickyEvent = EventBus.getDefault().getStickyEvent(WebRtcViewModel::class.java)
|
||||
if (stickyEvent == null) {
|
||||
Log.w(TAG, "Activity resumed without service event, perform delay destroy.")
|
||||
|
||||
lifecycleScope.launch {
|
||||
delay(1.seconds)
|
||||
val retryEvent = EventBus.getDefault().getStickyEvent(WebRtcViewModel::class.java)
|
||||
if (retryEvent == null) {
|
||||
Log.w(TAG, "Activity still without service event, finishing.")
|
||||
finish()
|
||||
} else {
|
||||
Log.i(TAG, "Event found after delay.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.consumeEnterPipOnResume()) {
|
||||
// TODO enterPipModeIfPossible()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
Log.i(TAG, "onPause")
|
||||
super.onPause()
|
||||
|
||||
if (!callPermissionsDialogController.isAskingForPermission && !webRtcCallViewModel.isCallStarting && !isChangingConfigurations) {
|
||||
val state = webRtcCallViewModel.callParticipantsStateSnapshot
|
||||
if (state != null && state.callState.isPreJoinOrNetworkUnavailable) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
Log.i(TAG, "onStop")
|
||||
super.onStop()
|
||||
|
||||
/*
|
||||
TODO
|
||||
ephemeralStateDisposable.dispose();
|
||||
*/
|
||||
|
||||
if (!isInPipMode() || isFinishing) {
|
||||
viewModel.unregisterEventBus()
|
||||
// TODO
|
||||
// requestNewSizesThrottle.clear();
|
||||
}
|
||||
|
||||
AppDependencies.signalCallManager.setEnableVideo(false)
|
||||
|
||||
if (!webRtcCallViewModel.isCallStarting && !isChangingConfigurations) {
|
||||
val state = webRtcCallViewModel.callParticipantsStateSnapshot
|
||||
if (state != null) {
|
||||
if (state.callState.isPreJoinOrNetworkUnavailable) {
|
||||
AppDependencies.signalCallManager.cancelPreJoin()
|
||||
} else if (state.callState.inOngoingCall && isInPipMode()) {
|
||||
AppDependencies.signalCallManager.relaunchPipOnForeground()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// TODO windowInfoTrackerCallbackAdapter.removeWindowLayoutInfoListener(windowLayoutInfoConsumer);
|
||||
viewModel.unregisterEventBus()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun onAudioDeviceSheetDisplayChanged(displayed: Boolean) {
|
||||
viewModel.onAudioDeviceSheetDisplayChanged(displayed)
|
||||
}
|
||||
|
||||
override fun onSelectedAudioDeviceChanged(audioDevice: WebRtcAudioDevice) {
|
||||
viewModel.onSelectedAudioDeviceChanged(audioDevice)
|
||||
}
|
||||
|
||||
override fun onVideoToggleClick(enabled: Boolean) {
|
||||
if (webRtcCallViewModel.recipient.get() != Recipient.UNKNOWN) {
|
||||
callPermissionsDialogController.requestCameraPermission(
|
||||
activity = this,
|
||||
onAllGranted = { viewModel.onVideoToggleChanged(enabled) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMicToggleClick(enabled: Boolean) {
|
||||
callPermissionsDialogController.requestAudioPermission(
|
||||
activity = this,
|
||||
onGranted = { viewModel.onMicToggledChanged(enabled) },
|
||||
onDenied = { viewModel.deny() }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onGroupRingingToggleClick(enabled: Boolean, allowed: Boolean) {
|
||||
viewModel.onGroupRingToggleChanged(enabled, allowed)
|
||||
}
|
||||
|
||||
override fun onAdditionalActionsClick() {
|
||||
viewModel.onAdditionalActionsClick()
|
||||
}
|
||||
|
||||
override fun onStartCallClick(isVideoCall: Boolean) {
|
||||
webRtcCallViewModel.startCall(isVideoCall)
|
||||
}
|
||||
|
||||
override fun onEndCallClick() {
|
||||
viewModel.hangup()
|
||||
}
|
||||
|
||||
override fun onVideoTooltipDismissed() {
|
||||
viewModel.onVideoTooltipDismissed()
|
||||
}
|
||||
|
||||
private fun observeCallEvents() {
|
||||
webRtcCallViewModel.events.observe(this) { event ->
|
||||
viewModel.onCallEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInPipMode(): Boolean {
|
||||
return isSystemPipEnabledAndAvailable() && isInPictureInPictureMode
|
||||
}
|
||||
|
||||
private fun isSystemPipEnabledAndAvailable(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= 26 && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,6 @@ package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
|
||||
/**
|
||||
* CallIntent wraps an intent inside one of the call activities to allow for easy typed access to the necessary data within it.
|
||||
@@ -23,11 +21,7 @@ class CallIntent(
|
||||
private const val CALL_INTENT_PREFIX = "CallIntent"
|
||||
|
||||
@JvmStatic
|
||||
fun getActivityClass(): Class<out Activity> = if (RemoteConfig.newCallUi || SignalStore.internal.newCallingUi) {
|
||||
CallActivity::class.java
|
||||
} else {
|
||||
WebRtcCallActivity::class.java
|
||||
}
|
||||
fun getActivityClass(): Class<out Activity> = WebRtcCallActivity::class.java
|
||||
|
||||
private fun getActionString(action: Action): String {
|
||||
return "$CALL_INTENT_PREFIX.${action.code}"
|
||||
|
||||
@@ -1,441 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel
|
||||
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.CallLinkDisconnectReason
|
||||
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Presentation logic and state holder for information that was generally done
|
||||
* in-activity for the V1 call screen.
|
||||
*/
|
||||
class CallViewModel(
|
||||
private val webRtcCallViewModel: WebRtcCallViewModel,
|
||||
private val controlsAndInfoViewModel: ControlsAndInfoViewModel
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(CallViewModel::class)
|
||||
}
|
||||
|
||||
private var previousEvent: WebRtcViewModel? = null
|
||||
|
||||
private var enableVideoIfAvailable = false
|
||||
|
||||
private var lastProcessedIntentTimestamp = 0L
|
||||
private var lastCallLinkDisconnectDialogShowTime = 0L
|
||||
|
||||
private var enterPipOnResume = false
|
||||
|
||||
private val internalCallScreenState = MutableStateFlow(CallScreenState())
|
||||
val callScreenState: StateFlow<CallScreenState> = internalCallScreenState
|
||||
|
||||
private val internalDialog = MutableStateFlow(CallScreenDialogType.NONE)
|
||||
val dialog: StateFlow<CallScreenDialogType> = internalDialog
|
||||
|
||||
private val internalCallActions = MutableSharedFlow<Action>()
|
||||
val callActions: Flow<Action> = internalCallActions
|
||||
|
||||
fun consumeEnterPipOnResume(): Boolean {
|
||||
val enter = enterPipOnResume
|
||||
enterPipOnResume = false
|
||||
return enter
|
||||
}
|
||||
|
||||
fun unregisterEventBus() {
|
||||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
|
||||
fun onMicToggledChanged(enabled: Boolean) {
|
||||
AppDependencies.signalCallManager.setMuteAudio(!enabled)
|
||||
|
||||
val update = if (enabled) CallControlsChange.MIC_ON else CallControlsChange.MIC_OFF
|
||||
performCallStateUpdateChange(update)
|
||||
}
|
||||
|
||||
fun onVideoToggleChanged(enabled: Boolean) {
|
||||
AppDependencies.signalCallManager.setEnableVideo(enabled)
|
||||
}
|
||||
|
||||
fun onGroupRingAllowedChanged(allowed: Boolean) {
|
||||
AppDependencies.signalCallManager.setRingGroup(allowed)
|
||||
}
|
||||
|
||||
fun onAdditionalActionsClick() {
|
||||
// TODO Toggle overflow popup
|
||||
}
|
||||
|
||||
/**
|
||||
* Denies the call. If successful, returns true.
|
||||
*/
|
||||
fun deny() {
|
||||
val recipient = webRtcCallViewModel.recipient.get()
|
||||
if (recipient != Recipient.UNKNOWN) {
|
||||
AppDependencies.signalCallManager.denyCall()
|
||||
|
||||
internalCallScreenState.update {
|
||||
it.copy(
|
||||
callStatus = CallString.ResourceString(R.string.RedPhone_ending_call),
|
||||
hangup = CallScreenState.Hangup(
|
||||
hangupMessageType = HangupMessage.Type.NORMAL
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun hangup() {
|
||||
Log.i(TAG, "Hangup pressed, handling termination now...")
|
||||
AppDependencies.signalCallManager.localHangup()
|
||||
|
||||
internalCallScreenState.update {
|
||||
it.copy(
|
||||
hangup = CallScreenState.Hangup(
|
||||
hangupMessageType = HangupMessage.Type.NORMAL
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onGroupRingToggleChanged(enabled: Boolean, allowed: Boolean) {
|
||||
if (allowed) {
|
||||
AppDependencies.signalCallManager.setRingGroup(enabled)
|
||||
val update = if (enabled) CallControlsChange.RINGING_ON else CallControlsChange.RINGING_OFF
|
||||
performCallStateUpdateChange(update)
|
||||
} else {
|
||||
AppDependencies.signalCallManager.setRingGroup(false)
|
||||
performCallStateUpdateChange(CallControlsChange.RINGING_DISABLED)
|
||||
}
|
||||
}
|
||||
|
||||
fun onCallScreenDialogDismissed() {
|
||||
internalDialog.update { CallScreenDialogType.NONE }
|
||||
}
|
||||
|
||||
fun onVideoTooltipDismissed() {
|
||||
webRtcCallViewModel.onDismissedVideoTooltip()
|
||||
internalCallScreenState.update { it.copy(displayVideoTooltip = false) }
|
||||
}
|
||||
|
||||
fun onCallEvent(event: CallEvent) {
|
||||
when (event) {
|
||||
CallEvent.DismissSwitchCameraTooltip -> internalCallScreenState.update { it.copy(displaySwitchCameraTooltip = false) }
|
||||
CallEvent.DismissVideoTooltip -> internalCallScreenState.update { it.copy(displayVideoTooltip = false) }
|
||||
is CallEvent.ShowGroupCallSafetyNumberChange -> {
|
||||
viewModelScope.launch {
|
||||
internalCallActions.emit(Action.ShowGroupCallSafetyNumberChangeDialog(event.identityRecords))
|
||||
}
|
||||
}
|
||||
CallEvent.ShowSwipeToSpeakerHint -> internalCallScreenState.update { it.copy(displaySwipeToSpeakerHint = true) }
|
||||
CallEvent.ShowSwitchCameraTooltip -> internalCallScreenState.update { it.copy(displaySwitchCameraTooltip = true) }
|
||||
CallEvent.ShowVideoTooltip -> internalCallScreenState.update { it.copy(displayVideoTooltip = true) }
|
||||
CallEvent.ShowWifiToCellularPopup -> internalCallScreenState.update { it.copy(displayWifiToCellularPopup = true) }
|
||||
is CallEvent.StartCall -> startCall(event.isVideoCall)
|
||||
CallEvent.SwitchToSpeaker -> {
|
||||
viewModelScope.launch {
|
||||
internalCallActions.emit(Action.SwitchToSpeaker)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
fun onWebRtcEvent(event: WebRtcViewModel) {
|
||||
Log.i(TAG, "Got message from service: " + event.describeDifference(previousEvent))
|
||||
previousEvent = event
|
||||
|
||||
webRtcCallViewModel.setRecipient(event.recipient)
|
||||
internalCallScreenState.update {
|
||||
it.copy(
|
||||
callRecipientId = event.recipient.id
|
||||
)
|
||||
}
|
||||
controlsAndInfoViewModel.setRecipient(event.recipient)
|
||||
|
||||
when (event.state) {
|
||||
WebRtcViewModel.State.IDLE -> Unit
|
||||
WebRtcViewModel.State.CALL_PRE_JOIN -> handlePreJoin(event)
|
||||
WebRtcViewModel.State.CALL_INCOMING -> Unit
|
||||
WebRtcViewModel.State.CALL_OUTGOING -> handleOutgoing(event)
|
||||
WebRtcViewModel.State.CALL_CONNECTED -> handleConnected(event)
|
||||
WebRtcViewModel.State.CALL_RINGING -> handleRinging()
|
||||
WebRtcViewModel.State.CALL_BUSY -> handleBusy()
|
||||
WebRtcViewModel.State.CALL_DISCONNECTED -> handleCallTerminated(HangupMessage.Type.NORMAL)
|
||||
WebRtcViewModel.State.CALL_DISCONNECTED_GLARE -> handleGlare(event.recipient.id)
|
||||
WebRtcViewModel.State.CALL_NEEDS_PERMISSION -> handleCallTerminated(HangupMessage.Type.NEED_PERMISSION)
|
||||
WebRtcViewModel.State.CALL_RECONNECTING -> handleReconnecting()
|
||||
WebRtcViewModel.State.NETWORK_FAILURE -> handleNetworkFailure()
|
||||
WebRtcViewModel.State.RECIPIENT_UNAVAILABLE -> handleRecipientUnavailable()
|
||||
WebRtcViewModel.State.NO_SUCH_USER -> Unit // TODO
|
||||
WebRtcViewModel.State.UNTRUSTED_IDENTITY -> Unit // TODO
|
||||
WebRtcViewModel.State.CALL_ACCEPTED_ELSEWHERE -> handleCallTerminated(HangupMessage.Type.ACCEPTED)
|
||||
WebRtcViewModel.State.CALL_DECLINED_ELSEWHERE -> handleCallTerminated(HangupMessage.Type.DECLINED)
|
||||
WebRtcViewModel.State.CALL_ONGOING_ELSEWHERE -> handleCallTerminated(HangupMessage.Type.BUSY)
|
||||
}
|
||||
|
||||
if (event.callLinkDisconnectReason != null && event.callLinkDisconnectReason.postedAt > lastCallLinkDisconnectDialogShowTime) {
|
||||
lastCallLinkDisconnectDialogShowTime = System.currentTimeMillis()
|
||||
|
||||
when (event.callLinkDisconnectReason) {
|
||||
is CallLinkDisconnectReason.RemovedFromCall -> internalDialog.update { CallScreenDialogType.REMOVED_FROM_CALL_LINK }
|
||||
is CallLinkDisconnectReason.DeniedRequestToJoinCall -> internalDialog.update { CallScreenDialogType.DENIED_REQUEST_TO_JOIN_CALL_LINK }
|
||||
}
|
||||
}
|
||||
|
||||
val enableVideo = event.localParticipant.cameraState.cameraCount > 0 && enableVideoIfAvailable
|
||||
webRtcCallViewModel.updateFromWebRtcViewModel(event, enableVideo)
|
||||
|
||||
if (enableVideo) {
|
||||
enableVideoIfAvailable = false
|
||||
viewModelScope.launch {
|
||||
internalCallActions.emit(Action.EnableVideo)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [alex] -- handle denied bluetooth permission
|
||||
}
|
||||
|
||||
private fun startCall(isVideoCall: Boolean) {
|
||||
enableVideoIfAvailable = isVideoCall
|
||||
|
||||
if (isVideoCall) {
|
||||
AppDependencies.signalCallManager.startOutgoingVideoCall(webRtcCallViewModel.recipient.get())
|
||||
} else {
|
||||
AppDependencies.signalCallManager.startOutgoingAudioCall(webRtcCallViewModel.recipient.get())
|
||||
}
|
||||
|
||||
MessageSender.onMessageSent()
|
||||
}
|
||||
|
||||
private fun performCallStateUpdateChange(update: CallControlsChange) {
|
||||
viewModelScope.launch {
|
||||
internalCallScreenState.update {
|
||||
it.copy(callControlsChange = update)
|
||||
}
|
||||
|
||||
delay(1000)
|
||||
|
||||
internalCallScreenState.update {
|
||||
if (it.callControlsChange == update) {
|
||||
it.copy(callControlsChange = null)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePreJoin(event: WebRtcViewModel) {
|
||||
if (event.groupState.isNotIdle && event.ringGroup && event.areRemoteDevicesInCall()) {
|
||||
AppDependencies.signalCallManager.setRingGroup(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOutgoing(event: WebRtcViewModel) {
|
||||
val status = if (event.groupState.isNotIdle) {
|
||||
getStatusFromGroupState(event.groupState)
|
||||
} else {
|
||||
CallString.ResourceString(R.string.WebRtcCallActivity__calling)
|
||||
}
|
||||
|
||||
internalCallScreenState.update {
|
||||
it.copy(callStatus = status)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConnected(event: WebRtcViewModel) {
|
||||
if (event.groupState.isNotIdleOrConnected) {
|
||||
val status = getStatusFromGroupState(event.groupState)
|
||||
|
||||
internalCallScreenState.update {
|
||||
it.copy(callStatus = status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRinging() {
|
||||
internalCallScreenState.update {
|
||||
it.copy(callStatus = CallString.ResourceString(R.string.RedPhone_ringing))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBusy() {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel::class.java)
|
||||
internalCallScreenState.update {
|
||||
it.copy(callStatus = CallString.ResourceString(R.string.RedPhone_busy))
|
||||
}
|
||||
|
||||
internalCallScreenState.update {
|
||||
it.copy(
|
||||
hangup = CallScreenState.Hangup(
|
||||
hangupMessageType = HangupMessage.Type.BUSY,
|
||||
delay = SignalCallManager.BUSY_TONE_LENGTH.milliseconds
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGlare(recipientId: RecipientId) {
|
||||
Log.i(TAG, "handleGlare: $recipientId")
|
||||
|
||||
internalCallScreenState.update {
|
||||
it.copy(callStatus = null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReconnecting() {
|
||||
internalCallScreenState.update {
|
||||
it.copy(callStatus = CallString.ResourceString(R.string.WebRtcCallView__reconnecting))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNetworkFailure() {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel::class.java)
|
||||
|
||||
internalCallScreenState.update {
|
||||
it.copy(callStatus = CallString.ResourceString(R.string.RedPhone_network_failed))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRecipientUnavailable() {
|
||||
}
|
||||
|
||||
private fun handleCallTerminated(hangupType: HangupMessage.Type) {
|
||||
Log.i(TAG, "handleTerminate called: " + hangupType.name)
|
||||
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel::class.java)
|
||||
|
||||
internalCallScreenState.update {
|
||||
it.copy(
|
||||
callStatus = CallString.ResourceString(getStatusFromHangupType(hangupType)),
|
||||
hangup = CallScreenState.Hangup(
|
||||
hangupMessageType = hangupType
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@StringRes
|
||||
private fun getStatusFromHangupType(hangupType: HangupMessage.Type): Int {
|
||||
return when (hangupType) {
|
||||
HangupMessage.Type.NORMAL, HangupMessage.Type.NEED_PERMISSION -> R.string.RedPhone_ending_call
|
||||
HangupMessage.Type.ACCEPTED -> R.string.WebRtcCallActivity__answered_on_a_linked_device
|
||||
HangupMessage.Type.DECLINED -> R.string.WebRtcCallActivity__declined_on_a_linked_device
|
||||
HangupMessage.Type.BUSY -> R.string.WebRtcCallActivity__busy_on_a_linked_device
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatusFromGroupState(groupState: WebRtcViewModel.GroupCallState): CallString? {
|
||||
return when (groupState) {
|
||||
WebRtcViewModel.GroupCallState.DISCONNECTED -> CallString.ResourceString(R.string.WebRtcCallView__disconnected)
|
||||
WebRtcViewModel.GroupCallState.RECONNECTING -> CallString.ResourceString(R.string.WebRtcCallView__reconnecting)
|
||||
WebRtcViewModel.GroupCallState.CONNECTED_AND_PENDING -> CallString.ResourceString(R.string.WebRtcCallView__joining)
|
||||
WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING -> CallString.ResourceString(R.string.WebRtcCallView__waiting_to_be_let_in)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun onAudioDeviceSheetDisplayChanged(displayed: Boolean) {
|
||||
internalCallScreenState.update {
|
||||
it.copy(isDisplayingAudioToggleSheet = displayed)
|
||||
}
|
||||
}
|
||||
|
||||
fun onSelectedAudioDeviceChanged(audioDevice: WebRtcAudioDevice) {
|
||||
// TODO [alex] maybeDisplaySpeakerphonePopup(audioOutput);
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
AppDependencies.signalCallManager.selectAudioDevice(SignalAudioManager.ChosenAudioDeviceIdentifier(audioDevice.deviceId!!))
|
||||
} else {
|
||||
val managerDevice = when (audioDevice.webRtcAudioOutput) {
|
||||
WebRtcAudioOutput.HANDSET -> SignalAudioManager.AudioDevice.EARPIECE
|
||||
WebRtcAudioOutput.SPEAKER -> SignalAudioManager.AudioDevice.SPEAKER_PHONE
|
||||
WebRtcAudioOutput.BLUETOOTH_HEADSET -> SignalAudioManager.AudioDevice.BLUETOOTH
|
||||
WebRtcAudioOutput.WIRED_HEADSET -> SignalAudioManager.AudioDevice.WIRED_HEADSET
|
||||
}
|
||||
|
||||
AppDependencies.signalCallManager.selectAudioDevice(SignalAudioManager.ChosenAudioDeviceIdentifier(managerDevice))
|
||||
}
|
||||
}
|
||||
|
||||
fun processCallIntent(callIntent: CallIntent) {
|
||||
if (callIntent.action == CallIntent.Action.ANSWER_VIDEO) {
|
||||
enableVideoIfAvailable = true
|
||||
} else if (callIntent.action == CallIntent.Action.ANSWER_AUDIO || callIntent.isStartedFromFullScreen) {
|
||||
enableVideoIfAvailable = false
|
||||
} else {
|
||||
enableVideoIfAvailable = callIntent.shouldEnableVideoIfAvailable
|
||||
callIntent.shouldEnableVideoIfAvailable = false
|
||||
}
|
||||
|
||||
when (callIntent.action) {
|
||||
CallIntent.Action.ANSWER_AUDIO -> startCall(false)
|
||||
CallIntent.Action.ANSWER_VIDEO -> startCall(true)
|
||||
CallIntent.Action.DENY -> deny()
|
||||
CallIntent.Action.END_CALL -> hangup()
|
||||
CallIntent.Action.VIEW -> Unit
|
||||
}
|
||||
|
||||
// Prevents some issues around intent re-use when dealing with picture-in-picture.
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastProcessedIntentTimestamp > 1.seconds.inWholeMilliseconds) {
|
||||
enterPipOnResume = callIntent.shouldLaunchInPip
|
||||
}
|
||||
|
||||
lastProcessedIntentTimestamp = now
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions that require activity-level context (for example, to request permissions.)
|
||||
*/
|
||||
sealed interface Action {
|
||||
/**
|
||||
* Tries to enable local video via the normal toggle callback. Should display permissions
|
||||
* dialogs as necessary.
|
||||
*/
|
||||
data object EnableVideo : Action
|
||||
|
||||
/**
|
||||
* Display the safety number change dialog for the given untrusted identities. Since this dialog
|
||||
* is not in compose-land, we delegate this as an action instead of embedding it in the screen state.
|
||||
*/
|
||||
data class ShowGroupCallSafetyNumberChangeDialog(val untrustedIdentities: List<IdentityRecord>) : Action
|
||||
|
||||
/**
|
||||
* Immediately switch the user to speaker view
|
||||
*/
|
||||
data object SwitchToSpeaker : Action
|
||||
}
|
||||
}
|
||||
@@ -60,8 +60,6 @@ import org.thoughtcrime.securesms.components.webrtc.PendingParticipantsView
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel.SafetyNumberChangeEvent
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls
|
||||
import org.thoughtcrime.securesms.components.webrtc.WifiToCellularPopupWindow
|
||||
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoController
|
||||
@@ -487,27 +485,27 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
viewModel.setIsLandscapeEnabled(true)
|
||||
viewModel.setIsInPipMode(isInPipMode())
|
||||
viewModel.microphoneEnabled.observe(this, callScreen::setMicEnabled)
|
||||
viewModel.webRtcControls.observe(this) { controls ->
|
||||
viewModel.getWebRtcControls().observe(this) { controls ->
|
||||
callScreen.setWebRtcControls(controls)
|
||||
controlsAndInfo.updateControls(controls)
|
||||
}
|
||||
viewModel.events.observe(this, this::handleViewModelEvent)
|
||||
viewModel.getEvents().observe(this, this::handleViewModelEvent)
|
||||
|
||||
lifecycleDisposable.add(viewModel.inCallstatus.subscribe(this::handleInCallStatus))
|
||||
lifecycleDisposable.add(viewModel.recipientFlowable.subscribe(callScreen::setRecipient))
|
||||
lifecycleDisposable.add(viewModel.getInCallStatus().subscribe(this::handleInCallStatus))
|
||||
lifecycleDisposable.add(viewModel.getRecipientFlowable().subscribe(callScreen::setRecipient))
|
||||
|
||||
val isStartedFromCallLink = getCallIntent().isStartedFromCallLink
|
||||
LiveDataUtil.combineLatest(
|
||||
viewModel.callParticipantsState.toFlowable(BackpressureStrategy.LATEST).toLiveData(),
|
||||
viewModel.ephemeralState
|
||||
viewModel.getEphemeralState()
|
||||
) { state, ephemeralState ->
|
||||
CallParticipantsViewState(state, ephemeralState, orientation == Orientation.PORTRAIT_BOTTOM_EDGE, true, isStartedFromCallLink)
|
||||
}.observe(this, callScreen::updateCallParticipants)
|
||||
|
||||
viewModel.callParticipantListUpdate.observe(this, participantUpdateWindow::addCallParticipantListUpdate)
|
||||
viewModel.safetyNumberChangeEvent.observe(this, this::handleSafetyNumberChangeEvent)
|
||||
viewModel.groupMembersChanged.observe(this) { updateGroupMembersForGroupCall() }
|
||||
viewModel.groupMemberCount.observe(this, this::handleGroupMemberCountChange)
|
||||
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate)
|
||||
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent)
|
||||
viewModel.getGroupMembersChanged().observe(this) { updateGroupMembersForGroupCall() }
|
||||
viewModel.getGroupMemberCount().observe(this, this::handleGroupMemberCountChange)
|
||||
lifecycleDisposable.add(viewModel.shouldShowSpeakerHint().subscribe(this::updateSpeakerHint))
|
||||
|
||||
callScreen.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
@@ -530,7 +528,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
}
|
||||
|
||||
callScreen.setPendingParticipantsViewListener(PendingParticipantsViewListener())
|
||||
lifecycleDisposable += viewModel.pendingParticipants.subscribe(callScreen::updatePendingParticipantsList)
|
||||
lifecycleDisposable += viewModel.getPendingParticipants().subscribe(callScreen::updatePendingParticipantsList)
|
||||
}
|
||||
|
||||
private fun initializePictureInPictureParams() {
|
||||
@@ -589,7 +587,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
this
|
||||
) { _, result ->
|
||||
val action: PendingParticipantsBottomSheet.Action = PendingParticipantsBottomSheet.getAction(result)
|
||||
val recipientIds = viewModel.pendingParticipantsSnapshot.getUnresolvedPendingParticipants().map { it.recipient.id }
|
||||
val recipientIds = viewModel.getPendingParticipantsSnapshot().getUnresolvedPendingParticipants().map { it.recipient.id }
|
||||
|
||||
when (action) {
|
||||
PendingParticipantsBottomSheet.Action.NONE -> Unit
|
||||
@@ -911,7 +909,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSafetyNumberChangeEvent(safetyNumberChangeEvent: SafetyNumberChangeEvent) {
|
||||
private fun handleSafetyNumberChangeEvent(safetyNumberChangeEvent: WebRtcCallViewModel.SafetyNumberChangeEvent) {
|
||||
if (safetyNumberChangeEvent.recipientIds.isNotEmpty()) {
|
||||
if (safetyNumberChangeEvent.isInPipMode) {
|
||||
GroupCallSafetyNumberChangeNotificationUtil.showNotification(this, viewModel.recipient.get())
|
||||
@@ -1033,7 +1031,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
}
|
||||
|
||||
private fun maybeDisplaySpeakerphonePopup(nextOutput: WebRtcAudioOutput) {
|
||||
val currentOutput = viewModel.currentAudioOutput
|
||||
val currentOutput = viewModel.getCurrentAudioOutput()
|
||||
if (currentOutput == WebRtcAudioOutput.SPEAKER && nextOutput != WebRtcAudioOutput.SPEAKER) {
|
||||
callStateUpdatePopupWindow.onCallStateUpdate(CallControlsChange.SPEAKER_OFF)
|
||||
} else if (currentOutput != WebRtcAudioOutput.SPEAKER && nextOutput == WebRtcAudioOutput.SPEAKER) {
|
||||
@@ -1065,7 +1063,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
}
|
||||
|
||||
override fun onHidden() {
|
||||
val controlState = viewModel.webRtcControls.value
|
||||
val controlState = viewModel.getWebRtcControls().value
|
||||
if (controlState == null || !controlState.displayErrorControls()) {
|
||||
fullscreenHelper.hideSystemUI()
|
||||
videoTooltip?.dismiss()
|
||||
@@ -1130,7 +1128,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
}
|
||||
|
||||
override fun onAcceptCallPressed() {
|
||||
if (viewModel.isAnswerWithVideoAvailable) {
|
||||
if (viewModel.isAnswerWithVideoAvailable()) {
|
||||
handleAnswerWithVideo()
|
||||
} else {
|
||||
handleAnswerWithAudio()
|
||||
@@ -1165,7 +1163,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
}
|
||||
|
||||
override fun toggleControls() {
|
||||
val controlState = viewModel.webRtcControls.value
|
||||
val controlState = viewModel.getWebRtcControls().value
|
||||
if (controlState != null && !controlState.displayIncomingCallButtons() && !controlState.displayErrorControls()) {
|
||||
controlsAndInfo.toggleControls()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.switchMap
|
||||
import androidx.lifecycle.toPublisher
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantListUpdate
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState
|
||||
import org.thoughtcrime.securesms.components.webrtc.InCallStatus
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallRepository
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcLocalRenderState
|
||||
import org.thoughtcrime.securesms.database.GroupTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.events.CallParticipant
|
||||
import org.thoughtcrime.securesms.events.CallParticipantId
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.PendingParticipantCollection
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.PendingParticipantsState
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState
|
||||
import org.thoughtcrime.securesms.util.NetworkUtil
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||
import java.util.Collections
|
||||
|
||||
class WebRtcCallViewModel : ViewModel() {
|
||||
private val internalMicrophoneEnabled = MutableLiveData(true)
|
||||
private val isInPipMode = MutableLiveData(false)
|
||||
private val webRtcControls = MutableLiveData(WebRtcControls.NONE)
|
||||
private val foldableState = MutableLiveData(WebRtcControls.FoldableState.flat())
|
||||
private val controlsWithFoldableState: LiveData<WebRtcControls> = LiveDataUtil.combineLatest(foldableState, webRtcControls, this::updateControlsFoldableState)
|
||||
private val realWebRtcControls: LiveData<WebRtcControls> = LiveDataUtil.combineLatest(isInPipMode, controlsWithFoldableState, this::getRealWebRtcControls)
|
||||
private val events = SingleLiveEvent<CallEvent>()
|
||||
private val elapsed = BehaviorSubject.createDefault(-1L)
|
||||
private val liveRecipient = MutableLiveData(Recipient.UNKNOWN.live())
|
||||
private val participantsState = BehaviorSubject.createDefault(CallParticipantsState.STARTING_STATE)
|
||||
private val callParticipantListUpdate = SingleLiveEvent<CallParticipantListUpdate>()
|
||||
private val identityChangedRecipients = MutableLiveData<Collection<RecipientId>>(Collections.emptyList())
|
||||
private val safetyNumberChangeEvent: LiveData<SafetyNumberChangeEvent> = LiveDataUtil.combineLatest(isInPipMode, identityChangedRecipients, ::SafetyNumberChangeEvent)
|
||||
private val groupRecipient: LiveData<Recipient> = LiveDataUtil.filter(liveRecipient.switchMap(LiveRecipient::getLiveData), Recipient::isActiveGroup)
|
||||
private val groupMembers: LiveData<List<GroupMemberEntry.FullMember>> = groupRecipient.switchMap { r -> LiveGroup(r.requireGroupId()).fullMembers }
|
||||
private val groupMembersChanged: LiveData<List<GroupMemberEntry.FullMember>> = LiveDataUtil.skip(groupMembers, 1)
|
||||
private val groupMembersCount: LiveData<Int> = groupMembers.map { it.size }
|
||||
private val shouldShowSpeakerHint: Observable<Boolean> = participantsState.map(this::shouldShowSpeakerHint)
|
||||
private val isLandscapeEnabled = MutableLiveData<Boolean>()
|
||||
private val canEnterPipMode = MutableLiveData(false)
|
||||
private val groupMemberStateUpdater = Observer<List<GroupMemberEntry.FullMember>> { m -> participantsState.onNext(CallParticipantsState.update(participantsState.value!!, m)) }
|
||||
private val ephemeralState = MutableLiveData<WebRtcEphemeralState>()
|
||||
private val recipientId = BehaviorProcessor.createDefault(RecipientId.UNKNOWN)
|
||||
|
||||
private val pendingParticipants = BehaviorSubject.create<PendingParticipantCollection>()
|
||||
|
||||
private val elapsedTimeHandler = Handler(Looper.getMainLooper())
|
||||
private val elapsedTimeRunnable = Runnable { handleTick() }
|
||||
private val stopOutgoingRingingMode = Runnable { stopOutgoingRingingMode() }
|
||||
|
||||
private var canDisplayTooltipIfNeeded = true
|
||||
private var canDisplaySwitchCameraTooltipIfNeeded = true
|
||||
private var canDisplayPopupIfNeeded = true
|
||||
private var hasEnabledLocalVideo = false
|
||||
private var wasInOutgoingRingingMode = false
|
||||
private var callConnectedTime = -1L
|
||||
private var answerWithVideoAvailable = false
|
||||
private var previousParticipantList = Collections.emptyList<CallParticipant>()
|
||||
private var switchOnFirstScreenShare = true
|
||||
private var showScreenShareTip = true
|
||||
|
||||
var isCallStarting = false
|
||||
private set
|
||||
|
||||
init {
|
||||
groupMembers.observeForever(groupMemberStateUpdater)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
cancelTimer()
|
||||
groupMembers.removeObserver(groupMemberStateUpdater)
|
||||
}
|
||||
|
||||
val microphoneEnabled: LiveData<Boolean> get() = internalMicrophoneEnabled.distinctUntilChanged()
|
||||
|
||||
fun getWebRtcControls(): LiveData<WebRtcControls> = realWebRtcControls
|
||||
|
||||
val recipient: LiveRecipient get() = liveRecipient.value!!
|
||||
|
||||
fun getRecipientFlowable(): Flowable<Recipient> {
|
||||
return recipientId
|
||||
.switchMap { Recipient.observable(it).toFlowable(BackpressureStrategy.LATEST) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun setRecipient(recipient: Recipient) {
|
||||
recipientId.onNext(recipient.id)
|
||||
liveRecipient.value = recipient.live()
|
||||
}
|
||||
|
||||
fun setFoldableState(foldableState: WebRtcControls.FoldableState) {
|
||||
this.foldableState.postValue(foldableState)
|
||||
ThreadUtil.runOnMain { participantsState.onNext(CallParticipantsState.update(participantsState.value!!, foldableState)) }
|
||||
}
|
||||
|
||||
fun getEvents(): LiveData<CallEvent> {
|
||||
return events
|
||||
}
|
||||
|
||||
fun getInCallStatus(): Observable<InCallStatus> {
|
||||
val elapsedTime: Observable<Long> = elapsed.map { timeInCall -> if (callConnectedTime == -1L) -1L else timeInCall }
|
||||
|
||||
return Observable.combineLatest(
|
||||
elapsedTime,
|
||||
pendingParticipants,
|
||||
participantsState
|
||||
) { time, pendingParticipants, participantsState ->
|
||||
if (!recipient.get().isCallLink) {
|
||||
return@combineLatest InCallStatus.ElapsedTime(time)
|
||||
}
|
||||
|
||||
val pending: Set<PendingParticipantCollection.Entry> = pendingParticipants.getUnresolvedPendingParticipants()
|
||||
|
||||
if (pending.isNotEmpty()) {
|
||||
InCallStatus.PendingCallLinkUsers(pending.size)
|
||||
} else {
|
||||
InCallStatus.JoinedCallLinkUsers(participantsState.participantCount.orElse(0L).toInt())
|
||||
}
|
||||
}.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun getCallControlsState(lifecycleOwner: LifecycleOwner): Flowable<CallControlsState> {
|
||||
val groupSize: Flowable<Int> = recipientId.filter { it != RecipientId.UNKNOWN }
|
||||
.switchMap { Recipient.observable(it).toFlowable(BackpressureStrategy.LATEST) }
|
||||
.map {
|
||||
if (it.isActiveGroup) {
|
||||
SignalDatabase.groups.getGroupMemberIds(it.requireGroupId(), GroupTable.MemberSet.FULL_MEMBERS_INCLUDING_SELF).size
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
return Flowable.combineLatest(
|
||||
callParticipantsState.toFlowable(BackpressureStrategy.LATEST),
|
||||
getWebRtcControls().toPublisher(lifecycleOwner),
|
||||
groupSize,
|
||||
CallControlsState::fromViewModelData
|
||||
)
|
||||
}
|
||||
|
||||
val callParticipantsState: Observable<CallParticipantsState> get() = participantsState
|
||||
|
||||
val callParticipantsStateSnapshot: CallParticipantsState? get() = participantsState.value
|
||||
|
||||
fun getCallParticipantListUpdate(): LiveData<CallParticipantListUpdate> {
|
||||
return callParticipantListUpdate
|
||||
}
|
||||
|
||||
fun getSafetyNumberChangeEvent(): LiveData<SafetyNumberChangeEvent> {
|
||||
return safetyNumberChangeEvent
|
||||
}
|
||||
|
||||
fun getGroupMembersChanged(): LiveData<List<GroupMemberEntry.FullMember>> {
|
||||
return groupMembersChanged
|
||||
}
|
||||
|
||||
fun getGroupMemberCount(): LiveData<Int> {
|
||||
return groupMembersCount
|
||||
}
|
||||
|
||||
fun shouldShowSpeakerHint(): Observable<Boolean> {
|
||||
return shouldShowSpeakerHint.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun getCurrentAudioOutput(): WebRtcAudioOutput {
|
||||
return getWebRtcControls().value!!.audioOutput
|
||||
}
|
||||
|
||||
fun getEphemeralState(): LiveData<WebRtcEphemeralState> {
|
||||
return ephemeralState
|
||||
}
|
||||
|
||||
fun canEnterPipMode(): LiveData<Boolean> {
|
||||
return canEnterPipMode
|
||||
}
|
||||
|
||||
fun isAnswerWithVideoAvailable(): Boolean {
|
||||
return answerWithVideoAvailable
|
||||
}
|
||||
|
||||
fun getPendingParticipants(): Observable<PendingParticipantsState> {
|
||||
val isInPipMode: Observable<Boolean> = participantsState.map { it.isInPipMode }.distinctUntilChanged()
|
||||
return Observable.combineLatest(pendingParticipants, isInPipMode, ::PendingParticipantsState)
|
||||
}
|
||||
|
||||
fun getPendingParticipantsSnapshot(): PendingParticipantCollection {
|
||||
return pendingParticipants.value!!
|
||||
}
|
||||
|
||||
fun setIsInPipMode(isInPipMode: Boolean) {
|
||||
this.isInPipMode.value = isInPipMode
|
||||
participantsState.onNext(CallParticipantsState.update(participantsState.value!!, isInPipMode))
|
||||
}
|
||||
|
||||
fun setIsLandscapeEnabled(isLandscapeEnabled: Boolean) {
|
||||
this.isLandscapeEnabled.postValue(isLandscapeEnabled)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun setIsViewingFocusedParticipant(page: CallParticipantsState.SelectedPage) {
|
||||
if (page == CallParticipantsState.SelectedPage.FOCUSED) {
|
||||
SignalStore.tooltips.markGroupCallSpeakerViewSeen()
|
||||
}
|
||||
|
||||
val state = participantsState.value!!
|
||||
if (showScreenShareTip &&
|
||||
state.focusedParticipant.isScreenSharing &&
|
||||
state.isViewingFocusedParticipant &&
|
||||
page == CallParticipantsState.SelectedPage.GRID
|
||||
) {
|
||||
showScreenShareTip = false
|
||||
events.value = CallEvent.ShowSwipeToSpeakerHint
|
||||
}
|
||||
|
||||
participantsState.onNext(CallParticipantsState.update(participantsState.value!!, page))
|
||||
}
|
||||
|
||||
fun onLocalPictureInPictureClicked() {
|
||||
val state = participantsState.value!!
|
||||
participantsState.onNext(CallParticipantsState.setExpanded(participantsState.value!!, state.localRenderState != WebRtcLocalRenderState.EXPANDED))
|
||||
}
|
||||
|
||||
fun onDismissedVideoTooltip() {
|
||||
canDisplayTooltipIfNeeded = false
|
||||
}
|
||||
|
||||
fun onDismissedSwitchCameraTooltip() {
|
||||
canDisplaySwitchCameraTooltipIfNeeded = false
|
||||
SignalStore.tooltips.markCallingSwitchCameraTooltipSeen()
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun updateFromWebRtcViewModel(webRtcViewModel: WebRtcViewModel, enableVideo: Boolean) {
|
||||
canEnterPipMode.value = !webRtcViewModel.state.isPreJoinOrNetworkUnavailable
|
||||
if (isCallStarting && webRtcViewModel.state.isPassedPreJoin) {
|
||||
isCallStarting = false
|
||||
}
|
||||
|
||||
val localParticipant = webRtcViewModel.localParticipant
|
||||
|
||||
internalMicrophoneEnabled.value = localParticipant.isMicrophoneEnabled
|
||||
|
||||
val state: CallParticipantsState = participantsState.value!!
|
||||
val wasScreenSharing: Boolean = state.focusedParticipant.isScreenSharing
|
||||
val newState: CallParticipantsState = CallParticipantsState.update(state, webRtcViewModel, enableVideo)
|
||||
|
||||
participantsState.onNext(newState)
|
||||
if (switchOnFirstScreenShare && !wasScreenSharing && newState.focusedParticipant.isScreenSharing) {
|
||||
switchOnFirstScreenShare = false
|
||||
events.value = CallEvent.SwitchToSpeaker
|
||||
}
|
||||
|
||||
if (webRtcViewModel.groupState.isConnected) {
|
||||
if (!containsPlaceholders(previousParticipantList)) {
|
||||
val update = CallParticipantListUpdate.computeDeltaUpdate(previousParticipantList, webRtcViewModel.remoteParticipants)
|
||||
callParticipantListUpdate.value = update
|
||||
}
|
||||
|
||||
previousParticipantList = webRtcViewModel.remoteParticipants
|
||||
identityChangedRecipients.value = webRtcViewModel.identityChangedParticipants
|
||||
}
|
||||
|
||||
updateWebRtcControls(
|
||||
webRtcViewModel.state,
|
||||
webRtcViewModel.groupState,
|
||||
localParticipant.cameraState.isEnabled,
|
||||
webRtcViewModel.isRemoteVideoEnabled,
|
||||
webRtcViewModel.isRemoteVideoOffer,
|
||||
localParticipant.isMoreThanOneCameraAvailable,
|
||||
webRtcViewModel.hasAtLeastOneRemote,
|
||||
webRtcViewModel.activeDevice,
|
||||
webRtcViewModel.availableDevices,
|
||||
webRtcViewModel.remoteDevicesCount.orElse(0L),
|
||||
webRtcViewModel.participantLimit,
|
||||
webRtcViewModel.recipient.isCallLink,
|
||||
webRtcViewModel.remoteParticipants.size > CallParticipantsState.SMALL_GROUP_MAX
|
||||
)
|
||||
|
||||
pendingParticipants.onNext(webRtcViewModel.pendingParticipants)
|
||||
|
||||
if (newState.isInOutgoingRingingMode) {
|
||||
cancelTimer()
|
||||
if (!wasInOutgoingRingingMode) {
|
||||
elapsedTimeHandler.postDelayed(stopOutgoingRingingMode, CallParticipantsState.MAX_OUTGOING_GROUP_RING_DURATION)
|
||||
}
|
||||
wasInOutgoingRingingMode = true
|
||||
} else {
|
||||
if (webRtcViewModel.state == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1L) {
|
||||
callConnectedTime = if (wasInOutgoingRingingMode) System.currentTimeMillis() else webRtcViewModel.callConnectedTime
|
||||
startTimer()
|
||||
} else if (webRtcViewModel.state != WebRtcViewModel.State.CALL_CONNECTED || webRtcViewModel.groupState.isNotIdleOrConnected) {
|
||||
cancelTimer()
|
||||
callConnectedTime = -1L
|
||||
}
|
||||
}
|
||||
|
||||
if (webRtcViewModel.state == WebRtcViewModel.State.CALL_PRE_JOIN && webRtcViewModel.groupState.isNotIdle) {
|
||||
// Set flag
|
||||
|
||||
if (webRtcViewModel.ringGroup && webRtcViewModel.areRemoteDevicesInCall()) {
|
||||
AppDependencies.signalCallManager.setRingGroup(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (localParticipant.cameraState.isEnabled) {
|
||||
canDisplayTooltipIfNeeded = false
|
||||
hasEnabledLocalVideo = true
|
||||
events.value = CallEvent.DismissVideoTooltip
|
||||
}
|
||||
|
||||
if (canDisplayTooltipIfNeeded && webRtcViewModel.isRemoteVideoEnabled && !hasEnabledLocalVideo) {
|
||||
canDisplayTooltipIfNeeded = false
|
||||
events.value = CallEvent.ShowVideoTooltip
|
||||
}
|
||||
|
||||
if (canDisplayPopupIfNeeded && webRtcViewModel.isCellularConnection && NetworkUtil.isConnectedWifi(AppDependencies.application)) {
|
||||
canDisplayPopupIfNeeded = false
|
||||
events.value = CallEvent.ShowWifiToCellularPopup
|
||||
} else if (!webRtcViewModel.isCellularConnection) {
|
||||
canDisplayPopupIfNeeded = true
|
||||
}
|
||||
|
||||
if (SignalStore.tooltips.showCallingSwitchCameraTooltip() &&
|
||||
canDisplaySwitchCameraTooltipIfNeeded &&
|
||||
localParticipant.cameraState.isEnabled &&
|
||||
webRtcViewModel.state == WebRtcViewModel.State.CALL_CONNECTED &&
|
||||
newState.allRemoteParticipants.isNotEmpty()
|
||||
) {
|
||||
canDisplaySwitchCameraTooltipIfNeeded = false
|
||||
events.value = CallEvent.ShowSwitchCameraTooltip
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun updateFromEphemeralState(state: WebRtcEphemeralState) {
|
||||
ephemeralState.value = state
|
||||
}
|
||||
|
||||
fun startCall(isVideoCall: Boolean) {
|
||||
isCallStarting = true
|
||||
val recipient = recipient.get()
|
||||
if (recipient.isGroup) {
|
||||
WebRtcCallRepository.getIdentityRecords(recipient) { identityRecords ->
|
||||
if (identityRecords.isUntrusted(false) || identityRecords.isUnverified(false)) {
|
||||
val records = identityRecords.unverifiedRecords + identityRecords.untrustedRecords
|
||||
events.postValue(CallEvent.ShowGroupCallSafetyNumberChange(records))
|
||||
} else {
|
||||
events.postValue(CallEvent.StartCall(isVideoCall))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
events.postValue(CallEvent.StartCall(isVideoCall))
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopOutgoingRingingMode() {
|
||||
if (callConnectedTime == -1L) {
|
||||
callConnectedTime = System.currentTimeMillis()
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTick() {
|
||||
if (callConnectedTime == -1L) {
|
||||
return
|
||||
}
|
||||
|
||||
val newValue = (System.currentTimeMillis() - callConnectedTime) / 1000
|
||||
elapsed.onNext(newValue)
|
||||
elapsedTimeHandler.postDelayed(elapsedTimeRunnable, 1000)
|
||||
}
|
||||
|
||||
private fun updateWebRtcControls(
|
||||
state: WebRtcViewModel.State,
|
||||
groupState: WebRtcViewModel.GroupCallState,
|
||||
isLocalVideoEnabled: Boolean,
|
||||
isRemoteVideoEnabled: Boolean,
|
||||
isRemoteVideoOffer: Boolean,
|
||||
isMoreThanOneCameraAvailable: Boolean,
|
||||
hasAtLeastOneRemote: Boolean,
|
||||
activeDevice: SignalAudioManager.AudioDevice,
|
||||
availableDevices: Set<SignalAudioManager.AudioDevice>,
|
||||
remoteDevicesCount: Long,
|
||||
participantLimit: Long?,
|
||||
isCallLink: Boolean,
|
||||
hasParticipantOverflow: Boolean
|
||||
) {
|
||||
val callState = when (state) {
|
||||
WebRtcViewModel.State.CALL_PRE_JOIN -> WebRtcControls.CallState.PRE_JOIN
|
||||
WebRtcViewModel.State.CALL_INCOMING -> {
|
||||
answerWithVideoAvailable = isRemoteVideoOffer
|
||||
WebRtcControls.CallState.INCOMING
|
||||
}
|
||||
WebRtcViewModel.State.CALL_OUTGOING, WebRtcViewModel.State.CALL_RINGING -> WebRtcControls.CallState.OUTGOING
|
||||
WebRtcViewModel.State.CALL_BUSY, WebRtcViewModel.State.CALL_NEEDS_PERMISSION, WebRtcViewModel.State.CALL_DISCONNECTED -> WebRtcControls.CallState.ENDING
|
||||
WebRtcViewModel.State.CALL_DISCONNECTED_GLARE -> WebRtcControls.CallState.INCOMING
|
||||
WebRtcViewModel.State.CALL_RECONNECTING -> WebRtcControls.CallState.RECONNECTING
|
||||
WebRtcViewModel.State.NETWORK_FAILURE -> WebRtcControls.CallState.ERROR
|
||||
WebRtcViewModel.State.CALL_ACCEPTED_ELSEWHERE, WebRtcViewModel.State.CALL_DECLINED_ELSEWHERE, WebRtcViewModel.State.CALL_ONGOING_ELSEWHERE -> WebRtcControls.CallState.HANDLED_ELSEWHERE
|
||||
else -> WebRtcControls.CallState.ONGOING
|
||||
}
|
||||
|
||||
val groupCallState = when (groupState) {
|
||||
WebRtcViewModel.GroupCallState.DISCONNECTED -> WebRtcControls.GroupCallState.DISCONNECTED
|
||||
WebRtcViewModel.GroupCallState.CONNECTING, WebRtcViewModel.GroupCallState.RECONNECTING -> {
|
||||
if (participantLimit == null || remoteDevicesCount < participantLimit) WebRtcControls.GroupCallState.CONNECTING else WebRtcControls.GroupCallState.FULL
|
||||
}
|
||||
WebRtcViewModel.GroupCallState.CONNECTED, WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING, WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED -> WebRtcControls.GroupCallState.CONNECTED
|
||||
WebRtcViewModel.GroupCallState.CONNECTED_AND_PENDING -> WebRtcControls.GroupCallState.PENDING
|
||||
else -> WebRtcControls.GroupCallState.NONE
|
||||
}
|
||||
|
||||
webRtcControls.value = WebRtcControls(
|
||||
isLocalVideoEnabled,
|
||||
isRemoteVideoEnabled || isRemoteVideoOffer,
|
||||
isMoreThanOneCameraAvailable,
|
||||
isInPipMode.value == true,
|
||||
hasAtLeastOneRemote,
|
||||
callState,
|
||||
groupCallState,
|
||||
participantLimit,
|
||||
WebRtcControls.FoldableState.flat(),
|
||||
activeDevice,
|
||||
availableDevices,
|
||||
isCallLink,
|
||||
hasParticipantOverflow
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateControlsFoldableState(foldableState: WebRtcControls.FoldableState, controls: WebRtcControls): WebRtcControls {
|
||||
return controls.withFoldableState(foldableState)
|
||||
}
|
||||
|
||||
private fun getRealWebRtcControls(isInPipMode: Boolean, controls: WebRtcControls): WebRtcControls {
|
||||
return if (isInPipMode) WebRtcControls.PIP else controls
|
||||
}
|
||||
|
||||
private fun shouldShowSpeakerHint(state: CallParticipantsState): Boolean {
|
||||
return !state.isInPipMode &&
|
||||
state.remoteDevicesCount.orElse(0L) > 1L &&
|
||||
state.groupCallState.isConnected &&
|
||||
!SignalStore.tooltips.hasSeenGroupCallSpeakerView()
|
||||
}
|
||||
|
||||
private fun startTimer() {
|
||||
cancelTimer()
|
||||
elapsedTimeHandler.removeCallbacks(stopOutgoingRingingMode)
|
||||
elapsedTimeHandler.post(elapsedTimeRunnable)
|
||||
}
|
||||
|
||||
private fun cancelTimer() {
|
||||
return elapsedTimeHandler.removeCallbacks(elapsedTimeRunnable)
|
||||
}
|
||||
|
||||
private fun containsPlaceholders(callParticipants: List<CallParticipant>): Boolean {
|
||||
return callParticipants.any { it.callParticipantId.demuxId == CallParticipantId.DEFAULT_ID }
|
||||
}
|
||||
|
||||
class SafetyNumberChangeEvent(
|
||||
val isInPipMode: Boolean,
|
||||
val recipientIds: Collection<RecipientId>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user