mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 02:39:55 +01:00
Add Small Group Ringing support.
This commit is contained in:
committed by
Alex Hart
parent
5787a5f68a
commit
db7272730e
@@ -1,378 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.ComparatorCompat;
|
||||
import com.annimon.stream.OptionalLong;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.collections.ParticipantCollection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the state of all participants, remote and local, combined with view state
|
||||
* needed to properly render the participants. The view state primarily consists of
|
||||
* if we are in System PIP mode and if we should show our video for an outgoing call.
|
||||
*/
|
||||
public final class CallParticipantsState {
|
||||
|
||||
private static final int SMALL_GROUP_MAX = 6;
|
||||
|
||||
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
|
||||
WebRtcViewModel.GroupCallState.IDLE,
|
||||
new ParticipantCollection(SMALL_GROUP_MAX),
|
||||
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(), false),
|
||||
CallParticipant.EMPTY,
|
||||
WebRtcLocalRenderState.GONE,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
OptionalLong.empty(),
|
||||
WebRtcControls.FoldableState.flat());
|
||||
|
||||
private final WebRtcViewModel.State callState;
|
||||
private final WebRtcViewModel.GroupCallState groupCallState;
|
||||
private final ParticipantCollection remoteParticipants;
|
||||
private final CallParticipant localParticipant;
|
||||
private final CallParticipant focusedParticipant;
|
||||
private final WebRtcLocalRenderState localRenderState;
|
||||
private final boolean isInPipMode;
|
||||
private final boolean showVideoForOutgoing;
|
||||
private final boolean isViewingFocusedParticipant;
|
||||
private final OptionalLong remoteDevicesCount;
|
||||
private final WebRtcControls.FoldableState foldableState;
|
||||
|
||||
public CallParticipantsState(@NonNull WebRtcViewModel.State callState,
|
||||
@NonNull WebRtcViewModel.GroupCallState groupCallState,
|
||||
@NonNull ParticipantCollection remoteParticipants,
|
||||
@NonNull CallParticipant localParticipant,
|
||||
@NonNull CallParticipant focusedParticipant,
|
||||
@NonNull WebRtcLocalRenderState localRenderState,
|
||||
boolean isInPipMode,
|
||||
boolean showVideoForOutgoing,
|
||||
boolean isViewingFocusedParticipant,
|
||||
OptionalLong remoteDevicesCount,
|
||||
@NonNull WebRtcControls.FoldableState foldableState)
|
||||
{
|
||||
this.callState = callState;
|
||||
this.groupCallState = groupCallState;
|
||||
this.remoteParticipants = remoteParticipants;
|
||||
this.localParticipant = localParticipant;
|
||||
this.localRenderState = localRenderState;
|
||||
this.focusedParticipant = focusedParticipant;
|
||||
this.isInPipMode = isInPipMode;
|
||||
this.showVideoForOutgoing = showVideoForOutgoing;
|
||||
this.isViewingFocusedParticipant = isViewingFocusedParticipant;
|
||||
this.remoteDevicesCount = remoteDevicesCount;
|
||||
this.foldableState = foldableState;
|
||||
}
|
||||
|
||||
public @NonNull WebRtcViewModel.State getCallState() {
|
||||
return callState;
|
||||
}
|
||||
|
||||
public @NonNull WebRtcViewModel.GroupCallState getGroupCallState() {
|
||||
return groupCallState;
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getGridParticipants() {
|
||||
return remoteParticipants.getGridParticipants();
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getListParticipants() {
|
||||
List<CallParticipant> listParticipants = new ArrayList<>();
|
||||
|
||||
if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) {
|
||||
listParticipants.addAll(getAllRemoteParticipants());
|
||||
listParticipants.remove(focusedParticipant);
|
||||
} else {
|
||||
listParticipants.addAll(remoteParticipants.getListParticipants());
|
||||
}
|
||||
|
||||
if (foldableState.isFlat()) {
|
||||
listParticipants.add(CallParticipant.EMPTY);
|
||||
}
|
||||
|
||||
Collections.reverse(listParticipants);
|
||||
|
||||
return listParticipants;
|
||||
}
|
||||
|
||||
public @NonNull String getRemoteParticipantsDescription(@NonNull Context context) {
|
||||
switch (remoteParticipants.size()) {
|
||||
case 0:
|
||||
return context.getString(R.string.WebRtcCallView__no_one_else_is_here);
|
||||
case 1: {
|
||||
if (callState == WebRtcViewModel.State.CALL_PRE_JOIN && groupCallState.isNotIdle()) {
|
||||
return context.getString(remoteParticipants.get(0).isSelf() ? R.string.WebRtcCallView__s_are_in_this_call
|
||||
: R.string.WebRtcCallView__s_is_in_this_call,
|
||||
remoteParticipants.get(0).getShortRecipientDisplayName(context));
|
||||
} else {
|
||||
if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) {
|
||||
return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context));
|
||||
} else {
|
||||
return remoteParticipants.get(0).getRecipientDisplayName(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
case 2: {
|
||||
if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) {
|
||||
return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context));
|
||||
} else {
|
||||
return context.getString(R.string.WebRtcCallView__s_and_s_are_in_this_call,
|
||||
remoteParticipants.get(0).getShortRecipientDisplayName(context),
|
||||
remoteParticipants.get(1).getShortRecipientDisplayName(context));
|
||||
}
|
||||
}
|
||||
default: {
|
||||
if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) {
|
||||
return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context));
|
||||
} else {
|
||||
int others = remoteParticipants.size() - 2;
|
||||
return context.getResources().getQuantityString(R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call,
|
||||
others,
|
||||
remoteParticipants.get(0).getShortRecipientDisplayName(context),
|
||||
remoteParticipants.get(1).getShortRecipientDisplayName(context),
|
||||
others);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
|
||||
return remoteParticipants.getAllParticipants();
|
||||
}
|
||||
|
||||
public @NonNull CallParticipant getLocalParticipant() {
|
||||
return localParticipant;
|
||||
}
|
||||
|
||||
public @NonNull CallParticipant getFocusedParticipant() {
|
||||
return focusedParticipant;
|
||||
}
|
||||
|
||||
public @NonNull WebRtcLocalRenderState getLocalRenderState() {
|
||||
return localRenderState;
|
||||
}
|
||||
|
||||
public boolean isFolded() {
|
||||
return foldableState.isFolded();
|
||||
}
|
||||
|
||||
public boolean isLargeVideoGroup() {
|
||||
return getAllRemoteParticipants().size() > SMALL_GROUP_MAX;
|
||||
}
|
||||
|
||||
public boolean isInPipMode() {
|
||||
return isInPipMode;
|
||||
}
|
||||
|
||||
public boolean isViewingFocusedParticipant() {
|
||||
return isViewingFocusedParticipant;
|
||||
}
|
||||
|
||||
public boolean needsNewRequestSizes() {
|
||||
if (groupCallState.isNotIdle()) {
|
||||
return Stream.of(getAllRemoteParticipants()).anyMatch(p -> p.getVideoSink().needsNewRequestingSize());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull OptionalLong getRemoteDevicesCount() {
|
||||
return remoteDevicesCount;
|
||||
}
|
||||
|
||||
public @NonNull OptionalLong getParticipantCount() {
|
||||
boolean includeSelf = groupCallState == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
|
||||
|
||||
return remoteDevicesCount.map(l -> l + (includeSelf ? 1L : 0L))
|
||||
.or(() -> includeSelf ? OptionalLong.of(1L) : OptionalLong.empty());
|
||||
}
|
||||
|
||||
public boolean isIncomingRing() {
|
||||
return callState == WebRtcViewModel.State.CALL_INCOMING;
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState,
|
||||
@NonNull WebRtcViewModel webRtcViewModel,
|
||||
boolean enableVideo)
|
||||
{
|
||||
boolean newShowVideoForOutgoing = oldState.showVideoForOutgoing;
|
||||
if (enableVideo) {
|
||||
newShowVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
|
||||
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
|
||||
newShowVideoForOutgoing = false;
|
||||
}
|
||||
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(webRtcViewModel.getLocalParticipant(),
|
||||
oldState.isInPipMode,
|
||||
newShowVideoForOutgoing,
|
||||
webRtcViewModel.getGroupState().isNotIdle(),
|
||||
webRtcViewModel.getState(),
|
||||
webRtcViewModel.getRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
return new CallParticipantsState(webRtcViewModel.getState(),
|
||||
webRtcViewModel.getGroupState(),
|
||||
oldState.remoteParticipants.getNext(webRtcViewModel.getRemoteParticipants()),
|
||||
webRtcViewModel.getLocalParticipant(),
|
||||
getFocusedParticipant(webRtcViewModel.getRemoteParticipants()),
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
newShowVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant,
|
||||
webRtcViewModel.getRemoteDevicesCount(),
|
||||
oldState.foldableState);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, boolean isInPip) {
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
isInPip,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
oldState.focusedParticipant,
|
||||
localRenderState,
|
||||
isInPip,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.remoteDevicesCount,
|
||||
oldState.foldableState);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState setExpanded(@NonNull CallParticipantsState oldState, boolean expanded) {
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant,
|
||||
expanded);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
oldState.focusedParticipant,
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.remoteDevicesCount,
|
||||
oldState.foldableState);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) {
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
selectedPage == SelectedPage.FOCUSED,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
oldState.focusedParticipant,
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
selectedPage == SelectedPage.FOCUSED,
|
||||
oldState.remoteDevicesCount,
|
||||
oldState.foldableState);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull WebRtcControls.FoldableState foldableState) {
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
oldState.focusedParticipant,
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.remoteDevicesCount,
|
||||
foldableState);
|
||||
}
|
||||
|
||||
private static @NonNull WebRtcLocalRenderState determineLocalRenderMode(@NonNull CallParticipant localParticipant,
|
||||
boolean isInPip,
|
||||
boolean showVideoForOutgoing,
|
||||
boolean isNonIdleGroupCall,
|
||||
@NonNull WebRtcViewModel.State callState,
|
||||
int numberOfRemoteParticipants,
|
||||
boolean isViewingFocusedParticipant,
|
||||
boolean isExpanded)
|
||||
{
|
||||
boolean displayLocal = (numberOfRemoteParticipants == 0 || !isInPip) && (isNonIdleGroupCall || localParticipant.isVideoEnabled());
|
||||
WebRtcLocalRenderState localRenderState = WebRtcLocalRenderState.GONE;
|
||||
|
||||
if (isExpanded && (localParticipant.isVideoEnabled() || isNonIdleGroupCall)) {
|
||||
return WebRtcLocalRenderState.EXPANDED;
|
||||
} else if (displayLocal || showVideoForOutgoing) {
|
||||
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
|
||||
localRenderState = WebRtcLocalRenderState.SMALLER_RECTANGLE;
|
||||
} else if (numberOfRemoteParticipants == 1) {
|
||||
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
|
||||
} else {
|
||||
localRenderState = localParticipant.isVideoEnabled() ? WebRtcLocalRenderState.LARGE : WebRtcLocalRenderState.LARGE_NO_VIDEO;
|
||||
}
|
||||
} else if (callState != WebRtcViewModel.State.CALL_INCOMING && callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
|
||||
localRenderState = localParticipant.isVideoEnabled() ? WebRtcLocalRenderState.LARGE : WebRtcLocalRenderState.LARGE_NO_VIDEO;
|
||||
}
|
||||
} else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
localRenderState = WebRtcLocalRenderState.LARGE_NO_VIDEO;
|
||||
}
|
||||
|
||||
return localRenderState;
|
||||
}
|
||||
|
||||
private static @NonNull CallParticipant getFocusedParticipant(@NonNull List<CallParticipant> participants) {
|
||||
List<CallParticipant> participantsByLastSpoke = new ArrayList<>(participants);
|
||||
Collections.sort(participantsByLastSpoke, ComparatorCompat.reversed((p1, p2) -> Long.compare(p1.getLastSpoke(), p2.getLastSpoke())));
|
||||
|
||||
return participantsByLastSpoke.isEmpty() ? CallParticipant.EMPTY
|
||||
: participantsByLastSpoke.stream()
|
||||
.filter(CallParticipant::isScreenSharing)
|
||||
.findAny().orElse(participantsByLastSpoke.get(0));
|
||||
}
|
||||
|
||||
public enum SelectedPage {
|
||||
GRID,
|
||||
FOCUSED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.annimon.stream.OptionalLong
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls.FoldableState
|
||||
import org.thoughtcrime.securesms.events.CallParticipant
|
||||
import org.thoughtcrime.securesms.events.CallParticipant.Companion.createLocal
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState
|
||||
import org.thoughtcrime.securesms.service.webrtc.collections.ParticipantCollection
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Represents the state of all participants, remote and local, combined with view state
|
||||
* needed to properly render the participants. The view state primarily consists of
|
||||
* if we are in System PIP mode and if we should show our video for an outgoing call.
|
||||
*/
|
||||
data class CallParticipantsState(
|
||||
val callState: WebRtcViewModel.State = WebRtcViewModel.State.CALL_DISCONNECTED,
|
||||
val groupCallState: WebRtcViewModel.GroupCallState = WebRtcViewModel.GroupCallState.IDLE,
|
||||
private val remoteParticipants: ParticipantCollection = ParticipantCollection(SMALL_GROUP_MAX),
|
||||
val localParticipant: CallParticipant = createLocal(CameraState.UNKNOWN, BroadcastVideoSink(), false),
|
||||
val focusedParticipant: CallParticipant = CallParticipant.EMPTY,
|
||||
val localRenderState: WebRtcLocalRenderState = WebRtcLocalRenderState.GONE,
|
||||
val isInPipMode: Boolean = false,
|
||||
private val showVideoForOutgoing: Boolean = false,
|
||||
val isViewingFocusedParticipant: Boolean = false,
|
||||
val remoteDevicesCount: OptionalLong = OptionalLong.empty(),
|
||||
private val foldableState: FoldableState = FoldableState.flat(),
|
||||
val isInOutgoingRingingMode: Boolean = false,
|
||||
val ringGroup: Boolean = false,
|
||||
val ringerRecipient: Recipient = Recipient.UNKNOWN,
|
||||
val groupMembers: List<GroupMemberEntry.FullMember> = emptyList()
|
||||
) {
|
||||
|
||||
val allRemoteParticipants: List<CallParticipant> = remoteParticipants.allParticipants
|
||||
val isFolded: Boolean = foldableState.isFolded
|
||||
val isLargeVideoGroup: Boolean = allRemoteParticipants.size > SMALL_GROUP_MAX
|
||||
val isIncomingRing: Boolean = callState == WebRtcViewModel.State.CALL_INCOMING
|
||||
|
||||
val gridParticipants: List<CallParticipant>
|
||||
get() {
|
||||
return remoteParticipants.gridParticipants
|
||||
}
|
||||
|
||||
val listParticipants: List<CallParticipant>
|
||||
get() {
|
||||
val listParticipants: MutableList<CallParticipant> = mutableListOf()
|
||||
if (isViewingFocusedParticipant && allRemoteParticipants.size > 1) {
|
||||
listParticipants.addAll(allRemoteParticipants)
|
||||
listParticipants.remove(focusedParticipant)
|
||||
} else {
|
||||
listParticipants.addAll(remoteParticipants.listParticipants)
|
||||
}
|
||||
if (foldableState.isFlat) {
|
||||
listParticipants.add(CallParticipant.EMPTY)
|
||||
}
|
||||
listParticipants.reverse()
|
||||
return listParticipants
|
||||
}
|
||||
|
||||
val participantCount: OptionalLong
|
||||
get() {
|
||||
val includeSelf = groupCallState == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED
|
||||
return remoteDevicesCount.map { l: Long -> l + if (includeSelf) 1L else 0L }
|
||||
.or { if (includeSelf) OptionalLong.of(1L) else OptionalLong.empty() }
|
||||
}
|
||||
|
||||
fun getPreJoinGroupDescription(context: Context): String? {
|
||||
if (callState != WebRtcViewModel.State.CALL_PRE_JOIN || groupCallState.isIdle) {
|
||||
return null
|
||||
}
|
||||
|
||||
return if (remoteParticipants.isEmpty) {
|
||||
describeGroupMembers(
|
||||
context = context,
|
||||
oneParticipant = if (ringGroup) R.string.WebRtcCallView__signal_will_ring_s else R.string.WebRtcCallView__s_will_be_notified,
|
||||
twoParticipants = if (ringGroup) R.string.WebRtcCallView__signal_will_ring_s_and_s else R.string.WebRtcCallView__s_and_s_will_be_notified,
|
||||
multipleParticipants = if (ringGroup) R.plurals.WebRtcCallView__signal_will_ring_s_s_and_d_others else R.plurals.WebRtcCallView__s_s_and_d_others_will_be_notified,
|
||||
members = groupMembers
|
||||
)
|
||||
} else {
|
||||
when (remoteParticipants.size()) {
|
||||
0 -> context.getString(R.string.WebRtcCallView__no_one_else_is_here)
|
||||
1 -> context.getString(if (remoteParticipants[0].isSelf) R.string.WebRtcCallView__s_are_in_this_call else R.string.WebRtcCallView__s_is_in_this_call, remoteParticipants[0].getShortRecipientDisplayName(context))
|
||||
2 -> context.getString(
|
||||
R.string.WebRtcCallView__s_and_s_are_in_this_call,
|
||||
remoteParticipants[0].getShortRecipientDisplayName(context),
|
||||
remoteParticipants[1].getShortRecipientDisplayName(context)
|
||||
)
|
||||
else -> {
|
||||
val others = remoteParticipants.size() - 2
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call,
|
||||
others,
|
||||
remoteParticipants[0].getShortRecipientDisplayName(context),
|
||||
remoteParticipants[1].getShortRecipientDisplayName(context),
|
||||
others
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getOutgoingRingingGroupDescription(context: Context): String? {
|
||||
if (callState == WebRtcViewModel.State.CALL_CONNECTED &&
|
||||
groupCallState == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED &&
|
||||
isInOutgoingRingingMode
|
||||
) {
|
||||
return describeGroupMembers(
|
||||
context = context,
|
||||
oneParticipant = R.string.WebRtcCallView__ringing_s,
|
||||
twoParticipants = R.string.WebRtcCallView__ringing_s_and_s,
|
||||
multipleParticipants = R.plurals.WebRtcCallView__ringing_s_s_and_d_others,
|
||||
members = groupMembers
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun getIncomingRingingGroupDescription(context: Context): String? {
|
||||
if (callState == WebRtcViewModel.State.CALL_INCOMING &&
|
||||
groupCallState == WebRtcViewModel.GroupCallState.RINGING &&
|
||||
ringerRecipient.hasUuid()
|
||||
) {
|
||||
val ringerName = ringerRecipient.getShortDisplayName(context)
|
||||
val membersWithoutYouOrRinger: List<GroupMemberEntry.FullMember> = groupMembers.filterNot { it.member.isSelf || ringerRecipient.requireUuid() == it.member.uuid.orNull() }
|
||||
|
||||
return when (membersWithoutYouOrRinger.size) {
|
||||
0 -> context.getString(R.string.WebRtcCallView__s_is_calling_you, ringerName)
|
||||
1 -> context.getString(
|
||||
R.string.WebRtcCallView__s_is_calling_you_and_s,
|
||||
ringerName,
|
||||
membersWithoutYouOrRinger[0].member.getShortDisplayName(context)
|
||||
)
|
||||
2 -> context.getString(
|
||||
R.string.WebRtcCallView__s_is_calling_you_s_and_s,
|
||||
ringerName,
|
||||
membersWithoutYouOrRinger[0].member.getShortDisplayName(context),
|
||||
membersWithoutYouOrRinger[1].member.getShortDisplayName(context)
|
||||
)
|
||||
else -> {
|
||||
val others = membersWithoutYouOrRinger.size - 2
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.WebRtcCallView__s_is_calling_you_s_s_and_d_others,
|
||||
others,
|
||||
ringerName,
|
||||
membersWithoutYouOrRinger[0].member.getShortDisplayName(context),
|
||||
membersWithoutYouOrRinger[1].member.getShortDisplayName(context),
|
||||
others
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun needsNewRequestSizes(): Boolean {
|
||||
return if (groupCallState.isNotIdle) {
|
||||
allRemoteParticipants.any { it.videoSink.needsNewRequestingSize() }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SMALL_GROUP_MAX = 6
|
||||
|
||||
@JvmField
|
||||
val MAX_OUTGOING_GROUP_RING_DURATION = TimeUnit.MINUTES.toMillis(1)
|
||||
|
||||
@JvmField
|
||||
val STARTING_STATE = CallParticipantsState()
|
||||
|
||||
@JvmStatic
|
||||
fun update(
|
||||
oldState: CallParticipantsState,
|
||||
webRtcViewModel: WebRtcViewModel,
|
||||
enableVideo: Boolean
|
||||
): CallParticipantsState {
|
||||
|
||||
var newShowVideoForOutgoing: Boolean = oldState.showVideoForOutgoing
|
||||
if (enableVideo) {
|
||||
newShowVideoForOutgoing = webRtcViewModel.state == WebRtcViewModel.State.CALL_OUTGOING
|
||||
} else if (webRtcViewModel.state != WebRtcViewModel.State.CALL_OUTGOING) {
|
||||
newShowVideoForOutgoing = false
|
||||
}
|
||||
|
||||
val isInOutgoingRingingMode = if (oldState.isInOutgoingRingingMode) {
|
||||
webRtcViewModel.callConnectedTime + MAX_OUTGOING_GROUP_RING_DURATION > System.currentTimeMillis() && webRtcViewModel.remoteParticipants.size == 0
|
||||
} else {
|
||||
oldState.ringGroup &&
|
||||
webRtcViewModel.callConnectedTime + MAX_OUTGOING_GROUP_RING_DURATION > System.currentTimeMillis() &&
|
||||
webRtcViewModel.remoteParticipants.size == 0 &&
|
||||
oldState.callState == WebRtcViewModel.State.CALL_OUTGOING &&
|
||||
webRtcViewModel.state == WebRtcViewModel.State.CALL_CONNECTED
|
||||
}
|
||||
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(
|
||||
oldState = oldState,
|
||||
localParticipant = webRtcViewModel.localParticipant,
|
||||
showVideoForOutgoing = newShowVideoForOutgoing,
|
||||
isNonIdleGroupCall = webRtcViewModel.groupState.isNotIdle,
|
||||
callState = webRtcViewModel.state,
|
||||
numberOfRemoteParticipants = webRtcViewModel.remoteParticipants.size
|
||||
)
|
||||
|
||||
return oldState.copy(
|
||||
callState = webRtcViewModel.state,
|
||||
groupCallState = webRtcViewModel.groupState,
|
||||
remoteParticipants = oldState.remoteParticipants.getNext(webRtcViewModel.remoteParticipants),
|
||||
localParticipant = webRtcViewModel.localParticipant,
|
||||
focusedParticipant = getFocusedParticipant(webRtcViewModel.remoteParticipants),
|
||||
localRenderState = localRenderState,
|
||||
showVideoForOutgoing = newShowVideoForOutgoing,
|
||||
remoteDevicesCount = webRtcViewModel.remoteDevicesCount,
|
||||
ringGroup = webRtcViewModel.shouldRingGroup(),
|
||||
isInOutgoingRingingMode = isInOutgoingRingingMode,
|
||||
ringerRecipient = webRtcViewModel.ringerRecipient
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun update(oldState: CallParticipantsState, isInPip: Boolean): CallParticipantsState {
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState, isInPip = isInPip)
|
||||
|
||||
return oldState.copy(localRenderState = localRenderState, isInPipMode = isInPip)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setExpanded(oldState: CallParticipantsState, expanded: Boolean): CallParticipantsState {
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState, isExpanded = expanded)
|
||||
|
||||
return oldState.copy(localRenderState = localRenderState)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun update(oldState: CallParticipantsState, selectedPage: SelectedPage): CallParticipantsState {
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState, isViewingFocusedParticipant = selectedPage == SelectedPage.FOCUSED)
|
||||
|
||||
return oldState.copy(localRenderState = localRenderState, isViewingFocusedParticipant = selectedPage == SelectedPage.FOCUSED)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun update(oldState: CallParticipantsState, foldableState: FoldableState): CallParticipantsState {
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState)
|
||||
|
||||
return oldState.copy(localRenderState = localRenderState, foldableState = foldableState)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun update(oldState: CallParticipantsState, groupMembers: List<GroupMemberEntry.FullMember>): CallParticipantsState {
|
||||
return oldState.copy(groupMembers = groupMembers)
|
||||
}
|
||||
|
||||
private fun determineLocalRenderMode(
|
||||
oldState: CallParticipantsState,
|
||||
localParticipant: CallParticipant = oldState.localParticipant,
|
||||
isInPip: Boolean = oldState.isInPipMode,
|
||||
showVideoForOutgoing: Boolean = oldState.showVideoForOutgoing,
|
||||
isNonIdleGroupCall: Boolean = oldState.groupCallState.isNotIdle,
|
||||
callState: WebRtcViewModel.State = oldState.callState,
|
||||
numberOfRemoteParticipants: Int = oldState.allRemoteParticipants.size,
|
||||
isViewingFocusedParticipant: Boolean = oldState.isViewingFocusedParticipant,
|
||||
isExpanded: Boolean = oldState.localRenderState == WebRtcLocalRenderState.EXPANDED
|
||||
): WebRtcLocalRenderState {
|
||||
|
||||
val displayLocal: Boolean = (numberOfRemoteParticipants == 0 || !isInPip) && (isNonIdleGroupCall || localParticipant.isVideoEnabled)
|
||||
var localRenderState: WebRtcLocalRenderState = WebRtcLocalRenderState.GONE
|
||||
|
||||
if (isExpanded && (localParticipant.isVideoEnabled || isNonIdleGroupCall)) {
|
||||
return WebRtcLocalRenderState.EXPANDED
|
||||
} else if (displayLocal || showVideoForOutgoing) {
|
||||
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
localRenderState = if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
|
||||
WebRtcLocalRenderState.SMALLER_RECTANGLE
|
||||
} else if (numberOfRemoteParticipants == 1) {
|
||||
WebRtcLocalRenderState.SMALL_RECTANGLE
|
||||
} else {
|
||||
if (localParticipant.isVideoEnabled) WebRtcLocalRenderState.LARGE else WebRtcLocalRenderState.LARGE_NO_VIDEO
|
||||
}
|
||||
} else if (callState != WebRtcViewModel.State.CALL_INCOMING && callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
|
||||
localRenderState = if (localParticipant.isVideoEnabled) WebRtcLocalRenderState.LARGE else WebRtcLocalRenderState.LARGE_NO_VIDEO
|
||||
}
|
||||
} else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
localRenderState = WebRtcLocalRenderState.LARGE_NO_VIDEO
|
||||
}
|
||||
return localRenderState
|
||||
}
|
||||
|
||||
private fun getFocusedParticipant(participants: List<CallParticipant>): CallParticipant {
|
||||
val participantsByLastSpoke: List<CallParticipant> = participants.sortedByDescending(CallParticipant::lastSpoke)
|
||||
|
||||
return if (participantsByLastSpoke.isEmpty()) {
|
||||
CallParticipant.EMPTY
|
||||
} else {
|
||||
participantsByLastSpoke.firstOrNull(CallParticipant::isScreenSharing) ?: participantsByLastSpoke[0]
|
||||
}
|
||||
}
|
||||
|
||||
private fun describeGroupMembers(
|
||||
context: Context,
|
||||
@StringRes oneParticipant: Int,
|
||||
@StringRes twoParticipants: Int,
|
||||
@PluralsRes multipleParticipants: Int,
|
||||
members: List<GroupMemberEntry.FullMember>
|
||||
): String {
|
||||
val membersWithoutYou: List<GroupMemberEntry.FullMember> = members.filterNot { it.member.isSelf }
|
||||
|
||||
return when (membersWithoutYou.size) {
|
||||
0 -> ""
|
||||
1 -> context.getString(
|
||||
oneParticipant,
|
||||
membersWithoutYou[0].member.getShortDisplayName(context)
|
||||
)
|
||||
2 -> context.getString(
|
||||
twoParticipants,
|
||||
membersWithoutYou[0].member.getShortDisplayName(context),
|
||||
membersWithoutYou[1].member.getShortDisplayName(context)
|
||||
)
|
||||
else -> {
|
||||
val others = membersWithoutYou.size - 2
|
||||
context.resources.getQuantityString(
|
||||
multipleParticipants,
|
||||
others,
|
||||
membersWithoutYou[0].member.getShortDisplayName(context),
|
||||
membersWithoutYou[1].member.getShortDisplayName(context),
|
||||
others
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SelectedPage {
|
||||
GRID, FOCUSED
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,8 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
private ImageView answer;
|
||||
private ImageView cameraDirectionToggle;
|
||||
private TextView cameraDirectionToggleLabel;
|
||||
private AccessibleToggleButton ringToggle;
|
||||
private TextView ringToggleLabel;
|
||||
private PictureInPictureGestureHelper pictureInPictureGestureHelper;
|
||||
private ImageView hangup;
|
||||
private TextView hangupLabel;
|
||||
@@ -171,6 +173,8 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
answer = findViewById(R.id.call_screen_answer_call);
|
||||
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
|
||||
cameraDirectionToggleLabel = findViewById(R.id.call_screen_camera_direction_toggle_label);
|
||||
ringToggle = findViewById(R.id.call_screen_audio_ring_toggle);
|
||||
ringToggleLabel = findViewById(R.id.call_screen_audio_ring_toggle_label);
|
||||
hangup = findViewById(R.id.call_screen_end_call);
|
||||
hangupLabel = findViewById(R.id.call_screen_end_call_label);
|
||||
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
|
||||
@@ -239,6 +243,10 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
runIfNonNull(controlsListener, listener -> listener.onMicChanged(isOn));
|
||||
});
|
||||
|
||||
ringToggle.setOnCheckedChangeListener((v, isOn) -> {
|
||||
runIfNonNull(controlsListener, listener -> listener.onRingGroupChanged(isOn, ringToggle.isActivated()));
|
||||
});
|
||||
|
||||
cameraDirectionToggle.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCameraDirectionChanged));
|
||||
|
||||
hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed));
|
||||
@@ -358,8 +366,14 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode(), isPortrait, isLandscapeEnabled));
|
||||
}
|
||||
|
||||
if (state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN && state.getGroupCallState().isNotIdle()) {
|
||||
status.setText(state.getRemoteParticipantsDescription(getContext()));
|
||||
if (state.getGroupCallState().isNotIdle()) {
|
||||
if (state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
status.setText(state.getPreJoinGroupDescription(getContext()));
|
||||
} else if (state.getCallState() == WebRtcViewModel.State.CALL_CONNECTED && state.isInOutgoingRingingMode()) {
|
||||
status.setText(state.getOutgoingRingingGroupDescription(getContext()));
|
||||
} else if (state.getGroupCallState().isRinging()) {
|
||||
status.setText(state.getIncomingRingingGroupDescription(getContext()));
|
||||
}
|
||||
}
|
||||
|
||||
if (state.getGroupCallState().isNotIdle()) {
|
||||
@@ -641,6 +655,11 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
fullScreenShade.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayRingToggle()) {
|
||||
visibleViewSet.add(ringToggle);
|
||||
visibleViewSet.add(ringToggleLabel);
|
||||
}
|
||||
|
||||
if (webRtcControls.isFadeOutEnabled()) {
|
||||
if (!controls.isFadeOutEnabled()) {
|
||||
scheduleFadeOut();
|
||||
@@ -947,6 +966,7 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle);
|
||||
videoToggle.setBackgroundResource(R.drawable.webrtc_call_screen_video_toggle);
|
||||
audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle);
|
||||
ringToggle.setBackgroundResource(R.drawable.webrtc_call_screen_ring_toggle);
|
||||
}
|
||||
|
||||
private void updateButtonStateForSmallButtons() {
|
||||
@@ -955,6 +975,7 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle_small);
|
||||
videoToggle.setBackgroundResource(R.drawable.webrtc_call_screen_video_toggle_small);
|
||||
audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle_small);
|
||||
ringToggle.setBackgroundResource(R.drawable.webrtc_call_screen_ring_toggle_small);
|
||||
}
|
||||
|
||||
private boolean showParticipantsList() {
|
||||
@@ -968,6 +989,14 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public void setRingGroup(boolean shouldRingGroup) {
|
||||
ringToggle.setChecked(shouldRingGroup, false);
|
||||
}
|
||||
|
||||
public void enableRingGroup(boolean enabled) {
|
||||
ringToggle.setActivated(enabled);
|
||||
}
|
||||
|
||||
public interface ControlsListener {
|
||||
void onStartCall(boolean isVideoCall);
|
||||
void onCancelStartCall();
|
||||
@@ -985,5 +1014,6 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
void onShowParticipantsList();
|
||||
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
|
||||
void onLocalPictureInPictureClicked();
|
||||
void onRingGroupChanged(boolean ringGroup, boolean ringingAllowed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
@@ -29,6 +30,7 @@ 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.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
@@ -40,32 +42,38 @@ import java.util.Objects;
|
||||
|
||||
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<Event> events = new SingleLiveEvent<Event>();
|
||||
private final MutableLiveData<Long> elapsed = new MutableLiveData<>(-1L);
|
||||
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
|
||||
private final MutableLiveData<CallParticipantsState> participantsState = new MutableLiveData<>(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 = LiveDataUtil.skip(Transformations.switchMap(groupRecipient, r -> Transformations.distinctUntilChanged(new LiveGroup(r.requireGroupId()).getFullMembers())), 1);
|
||||
private final LiveData<Boolean> shouldShowSpeakerHint = Transformations.map(participantsState, this::shouldShowSpeakerHint);
|
||||
private final LiveData<Orientation> orientation;
|
||||
private final MutableLiveData<Boolean> isLandscapeEnabled = new MutableLiveData<>();
|
||||
private final LiveData<Integer> controlsRotation;
|
||||
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<Event> events = new SingleLiveEvent<>();
|
||||
private final MutableLiveData<Long> elapsed = new MutableLiveData<>(-1L);
|
||||
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
|
||||
private final DefaultValueLiveData<CallParticipantsState> participantsState = new DefaultValueLiveData<>(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 LiveData<Boolean> shouldShowSpeakerHint = Transformations.map(participantsState, 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 Handler elapsedTimeHandler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable elapsedTimeRunnable = this::handleTick;
|
||||
private final Runnable stopOutgoingRingingMode = this::stopOutgoingRingingMode;
|
||||
|
||||
private boolean canDisplayTooltipIfNeeded = true;
|
||||
private boolean hasEnabledLocalVideo = false;
|
||||
private boolean wasInOutgoingRingingMode = false;
|
||||
private long callConnectedTime = -1;
|
||||
private Handler elapsedTimeHandler = new Handler(Looper.getMainLooper());
|
||||
private boolean answerWithVideoAvailable = false;
|
||||
private Runnable elapsedTimeRunnable = this::handleTick;
|
||||
private boolean canEnterPipMode = false;
|
||||
private List<CallParticipant> previousParticipantsList = Collections.emptyList();
|
||||
private boolean callStarting = false;
|
||||
@@ -79,6 +87,8 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
controlsRotation = LiveDataUtil.combineLatest(Transformations.distinctUntilChanged(isLandscapeEnabled),
|
||||
Transformations.distinctUntilChanged(orientation),
|
||||
this::resolveRotation);
|
||||
|
||||
groupMembers.observeForever(groupMemberStateUpdater);
|
||||
}
|
||||
|
||||
public LiveData<Integer> getControlsRotation() {
|
||||
@@ -135,8 +145,12 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
return safetyNumberChangeEvent;
|
||||
}
|
||||
|
||||
public LiveData<List<GroupMemberEntry.FullMember>> getGroupMembers() {
|
||||
return groupMembers;
|
||||
public LiveData<List<GroupMemberEntry.FullMember>> getGroupMembersChanged() {
|
||||
return groupMembersChanged;
|
||||
}
|
||||
|
||||
public LiveData<Integer> getGroupMemberCount() {
|
||||
return groupMemberCount;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> shouldShowSpeakerHint() {
|
||||
@@ -159,7 +173,6 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
public void setIsInPipMode(boolean isInPipMode) {
|
||||
this.isInPipMode.setValue(isInPipMode);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), isInPipMode));
|
||||
}
|
||||
|
||||
@@ -174,11 +187,11 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
CallParticipantsState state = participantsState.getValue();
|
||||
if (state != null &&
|
||||
showScreenShareTip &&
|
||||
if (showScreenShareTip &&
|
||||
state.getFocusedParticipant().isScreenSharing() &&
|
||||
state.isViewingFocusedParticipant() &&
|
||||
page == CallParticipantsState.SelectedPage.GRID) {
|
||||
page == CallParticipantsState.SelectedPage.GRID)
|
||||
{
|
||||
showScreenShareTip = false;
|
||||
events.setValue(new Event.ShowSwipeToSpeakerHint());
|
||||
}
|
||||
@@ -211,15 +224,14 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
|
||||
microphoneEnabled.setValue(localParticipant.isMicrophoneEnabled());
|
||||
|
||||
CallParticipantsState state = participantsState.getValue();
|
||||
if (state != null) {
|
||||
boolean wasScreenSharing = state.getFocusedParticipant().isScreenSharing();
|
||||
CallParticipantsState newState = CallParticipantsState.update(state, webRtcViewModel, enableVideo);
|
||||
participantsState.setValue(newState);
|
||||
if (switchOnFirstScreenShare && !wasScreenSharing && newState.getFocusedParticipant().isScreenSharing()) {
|
||||
switchOnFirstScreenShare = false;
|
||||
events.setValue(new Event.SwitchToSpeaker());
|
||||
}
|
||||
CallParticipantsState state = participantsState.getValue();
|
||||
boolean wasScreenSharing = state.getFocusedParticipant().isScreenSharing();
|
||||
CallParticipantsState newState = CallParticipantsState.update(state, webRtcViewModel, enableVideo);
|
||||
|
||||
participantsState.setValue(newState);
|
||||
if (switchOnFirstScreenShare && !wasScreenSharing && newState.getFocusedParticipant().isScreenSharing()) {
|
||||
switchOnFirstScreenShare = false;
|
||||
events.setValue(new Event.SwitchToSpeaker());
|
||||
}
|
||||
|
||||
if (webRtcViewModel.getGroupState().isConnected()) {
|
||||
@@ -245,12 +257,20 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
webRtcViewModel.getRemoteDevicesCount().orElse(0),
|
||||
webRtcViewModel.getParticipantLimit());
|
||||
|
||||
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
|
||||
callConnectedTime = webRtcViewModel.getCallConnectedTime();
|
||||
startTimer();
|
||||
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_CONNECTED || webRtcViewModel.getGroupState().isNotIdleOrConnected()) {
|
||||
if (newState.isInOutgoingRingingMode()) {
|
||||
cancelTimer();
|
||||
callConnectedTime = -1;
|
||||
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 (localParticipant.getCameraState().isEnabled()) {
|
||||
@@ -371,18 +391,26 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
private boolean shouldShowSpeakerHint(@NonNull CallParticipantsState state) {
|
||||
return !state.isInPipMode() &&
|
||||
return !state.isInPipMode() &&
|
||||
state.getRemoteDevicesCount().orElse(0) > 1 &&
|
||||
state.getGroupCallState().isConnected() &&
|
||||
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;
|
||||
@@ -403,6 +431,7 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
cancelTimer();
|
||||
groupMembers.removeObserver(groupMemberStateUpdater);
|
||||
}
|
||||
|
||||
public void startCall(boolean isVideoCall) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.annotation.Px;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
public final class WebRtcControls {
|
||||
|
||||
@@ -183,6 +184,10 @@ public final class WebRtcControls {
|
||||
return isPreJoin() || isIncoming();
|
||||
}
|
||||
|
||||
boolean displayRingToggle() {
|
||||
return FeatureFlags.groupCallRinging() && isPreJoin() && isGroupCall() && !hasAtLeastOneRemote;
|
||||
}
|
||||
|
||||
private boolean isError() {
|
||||
return callState == CallState.ERROR;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user