Add Small Group Ringing support.

This commit is contained in:
Cody Henthorne
2021-08-24 10:18:39 -04:00
committed by Alex Hart
parent 5787a5f68a
commit db7272730e
39 changed files with 1597 additions and 609 deletions

View File

@@ -466,7 +466,7 @@ dependencies {
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.10.8'
implementation 'org.signal:ringrtc-android:2.11.1'
implementation "me.leolin:ShortcutBadger:1.1.22"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'

View File

@@ -17,6 +17,8 @@
package org.thoughtcrime.securesms;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.PictureInPictureParams;
@@ -32,6 +34,7 @@ import android.os.Bundle;
import android.util.Rational;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
@@ -69,6 +72,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
@@ -82,8 +86,6 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
private static final String TAG = Log.tag(WebRtcCallActivity.class);
@@ -290,13 +292,15 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
viewModel.getEvents().observe(this, this::handleViewModelEvent);
viewModel.getCallTime().observe(this, this::handleCallTime);
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(),
viewModel.getOrientationAndLandscapeEnabled(),
(s, o) -> new CallParticipantsViewState(s, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
.observe(this, p -> callScreen.updateCallParticipants(p));
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
viewModel.getGroupMembers().observe(this, unused -> updateGroupMembersForGroupCall());
viewModel.getGroupMembersChanged().observe(this, unused -> updateGroupMembersForGroupCall());
viewModel.getGroupMemberCount().observe(this, this::handleGroupMemberCountChange);
viewModel.shouldShowSpeakerHint().observe(this, this::updateSpeakerHint);
callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
@@ -546,6 +550,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
ApplicationDependencies.getSignalCallManager().requestUpdateGroupMembers();
}
public void handleGroupMemberCountChange(int count) {
boolean canRing = count <= FeatureFlags.maxGroupCallRingSize() && FeatureFlags.groupCallRinging();
callScreen.enableRingGroup(canRing);
ApplicationDependencies.getSignalCallManager().setRingGroup(canRing);
}
private void updateSpeakerHint(boolean showSpeakerHint) {
if (showSpeakerHint) {
callScreen.showSpeakerViewHint();
@@ -651,6 +661,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private void handleCallPreJoin(@NonNull WebRtcViewModel event) {
if (event.getGroupState().isNotIdle()) {
callScreen.setStatusFromGroupCallState(event.getGroupState());
callScreen.setRingGroup(event.shouldRingGroup());
if (event.shouldRingGroup() && event.areRemoteDevicesInCall()) {
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
}
}
}
@@ -765,6 +780,16 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
public void onLocalPictureInPictureClicked() {
viewModel.onLocalPictureInPictureClicked();
}
@Override
public void onRingGroupChanged(boolean ringGroup, boolean ringingAllowed) {
if (ringingAllowed) {
ApplicationDependencies.getSignalCallManager().setRingGroup(ringGroup);
} else {
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
Toast.makeText(WebRtcCallActivity.this, R.string.WebRtcCallActivity__group_is_too_large_to_ring_the_participants, Toast.LENGTH_SHORT).show();
}
}
}
private class WindowLayoutInfoConsumer implements Consumer<WindowLayoutInfo> {

View File

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

View File

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

View File

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

View File

@@ -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;
@@ -46,26 +48,32 @@ public class WebRtcCallViewModel extends ViewModel {
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 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 MutableLiveData<CallParticipantsState> participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE);
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 = LiveDataUtil.skip(Transformations.switchMap(groupRecipient, r -> Transformations.distinctUntilChanged(new LiveGroup(r.requireGroupId()).getFullMembers())), 1);
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());
}
@@ -212,15 +225,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());
}
}
if (webRtcViewModel.getGroupState().isConnected()) {
if (!containsPlaceholders(previousParticipantsList)) {
@@ -245,13 +257,21 @@ public class WebRtcCallViewModel extends ViewModel {
webRtcViewModel.getRemoteDevicesCount().orElse(0),
webRtcViewModel.getParticipantLimit());
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 = webRtcViewModel.getCallConnectedTime();
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()) {
canDisplayTooltipIfNeeded = false;
@@ -379,10 +399,18 @@ public class WebRtcCallViewModel extends ViewModel {
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) {

View File

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

View File

@@ -72,6 +72,7 @@ public class DatabaseFactory {
private final EmojiSearchDatabase emojiSearchDatabase;
private final MessageSendLogDatabase messageSendLogDatabase;
private final AvatarPickerDatabase avatarPickerDatabase;
private final GroupCallRingDatabase groupCallRingDatabase;
public static DatabaseFactory getInstance(Context context) {
if (instance == null) {
@@ -206,6 +207,10 @@ public class DatabaseFactory {
return getInstance(context).avatarPickerDatabase;
}
public static GroupCallRingDatabase getGroupCallRingDatabase(Context context) {
return getInstance(context).groupCallRingDatabase;
}
public static net.zetetic.database.sqlcipher.SQLiteDatabase getBackupDatabase(Context context) {
return getInstance(context).databaseHelper.getRawReadableDatabase();
}
@@ -268,6 +273,7 @@ public class DatabaseFactory {
this.emojiSearchDatabase = new EmojiSearchDatabase(context, databaseHelper);
this.messageSendLogDatabase = new MessageSendLogDatabase(context, databaseHelper);
this.avatarPickerDatabase = new AvatarPickerDatabase(context, databaseHelper);
this.groupCallRingDatabase = new GroupCallRingDatabase(context, databaseHelper);
}
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,

View File

@@ -0,0 +1,95 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import org.signal.ringrtc.CallManager
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.util.CursorUtil
import org.thoughtcrime.securesms.util.SqlUtil
import java.util.concurrent.TimeUnit
/**
* Track state of Group Call ring cancellations.
*/
class GroupCallRingDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Database(context, databaseHelper) {
companion object {
private val VALID_RING_DURATION = TimeUnit.MINUTES.toMillis(30)
private const val TABLE_NAME = "group_call_ring"
private const val ID = "_id"
private const val RING_ID = "ring_id"
private const val DATE_RECEIVED = "date_received"
private const val RING_STATE = "ring_state"
@JvmField
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$RING_ID INTEGER UNIQUE,
$DATE_RECEIVED INTEGER,
$RING_STATE INTEGER
)
""".trimIndent()
@JvmField
val CREATE_INDEXES = arrayOf(
"CREATE INDEX date_received_index on $TABLE_NAME ($DATE_RECEIVED)"
)
}
fun isCancelled(ringId: Long): Boolean {
val db = databaseHelper.signalReadableDatabase
db.query(TABLE_NAME, null, "$RING_ID = ?", SqlUtil.buildArgs(ringId), null, null, null).use { cursor ->
if (cursor.moveToFirst()) {
return CursorUtil.requireInt(cursor, RING_STATE) != 0
}
}
return false
}
fun insertGroupRing(ringId: Long, dateReceived: Long, ringState: CallManager.RingUpdate) {
val db = databaseHelper.signalWritableDatabase
val values = ContentValues().apply {
put(RING_ID, ringId)
put(DATE_RECEIVED, dateReceived)
put(RING_STATE, ringState.toCode())
}
db.insert(TABLE_NAME, null, values)
removeOldRings()
}
fun insertOrUpdateGroupRing(ringId: Long, dateReceived: Long, ringState: CallManager.RingUpdate) {
val db = databaseHelper.signalWritableDatabase
val values = ContentValues().apply {
put(RING_ID, ringId)
put(DATE_RECEIVED, dateReceived)
put(RING_STATE, ringState.toCode())
}
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE)
removeOldRings()
}
fun removeOldRings() {
val db = databaseHelper.signalWritableDatabase
db.delete(TABLE_NAME, "$DATE_RECEIVED < ?", SqlUtil.buildArgs(System.currentTimeMillis() - VALID_RING_DURATION))
}
}
private fun CallManager.RingUpdate.toCode(): Int {
return when (this) {
CallManager.RingUpdate.REQUESTED -> 0
CallManager.RingUpdate.EXPIRED_REQUEST -> 1
CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE -> 2
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE -> 3
CallManager.RingUpdate.BUSY_LOCALLY -> 4
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE -> 5
CallManager.RingUpdate.CANCELLED_BY_RINGER -> 6
}
}

View File

@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.ChatColorsDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
import org.thoughtcrime.securesms.database.GroupCallRingDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
@@ -212,8 +213,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
private static final int THREAD_CLEANUP = 112;
private static final int SESSION_MIGRATION = 113;
private static final int IDENTITY_MIGRATION = 114;
private static final int GROUP_CALL_RING_TABLE = 115;
private static final int DATABASE_VERSION = 114;
private static final int DATABASE_VERSION = 115;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -255,6 +257,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
db.execSQL(ChatColorsDatabase.CREATE_TABLE);
db.execSQL(EmojiSearchDatabase.CREATE_TABLE);
db.execSQL(AvatarPickerDatabase.CREATE_TABLE);
db.execSQL(GroupCallRingDatabase.CREATE_TABLE);
executeStatements(db, SearchDatabase.CREATE_TABLE);
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE);
executeStatements(db, MessageSendLogDatabase.CREATE_TABLE);
@@ -272,6 +275,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
executeStatements(db, MentionDatabase.CREATE_INDEXES);
executeStatements(db, PaymentDatabase.CREATE_INDEXES);
executeStatements(db, MessageSendLogDatabase.CREATE_INDEXES);
executeStatements(db, GroupCallRingDatabase.CREATE_INDEXES);
executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS);
@@ -2016,6 +2020,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
Log.d(TAG, "Identity migration took " + (System.currentTimeMillis() - start) + " ms");
}
if (oldVersion < GROUP_CALL_RING_TABLE) {
db.execSQL("CREATE TABLE group_call_ring (_id INTEGER PRIMARY KEY, ring_id INTEGER UNIQUE, date_received INTEGER, ring_state INTEGER)");
db.execSQL("CREATE INDEX date_received_index on group_call_ring (date_received)");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@@ -58,6 +58,7 @@ public class WebRtcViewModel {
public enum GroupCallState {
IDLE,
RINGING,
DISCONNECTED,
CONNECTING,
RECONNECTING,
@@ -65,6 +66,10 @@ public class WebRtcViewModel {
CONNECTED_AND_JOINING,
CONNECTED_AND_JOINED;
public boolean isIdle() {
return this == IDLE;
}
public boolean isNotIdle() {
return this != IDLE;
}
@@ -90,6 +95,10 @@ public class WebRtcViewModel {
return false;
}
public boolean isRinging() {
return this == RINGING;
}
}
private final @NonNull State state;
@@ -105,6 +114,8 @@ public class WebRtcViewModel {
private final Set<RecipientId> identityChangedRecipients;
private final OptionalLong remoteDevicesCount;
private final Long participantLimit;
private final boolean ringGroup;
private final Recipient ringerRecipient;
public WebRtcViewModel(@NonNull WebRtcServiceState state) {
this.state = state.getCallInfoState().getCallState();
@@ -117,6 +128,8 @@ public class WebRtcViewModel {
this.callConnectedTime = state.getCallInfoState().getCallConnectedTime();
this.remoteDevicesCount = state.getCallInfoState().getRemoteDevicesCount();
this.participantLimit = state.getCallInfoState().getParticipantLimit();
this.ringGroup = state.getCallSetupState().shouldRingGroup();
this.ringerRecipient = state.getCallSetupState().getRingerRecipient();
this.localParticipant = CallParticipant.createLocal(state.getLocalDeviceState().getCameraState(),
state.getVideoState().getLocalSink() != null ? state.getVideoState().getLocalSink()
: new BroadcastVideoSink(),
@@ -167,10 +180,22 @@ public class WebRtcViewModel {
return remoteDevicesCount;
}
public boolean areRemoteDevicesInCall() {
return remoteDevicesCount.isPresent() && remoteDevicesCount.getAsLong() > 0;
}
public @Nullable Long getParticipantLimit() {
return participantLimit;
}
public boolean shouldRingGroup() {
return ringGroup;
}
public @NonNull Recipient getRingerRecipient() {
return ringerRecipient;
}
@Override
public @NonNull String toString() {
return "WebRtcViewModel{" +

View File

@@ -34,6 +34,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.exceptions.InvalidUnidentifiedAccessHeaderException;
@@ -122,6 +123,22 @@ public final class GroupSendUtil {
return sendMessage(context, groupId, allTargets, false, new TypingSendOperation(message), cancelationSignal);
}
/**
* Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of
* {@link SendMessageResult}s just like we're used to.
*
* @param groupId The groupId of the group you're sending to, or null if you're sending to a collection of recipients not joined by a group.
*/
@WorkerThread
public static List<SendMessageResult> sendCallMessage(@NonNull Context context,
@Nullable GroupId.V2 groupId,
@NonNull List<Recipient> allTargets,
@NonNull SignalServiceCallMessage message)
throws IOException, UntrustedIdentityException
{
return sendMessage(context, groupId, allTargets, false, new CallSendOperation(message), null);
}
/**
* Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of
* {@link SendMessageResult}s just like we're used to.
@@ -434,6 +451,58 @@ public final class GroupSendUtil {
}
}
private static class CallSendOperation implements SendOperation {
private final SignalServiceCallMessage message;
private CallSendOperation(@NonNull SignalServiceCallMessage message) {
this.message = message;
}
@Override
public @NonNull List<SendMessageResult> sendWithSenderKey(@NonNull SignalServiceMessageSender messageSender,
@NonNull DistributionId distributionId,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> access,
boolean isRecipientUpdate)
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException
{
return messageSender.sendCallMessage(distributionId, targets, access, message);
}
@Override
public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Optional<UnidentifiedAccessPair>> access,
boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener,
@Nullable CancelationSignal cancelationSignal)
throws IOException
{
return messageSender.sendCallMessage(targets, access, message);
}
@Override
public @NonNull ContentHint getContentHint() {
return ContentHint.IMPLICIT;
}
@Override
public long getSentTimestamp() {
return message.getTimestamp().get();
}
@Override
public boolean shouldIncludeInMessageLog() {
return false;
}
@Override
public @NonNull MessageId getRelatedMessageId() {
throw new UnsupportedOperationException();
}
}
/**
* Little utility wrapper that lets us get the various different slices of recipient models that we need for different methods.
*/

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.notifications;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.NotificationManager;
import android.content.Context;
import android.database.Cursor;
@@ -28,6 +29,23 @@ public final class DoNotDisturbUtil {
}
@WorkerThread
@SuppressLint("SwitchIntDef")
public static boolean shouldDisturbUserWithCall(@NonNull Context context) {
if (Build.VERSION.SDK_INT <= 23) return true;
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
switch (notificationManager.getCurrentInterruptionFilter()) {
case NotificationManager.INTERRUPTION_FILTER_ALL:
case NotificationManager.INTERRUPTION_FILTER_UNKNOWN:
return true;
default:
return false;
}
}
@WorkerThread
@SuppressLint("SwitchIntDef")
public static boolean shouldDisturbUserWithCall(@NonNull Context context, @NonNull Recipient recipient) {
if (Build.VERSION.SDK_INT <= 23) return true;
@@ -91,6 +109,7 @@ public final class DoNotDisturbUtil {
return false;
}
//noinspection ConstantConditions
try (Cursor cursor = context.getContentResolver().query(recipient.resolve().getContactUri(), new String[]{ContactsContract.Contacts.STARRED}, null, null, null)) {
if (cursor == null || !cursor.moveToFirst()) return false;
return CursorUtil.requireInt(cursor, ContactsContract.Contacts.STARRED) == 1;

View File

@@ -10,7 +10,6 @@ import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
@@ -21,13 +20,11 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.VideoState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.VideoTrack;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -186,40 +183,37 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
}
@Override
protected @NonNull WebRtcServiceState handleReceivedOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleReceivedOpaqueMessage():");
protected @NonNull WebRtcServiceState handleSetRingGroup(@NonNull WebRtcServiceState currentState, boolean ringGroup) {
Log.i(tag, "handleReceivedOpaqueMessage(): ring: " + ringGroup);
try {
webRtcInteractor.getCallManager().receivedCallMessage(opaqueMessageMetadata.getUuid(),
opaqueMessageMetadata.getRemoteDeviceId(),
1,
opaqueMessageMetadata.getOpaque(),
opaqueMessageMetadata.getMessageAgeSeconds());
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to receive opaque message", e);
if (currentState.getCallSetupState().shouldRingGroup() == ringGroup) {
return currentState;
}
return currentState;
return currentState.builder()
.changeCallSetupState()
.setRingGroup(ringGroup)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleGroupMessageSentError(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeer,
@NonNull WebRtcViewModel.State errorCallState,
@NonNull Optional<IdentityKey> identityKey)
@NonNull Collection<RecipientId> recipientIds,
@NonNull WebRtcViewModel.State errorCallState)
{
Log.w(tag, "handleGroupMessageSentError(): error: " + errorCallState);
if (errorCallState == WebRtcViewModel.State.UNTRUSTED_IDENTITY) {
return currentState.builder()
.changeCallInfoState()
.addIdentityChangedRecipient(remotePeer.getId())
.addIdentityChangedRecipients(recipientIds)
.build();
}
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleGroupApproveSafetyNumberChange(@NonNull WebRtcServiceState currentState,
@NonNull List<RecipientId> recipientIds)
{
@@ -291,56 +285,4 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
return terminateGroupCall(currentState);
}
public @NonNull WebRtcServiceState groupCallFailure(@NonNull WebRtcServiceState currentState, @NonNull String message, @NonNull Throwable error) {
Log.w(tag, "groupCallFailure(): " + message, error);
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
Recipient recipient = currentState.getCallInfoState().getCallRecipient();
if (recipient != null && currentState.getCallInfoState().getGroupCallState().isConnected()) {
webRtcInteractor.sendGroupCallMessage(recipient, WebRtcUtil.getGroupCallEraId(groupCall));
}
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
webRtcInteractor.postStateUpdate(currentState);
try {
if (groupCall != null) {
groupCall.disconnect();
}
webRtcInteractor.getCallManager().reset();
} catch (CallException e) {
Log.w(tag, "Unable to reset call manager: ", e);
}
return terminateGroupCall(currentState);
}
public synchronized @NonNull WebRtcServiceState terminateGroupCall(@NonNull WebRtcServiceState currentState) {
return terminateGroupCall(currentState, true);
}
public synchronized @NonNull WebRtcServiceState terminateGroupCall(@NonNull WebRtcServiceState currentState, boolean terminateVideo) {
webRtcInteractor.updatePhoneState(LockManager.PhoneState.PROCESSING);
boolean playDisconnectSound = currentState.getCallInfoState().getCallState() == WebRtcViewModel.State.CALL_DISCONNECTED;
webRtcInteractor.stopAudio(playDisconnectSound);
webRtcInteractor.setWantsBluetoothConnection(false);
webRtcInteractor.stopForegroundService();
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
if (terminateVideo) {
WebRtcVideoUtil.deinitializeVideo(currentState);
}
GroupCallSafetyNumberChangeNotificationUtil.cancelNotification(context, currentState.getCallInfoState().getCallRecipient());
return new WebRtcServiceState(new IdleActionProcessor(webRtcInteractor));
}
}

View File

@@ -0,0 +1,14 @@
package org.thoughtcrime.securesms.service.webrtc
import org.signal.ringrtc.CallManager
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.RecipientId
import java.util.UUID
data class GroupCallRingCheckInfo(
val recipientId: RecipientId,
val groupId: GroupId.V2,
val ringId: Long,
val ringerUuid: UUID,
val ringUpdate: CallManager.RingUpdate
)

View File

@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.service.webrtc;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_ESTABLISHED;
import android.os.ResultReceiver;
import androidx.annotation.NonNull;
@@ -12,11 +14,10 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_ESTABLISHED;
/**
* Process actions to go from lobby to a joined call.
*/
@@ -81,6 +82,14 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
throw new RuntimeException(e);
}
if (FeatureFlags.groupCallRinging() && currentState.getCallSetupState().shouldRingGroup()) {
try {
groupCall.ringAll();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to ring group", e);
}
}
builder.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_CONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED)
@@ -89,8 +98,7 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
.changeLocalDeviceState()
.wantsBluetooth(true)
.commit()
.actionProcessor(new GroupConnectedActionProcessor(webRtcInteractor))
.build();
.actionProcessor(new GroupConnectedActionProcessor(webRtcInteractor));
} else if (device.getJoinState() == GroupCall.JoinState.JOINING) {
builder.changeCallInfoState()
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING)

View File

@@ -3,15 +3,17 @@ package org.thoughtcrime.securesms.service.webrtc;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.webrtc.CapturerObserver;
import org.webrtc.VideoFrame;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.UUID;
/**
* Action handler for when the system is at rest. Mainly responsible
* for starting pre-call state, starting an outgoing call, or receiving an
@@ -66,4 +68,59 @@ public class IdleActionProcessor extends WebRtcActionProcessor {
return isGroupCall ? currentState.getActionProcessor().handlePreJoinCall(currentState, remotePeer)
: currentState;
}
@Override
protected @NonNull WebRtcServiceState handleGroupCallRingUpdate(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeerGroup,
@NonNull GroupId.V2 groupId,
long ringId,
@NonNull UUID uuid,
@NonNull CallManager.RingUpdate ringUpdate)
{
Log.i(TAG, "handleGroupCallRingUpdate(): recipient: " + remotePeerGroup.getId() + " ring: " + ringId + " update: " + ringUpdate);
if (ringUpdate != CallManager.RingUpdate.REQUESTED) {
DatabaseFactory.getGroupCallRingDatabase(context).insertOrUpdateGroupRing(ringId, System.currentTimeMillis(), ringUpdate);
return currentState;
} else if (DatabaseFactory.getGroupCallRingDatabase(context).isCancelled(ringId)) {
try {
Log.i(TAG, "Incoming ring request for already cancelled ring: " + ringId);
webRtcInteractor.getCallManager().cancelGroupRing(groupId.getDecodedId(), ringId, null);
} catch (CallException e) {
Log.w(TAG, "Error while trying to cancel ring: " + ringId, e);
}
return currentState;
}
webRtcInteractor.peekGroupCallForRingingCheck(new GroupCallRingCheckInfo(remotePeerGroup.getId(), groupId, ringId, uuid, ringUpdate));
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleReceivedGroupCallPeekForRingingCheck(@NonNull WebRtcServiceState currentState, @NonNull GroupCallRingCheckInfo info, long deviceCount) {
Log.i(tag, "handleReceivedGroupCallPeekForRingingCheck(): recipient: " + info.getRecipientId() + " ring: " + info.getRingId() + " deviceCount: " + deviceCount);
if (DatabaseFactory.getGroupCallRingDatabase(context).isCancelled(info.getRingId())) {
try {
Log.i(TAG, "Ring was cancelled while getting peek info ring: " + info.getRingId());
webRtcInteractor.getCallManager().cancelGroupRing(info.getGroupId().getDecodedId(), info.getRingId(), null);
} catch (CallException e) {
Log.w(TAG, "Error while trying to cancel ring: " + info.getRingId(), e);
}
return currentState;
}
if (deviceCount == 0) {
Log.i(TAG, "No one in the group call, mark as expired and do not ring");
DatabaseFactory.getGroupCallRingDatabase(context).insertOrUpdateGroupRing(info.getRingId(), System.currentTimeMillis(), CallManager.RingUpdate.EXPIRED_REQUEST);
return currentState;
}
currentState = currentState.builder()
.actionProcessor(new IncomingGroupCallActionProcessor(webRtcInteractor))
.build();
return currentState.getActionProcessor().handleGroupCallRingUpdate(currentState, new RemotePeer(info.getRecipientId()), info.getGroupId(), info.getRingId(), info.getRingerUuid(), info.getRingUpdate());
}
}

View File

@@ -0,0 +1,258 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.media.AudioManager;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.notifications.DoNotDisturbUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.UUID;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_INCOMING_CONNECTING;
import static org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.TYPE_INCOMING_RINGING;
/**
* Process actions to go from incoming "ringing" group call to joining. By the time this processor
* is running, the group call to ring has been verified to have at least one active device.
*/
public final class IncomingGroupCallActionProcessor extends DeviceAwareActionProcessor {
private static final String TAG = Log.tag(IncomingGroupCallActionProcessor.class);
public IncomingGroupCallActionProcessor(WebRtcInteractor webRtcInteractor) {
super(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handleGroupCallRingUpdate(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeerGroup,
@NonNull GroupId.V2 groupId,
long ringId,
@NonNull UUID uuid,
@NonNull CallManager.RingUpdate ringUpdate)
{
Log.i(TAG, "handleGroupCallRingUpdate(): recipient: " + remotePeerGroup.getId() + " ring: " + ringId + " update: " + ringUpdate);
Recipient recipient = remotePeerGroup.getRecipient();
boolean updateForCurrentRingId = ringId == currentState.getCallSetupState().getRingId();
boolean isCurrentlyRinging = currentState.getCallInfoState().getGroupCallState().isRinging();
if (DatabaseFactory.getGroupCallRingDatabase(context).isCancelled(ringId)) {
try {
Log.i(TAG, "Ignoring incoming ring request for already cancelled ring: " + ringId);
webRtcInteractor.getCallManager().cancelGroupRing(groupId.getDecodedId(), ringId, null);
} catch (CallException e) {
Log.w(TAG, "Error while trying to cancel ring: " + ringId, e);
}
return currentState;
}
if (ringUpdate != CallManager.RingUpdate.REQUESTED) {
DatabaseFactory.getGroupCallRingDatabase(context).insertOrUpdateGroupRing(ringId, System.currentTimeMillis(), ringUpdate);
if (updateForCurrentRingId && isCurrentlyRinging) {
Log.i(TAG, "Cancelling current ring: " + ringId);
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.build();
webRtcInteractor.postStateUpdate(currentState);
return terminateGroupCall(currentState);
} else {
return currentState;
}
}
if (!updateForCurrentRingId && isCurrentlyRinging) {
try {
Log.i(TAG, "Already ringing so reply busy for new ring: " + ringId);
webRtcInteractor.getCallManager().cancelGroupRing(groupId.getDecodedId(), ringId, CallManager.RingCancelReason.Busy);
} catch (CallException e) {
Log.w(TAG, "Error while trying to cancel ring: " + ringId, e);
}
return currentState;
}
if (updateForCurrentRingId) {
Log.i(TAG, "Already ringing for ring: " + ringId);
return currentState;
}
Log.i(TAG, "Requesting new ring: " + ringId);
DatabaseFactory.getGroupCallRingDatabase(context).insertGroupRing(ringId, System.currentTimeMillis(), ringUpdate);
currentState = WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState);
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
androidAudioManager.setSpeakerphoneOn(false);
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_RINGING, remotePeerGroup);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.INTERACTIVE);
boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(context.getApplicationContext());
if (shouldDisturbUserWithCall) {
boolean started = webRtcInteractor.startWebRtcCallActivityIfPossible();
if (!started) {
Log.i(TAG, "Unable to start call activity due to OS version or not being in the foreground");
ApplicationDependencies.getAppForegroundObserver().addListener(webRtcInteractor.getForegroundListener());
}
}
webRtcInteractor.initializeAudioForCall();
if (shouldDisturbUserWithCall && SignalStore.settings().isCallNotificationsEnabled()) {
Uri ringtone = recipient.resolve().getCallRingtone();
RecipientDatabase.VibrateState vibrateState = recipient.resolve().getCallVibrate();
if (ringtone == null) {
ringtone = SignalStore.settings().getCallRingtone();
}
webRtcInteractor.startIncomingRinger(ringtone, vibrateState == RecipientDatabase.VibrateState.ENABLED || (vibrateState == RecipientDatabase.VibrateState.DEFAULT && SignalStore.settings().isCallVibrateEnabled()));
}
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_RINGING, remotePeerGroup);
webRtcInteractor.registerPowerButtonReceiver();
return currentState.builder()
.changeCallSetupState()
.isRemoteVideoOffer(true)
.ringId(ringId)
.ringerRecipient(Recipient.externalPush(context, uuid, null, false))
.commit()
.changeCallInfoState()
.callRecipient(remotePeerGroup.getRecipient())
.callState(WebRtcViewModel.State.CALL_INCOMING)
.groupCallState(WebRtcViewModel.GroupCallState.RINGING)
.putParticipant(remotePeerGroup.getRecipient(),
CallParticipant.createRemote(new CallParticipantId(remotePeerGroup.getRecipient()),
remotePeerGroup.getRecipient(),
null,
new BroadcastVideoSink(currentState.getVideoState().getLockableEglBase(),
false,
true,
currentState.getLocalDeviceState().getOrientation().getDegrees()),
true,
false,
0,
true,
0,
false,
CallParticipant.DeviceOrdinal.PRIMARY
))
.build();
}
@Override
protected @NonNull WebRtcServiceState handleAcceptCall(@NonNull WebRtcServiceState currentState, boolean answerWithVideo) {
byte[] groupId = currentState.getCallInfoState().getCallRecipient().requireGroupId().getDecodedId();
GroupCall groupCall = webRtcInteractor.getCallManager().createGroupCall(groupId,
SignalStore.internalValues().groupCallingServer(),
currentState.getVideoState().getLockableEglBase().require(),
webRtcInteractor.getGroupCallObserver());
try {
groupCall.setOutgoingAudioMuted(true);
groupCall.setOutgoingVideoMuted(true);
groupCall.setBandwidthMode(NetworkUtil.getCallingBandwidthMode(context));
Log.i(TAG, "Connecting to group call: " + currentState.getCallInfoState().getCallRecipient().getId());
groupCall.connect();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to connect to group call", e);
}
currentState = currentState.builder()
.changeCallInfoState()
.groupCall(groupCall)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.commit()
.changeCallSetupState()
.enableVideoOnCreate(answerWithVideo)
.build();
AudioManager androidAudioManager = ServiceUtil.getAudioManager(context);
androidAudioManager.setSpeakerphoneOn(false);
webRtcInteractor.updatePhoneState(WebRtcUtil.getInCallPhoneState(context));
webRtcInteractor.initializeAudioForCall();
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_CONNECTING, currentState.getCallInfoState().getCallRecipient());
webRtcInteractor.setWantsBluetoothConnection(true);
try {
groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera());
groupCall.setOutgoingVideoMuted(answerWithVideo);
groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled());
groupCall.setBandwidthMode(NetworkUtil.getCallingBandwidthMode(context));
groupCall.join();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to join group call", e);
}
return currentState.builder()
.actionProcessor(new GroupJoiningActionProcessor(webRtcInteractor))
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_OUTGOING)
.groupCallState(WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING)
.commit()
.changeLocalDeviceState()
.wantsBluetooth(true)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleDenyCall(@NonNull WebRtcServiceState currentState) {
Recipient recipient = currentState.getCallInfoState().getCallRecipient();
Optional<GroupId> groupId = recipient.getGroupId();
long ringId = currentState.getCallSetupState().getRingId();
DatabaseFactory.getGroupCallRingDatabase(context).insertOrUpdateGroupRing(ringId,
System.currentTimeMillis(),
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE);
try {
webRtcInteractor.getCallManager().cancelGroupRing(groupId.get().getDecodedId(),
ringId,
CallManager.RingCancelReason.DeclinedByUser);
} catch (CallException e) {
Log.w(TAG, "Error while trying to cancel ring " + ringId, e);
}
webRtcInteractor.updatePhoneState(LockManager.PhoneState.PROCESSING);
webRtcInteractor.stopAudio(false);
webRtcInteractor.setWantsBluetoothConnection(false);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
webRtcInteractor.stopForegroundService();
return WebRtcVideoUtil.deinitializeVideo(currentState)
.builder()
.actionProcessor(new IdleActionProcessor(webRtcInteractor))
.terminate()
.build();
}
}

View File

@@ -1,5 +1,11 @@
package org.thoughtcrime.securesms.service.webrtc;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.GroupCallState.IDLE;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.CALL_INCOMING;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NETWORK_FAILURE;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NO_SUCH_USER;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.UNTRUSTED_IDENTITY;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
@@ -21,10 +27,13 @@ import org.signal.ringrtc.GroupCall;
import org.signal.ringrtc.HttpHeader;
import org.signal.ringrtc.Remote;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.groups.GroupIdentifier;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.GroupCallPeekEvent;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
@@ -33,6 +42,7 @@ import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.jobs.GroupCallUpdateSendJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.messages.GroupSendUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
@@ -42,7 +52,9 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.BubbleUtil;
import org.thoughtcrime.securesms.util.RecipientAccessList;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.PeerConnection;
@@ -51,6 +63,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.calls.CallingResponse;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.OpaqueMessage;
@@ -63,16 +76,12 @@ import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.GroupCallState.IDLE;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.CALL_INCOMING;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NETWORK_FAILURE;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NO_SUCH_USER;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.UNTRUSTED_IDENTITY;
import java.util.stream.Collectors;
/**
* Entry point for all things calling. Lives for the life of the app instance and will spin up a foreground service when needed to
@@ -94,6 +103,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
private final LockManager lockManager;
private WebRtcServiceState serviceState;
private boolean needsToSetSelfUuid = true;
public SignalCallManager(@NonNull Application application) {
this.context = application.getApplicationContext();
@@ -136,6 +146,15 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
}
serviceExecutor.execute(() -> {
if (needsToSetSelfUuid) {
try {
callManager.setSelfUuid(Recipient.self().requireUuid());
needsToSetSelfUuid = false;
} catch (CallException e) {
Log.w(TAG, "Unable to set self UUID on CallManager", e);
}
}
Log.v(TAG, "Processing action, handler: " + serviceState.getActionProcessor().getTag());
WebRtcServiceState previous = serviceState;
serviceState = action.process(previous, previous.getActionProcessor());
@@ -270,6 +289,14 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
process((s, p) -> p.handleReceivedOpaqueMessage(s, opaqueMessageMetadata));
}
public void setRingGroup(boolean ringGroup) {
process((s, p) -> p.handleSetRingGroup(s, ringGroup));
}
private void receivedGroupCallPeekForRingingCheck(@NonNull GroupCallRingCheckInfo groupCallRingCheckInfo, long deviceCount) {
process((s, p) -> p.handleReceivedGroupCallPeekForRingingCheck(s, groupCallRingCheckInfo, deviceCount));
}
public void peekGroupCall(@NonNull RecipientId id) {
if (callManager == null) {
Log.i(TAG, "Unable to peekGroupCall, call manager is null");
@@ -307,6 +334,33 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
});
}
public void peekGroupCallForRingingCheck(@NonNull GroupCallRingCheckInfo info) {
if (callManager == null) {
Log.i(TAG, "Unable to peekGroupCall, call manager is null");
return;
}
networkExecutor.execute(() -> {
try {
Recipient group = Recipient.resolved(info.getRecipientId());
GroupId.V2 groupId = group.requireGroupId().requireV2();
GroupExternalCredential credential = GroupManager.getGroupExternalCredential(context, groupId);
List<GroupCall.GroupMemberInfo> members = GroupManager.getUuidCipherTexts(context, groupId)
.entrySet()
.stream()
.map(entry -> new GroupCall.GroupMemberInfo(entry.getKey(), entry.getValue().serialize()))
.collect(Collectors.toList());
callManager.peekGroupCall(SignalStore.internalValues().groupCallingServer(), credential.getTokenBytes().toByteArray(), members, peekInfo -> {
receivedGroupCallPeekForRingingCheck(info, peekInfo.getDeviceCount());
});
} catch (IOException | VerificationFailedException | CallException e) {
Log.e(TAG, "error peeking for ringing check", e);
}
});
}
public boolean startCallCardActivityIfPossible() {
if (Build.VERSION.SDK_INT >= 29 && !ApplicationDependencies.getAppForegroundObserver().isForegrounded()) {
return false;
@@ -535,7 +589,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
}
@Override
public void onSendCallMessage(@NonNull final UUID uuid, @NonNull final byte[] bytes) {
public void onSendCallMessage(@NonNull UUID uuid, @NonNull byte[] bytes, @NonNull CallManager.CallMessageUrgency unused) {
Log.i(TAG, "onSendCallMessage():");
OpaqueMessage opaqueMessage = new OpaqueMessage(bytes);
@@ -553,10 +607,49 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
} catch (UntrustedIdentityException e) {
Log.i(TAG, "sendOpaqueCallMessage onFailure: ", e);
RetrieveProfileJob.enqueue(recipient.getId());
process((s, p) -> p.handleGroupMessageSentError(s, new RemotePeer(recipient.getId()), UNTRUSTED_IDENTITY, Optional.fromNullable(e.getIdentityKey())));
process((s, p) -> p.handleGroupMessageSentError(s, Collections.singletonList(recipient.getId()), UNTRUSTED_IDENTITY));
} catch (IOException e) {
Log.i(TAG, "sendOpaqueCallMessage onFailure: ", e);
process((s, p) -> p.handleGroupMessageSentError(s, new RemotePeer(recipient.getId()), NETWORK_FAILURE, Optional.absent()));
process((s, p) -> p.handleGroupMessageSentError(s, Collections.singletonList(recipient.getId()), NETWORK_FAILURE));
}
});
}
@Override
public void onSendCallMessageToGroup(@NonNull byte[] groupIdBytes, @NonNull byte[] message, @NonNull CallManager.CallMessageUrgency unused) {
Log.i(TAG, "onSendCallMessageToGroup():");
networkExecutor.execute(() -> {
try {
GroupId groupId = GroupId.v2(new GroupIdentifier(groupIdBytes));
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
recipients = RecipientUtil.getEligibleForSending((recipients.stream()
.map(Recipient::resolve)
.filter(r -> !r.isBlocked())
.collect(Collectors.toList())));
OpaqueMessage opaqueMessage = new OpaqueMessage(message);
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forOutgoingGroupOpaque(groupId.getDecodedId(), System.currentTimeMillis(), opaqueMessage, true, null);
RecipientAccessList accessList = new RecipientAccessList(recipients);
List<SendMessageResult> results = GroupSendUtil.sendCallMessage(context,
groupId.requireV2(),
recipients,
callMessage);
Set<RecipientId> identifyFailureRecipientIds = results.stream()
.filter(result -> result.getIdentityFailure() != null)
.map(result -> accessList.requireIdByAddress(result.getAddress()))
.collect(Collectors.toSet());
if (Util.hasItems(identifyFailureRecipientIds)) {
process((s, p) -> p.handleGroupMessageSentError(s, identifyFailureRecipientIds, UNTRUSTED_IDENTITY));
RetrieveProfileJob.enqueue(identifyFailureRecipientIds);
}
} catch (UntrustedIdentityException | IOException | InvalidInputException e) {
Log.w(TAG, "onSendCallMessageToGroup failed", e);
}
});
}
@@ -594,6 +687,22 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
});
}
@Override
public void onGroupCallRingUpdate(@NonNull byte[] groupIdBytes, long ringId, @NonNull UUID uuid, @NonNull CallManager.RingUpdate ringUpdate) {
try {
GroupId.V2 groupId = GroupId.v2(new GroupIdentifier(groupIdBytes));
Optional<GroupDatabase.GroupRecord> group = DatabaseFactory.getGroupDatabase(context).getGroup(groupId);
if (group.isPresent()) {
process((s, p) -> p.handleGroupCallRingUpdate(s, new RemotePeer(group.get().getRecipientId()), groupId, ringId, uuid, ringUpdate));
} else {
Log.w(TAG, "Unable to ring unknown group.");
}
} catch (InvalidInputException e) {
Log.w(TAG, "Unable to ring group due to invalid group id", e);
}
}
@Override
public void requestMembershipProof(@NonNull final GroupCall groupCall) {
Log.i(TAG, "requestMembershipProof():");
@@ -663,7 +772,9 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
public void onForeground() {
process((s, p) -> {
WebRtcViewModel.State callState = s.getCallInfoState().getCallState();
if (callState == CALL_INCOMING && s.getCallInfoState().getGroupCallState() == IDLE) {
WebRtcViewModel.GroupCallState groupCallState = s.getCallInfoState().getGroupCallState();
if (callState == CALL_INCOMING && (groupCallState == IDLE || groupCallState.isRinging())) {
startCallCardActivityIfPossible();
}
ApplicationDependencies.getAppForegroundObserver().removeListener(this);

View File

@@ -1,5 +1,9 @@
package org.thoughtcrime.securesms.service.webrtc;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.AnswerMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.HangupMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedAnswerMetadata;
import android.content.Context;
import android.os.ResultReceiver;
@@ -10,14 +14,18 @@ import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallId;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.CallManager.RingUpdate;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.sensors.Orientation;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.ringrtc.CallState;
@@ -41,13 +49,10 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.AnswerMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.HangupMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.OpaqueMessageMetadata;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedAnswerMetadata;
import java.util.UUID;
/**
* Base WebRTC action processor and core of the calling state machine. As actions (as intents)
@@ -616,9 +621,8 @@ public abstract class WebRtcActionProcessor {
}
protected @NonNull WebRtcServiceState handleGroupMessageSentError(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeer,
@NonNull WebRtcViewModel.State errorCallState,
@NonNull Optional<IdentityKey> identityKey)
@NonNull Collection<RecipientId> recipientIds,
@NonNull WebRtcViewModel.State errorCallState)
{
Log.i(tag, "handleGroupMessageSentError not processed");
return currentState;
@@ -631,11 +635,106 @@ public abstract class WebRtcActionProcessor {
return currentState;
}
//endregion
protected @NonNull WebRtcServiceState handleReceivedOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleReceivedOpaqueMessage():");
protected @NonNull WebRtcServiceState handleReceivedOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleReceivedOpaqueMessage not processed");
try {
webRtcInteractor.getCallManager().receivedCallMessage(opaqueMessageMetadata.getUuid(),
opaqueMessageMetadata.getRemoteDeviceId(),
1,
opaqueMessageMetadata.getOpaque(),
opaqueMessageMetadata.getMessageAgeSeconds());
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to receive opaque message", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleGroupCallRingUpdate(@NonNull WebRtcServiceState currentState,
@NonNull RemotePeer remotePeerGroup,
@NonNull GroupId.V2 groupId,
long ringId,
@NonNull UUID uuid,
@NonNull RingUpdate ringUpdate)
{
Log.i(tag, "handleGroupCallRingUpdate(): recipient: " + remotePeerGroup.getId() + " ring: " + ringId + " update: " + ringUpdate);
try {
if (ringUpdate != RingUpdate.BUSY_LOCALLY && ringUpdate != RingUpdate.BUSY_ON_ANOTHER_DEVICE) {
webRtcInteractor.getCallManager().cancelGroupRing(groupId.getDecodedId(), ringId, CallManager.RingCancelReason.Busy);
}
DatabaseFactory.getGroupCallRingDatabase(context).insertOrUpdateGroupRing(ringId,
System.currentTimeMillis(),
ringUpdate == RingUpdate.REQUESTED ? RingUpdate.BUSY_LOCALLY : ringUpdate);
} catch (CallException e) {
Log.w(tag, "Unable to cancel ring", e);
}
return currentState;
}
protected @NonNull WebRtcServiceState handleSetRingGroup(@NonNull WebRtcServiceState currentState, boolean ringGroup) {
Log.i(tag, "handleSetRingGroup not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleReceivedGroupCallPeekForRingingCheck(@NonNull WebRtcServiceState currentState, @NonNull GroupCallRingCheckInfo info, long deviceCount) {
Log.i(tag, "handleReceivedGroupCallPeekForRingingCheck not processed");
return currentState;
}
protected @NonNull WebRtcServiceState groupCallFailure(@NonNull WebRtcServiceState currentState, @NonNull String message, @NonNull Throwable error) {
Log.w(tag, "groupCallFailure(): " + message, error);
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
Recipient recipient = currentState.getCallInfoState().getCallRecipient();
if (recipient != null && currentState.getCallInfoState().getGroupCallState().isConnected()) {
webRtcInteractor.sendGroupCallMessage(recipient, WebRtcUtil.getGroupCallEraId(groupCall));
}
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
webRtcInteractor.postStateUpdate(currentState);
try {
if (groupCall != null) {
groupCall.disconnect();
}
webRtcInteractor.getCallManager().reset();
} catch (CallException e) {
Log.w(tag, "Unable to reset call manager: ", e);
}
return terminateGroupCall(currentState);
}
protected synchronized @NonNull WebRtcServiceState terminateGroupCall(@NonNull WebRtcServiceState currentState) {
return terminateGroupCall(currentState, true);
}
protected synchronized @NonNull WebRtcServiceState terminateGroupCall(@NonNull WebRtcServiceState currentState, boolean terminateVideo) {
webRtcInteractor.updatePhoneState(LockManager.PhoneState.PROCESSING);
boolean playDisconnectSound = currentState.getCallInfoState().getCallState() == WebRtcViewModel.State.CALL_DISCONNECTED;
webRtcInteractor.stopAudio(playDisconnectSound);
webRtcInteractor.setWantsBluetoothConnection(false);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.IDLE);
webRtcInteractor.stopForegroundService();
if (terminateVideo) {
WebRtcVideoUtil.deinitializeVideo(currentState);
}
GroupCallSafetyNumberChangeNotificationUtil.cancelNotification(context, currentState.getCallInfoState().getCallRecipient());
return new WebRtcServiceState(new IdleActionProcessor(webRtcInteractor));
}
//endregion
}

View File

@@ -154,7 +154,7 @@ public class WebRtcInteractor {
audioManager.startCommunication(preserveSpeakerphone);
}
void peekGroupCall(@NonNull RecipientId recipientId) {
signalCallManager.peekGroupCall(recipientId);
void peekGroupCallForRingingCheck(@NonNull GroupCallRingCheckInfo groupCallRingCheckInfo) {
signalCallManager.peekGroupCallForRingingCheck(groupCallRingCheckInfo);
}
}

View File

@@ -1,44 +0,0 @@
package org.thoughtcrime.securesms.service.webrtc.state;
import androidx.annotation.NonNull;
/**
* Information specific to setting up a call.
*/
public final class CallSetupState {
boolean enableVideoOnCreate;
boolean isRemoteVideoOffer;
boolean acceptWithVideo;
boolean sentJoinedMessage;
public CallSetupState() {
this(false, false, false, false);
}
public CallSetupState(@NonNull CallSetupState toCopy) {
this(toCopy.enableVideoOnCreate, toCopy.isRemoteVideoOffer, toCopy.acceptWithVideo, toCopy.sentJoinedMessage);
}
public CallSetupState(boolean enableVideoOnCreate, boolean isRemoteVideoOffer, boolean acceptWithVideo, boolean sentJoinedMessage) {
this.enableVideoOnCreate = enableVideoOnCreate;
this.isRemoteVideoOffer = isRemoteVideoOffer;
this.acceptWithVideo = acceptWithVideo;
this.sentJoinedMessage = sentJoinedMessage;
}
public boolean isEnableVideoOnCreate() {
return enableVideoOnCreate;
}
public boolean isRemoteVideoOffer() {
return isRemoteVideoOffer;
}
public boolean isAcceptWithVideo() {
return acceptWithVideo;
}
public boolean hasSentJoinedMessage() {
return sentJoinedMessage;
}
}

View File

@@ -0,0 +1,25 @@
package org.thoughtcrime.securesms.service.webrtc.state
import org.thoughtcrime.securesms.recipients.Recipient
/**
* Information specific to setting up a call.
*/
data class CallSetupState(
var isEnableVideoOnCreate: Boolean = false,
var isRemoteVideoOffer: Boolean = false,
var isAcceptWithVideo: Boolean = false,
@get:JvmName("hasSentJoinedMessage") var sentJoinedMessage: Boolean = false,
@get:JvmName("shouldRingGroup") var ringGroup: Boolean = true,
var ringId: Long = NO_RING,
var ringerRecipient: Recipient = Recipient.UNKNOWN
) {
fun duplicate(): CallSetupState {
return copy()
}
companion object {
const val NO_RING = 0L
}
}

View File

@@ -25,7 +25,7 @@ public final class WebRtcServiceState {
public WebRtcServiceState(@NonNull WebRtcServiceState toCopy) {
this.actionProcessor = toCopy.actionProcessor;
this.callSetupState = new CallSetupState(toCopy.callSetupState);
this.callSetupState = toCopy.callSetupState.duplicate();
this.callInfoState = new CallInfoState(toCopy.callInfoState);
this.localDeviceState = new LocalDeviceState(toCopy.localDeviceState);
this.videoState = new VideoState(toCopy.videoState);

View File

@@ -126,7 +126,7 @@ public class WebRtcServiceStateBuilder {
private CallSetupState toBuild;
public CallSetupStateBuilder() {
toBuild = new CallSetupState(WebRtcServiceStateBuilder.this.toBuild.callSetupState);
toBuild = WebRtcServiceStateBuilder.this.toBuild.callSetupState.duplicate();
}
public @NonNull WebRtcServiceStateBuilder commit() {
@@ -140,22 +140,37 @@ public class WebRtcServiceStateBuilder {
}
public @NonNull CallSetupStateBuilder enableVideoOnCreate(boolean enableVideoOnCreate) {
toBuild.enableVideoOnCreate = enableVideoOnCreate;
toBuild.setEnableVideoOnCreate(enableVideoOnCreate);
return this;
}
public @NonNull CallSetupStateBuilder isRemoteVideoOffer(boolean isRemoteVideoOffer) {
toBuild.isRemoteVideoOffer = isRemoteVideoOffer;
toBuild.setRemoteVideoOffer(isRemoteVideoOffer);
return this;
}
public @NonNull CallSetupStateBuilder acceptWithVideo(boolean acceptWithVideo) {
toBuild.acceptWithVideo = acceptWithVideo;
toBuild.setAcceptWithVideo(acceptWithVideo);
return this;
}
public @NonNull CallSetupStateBuilder sentJoinedMessage(boolean sentJoinedMessage) {
toBuild.sentJoinedMessage = sentJoinedMessage;
toBuild.setSentJoinedMessage(sentJoinedMessage);
return this;
}
public @NonNull CallSetupStateBuilder setRingGroup(boolean ringGroup) {
toBuild.setRingGroup(ringGroup);
return this;
}
public @NonNull CallSetupStateBuilder ringId(long ringId) {
toBuild.setRingId(ringId);
return this;
}
public @NonNull CallSetupStateBuilder ringerRecipient(@NonNull Recipient ringerRecipient) {
toBuild.setRingerRecipient(ringerRecipient);
return this;
}
}
@@ -270,8 +285,8 @@ public class WebRtcServiceStateBuilder {
return this;
}
public @NonNull CallInfoStateBuilder addIdentityChangedRecipient(@NonNull RecipientId id) {
toBuild.identityChangedRecipients.add(id);
public @NonNull CallInfoStateBuilder addIdentityChangedRecipients(@NonNull Collection<RecipientId> id) {
toBuild.identityChangedRecipients.addAll(id);
return this;
}

View File

@@ -82,6 +82,8 @@ public final class FeatureFlags {
private static final String RETRY_RECEIPTS = "android.retryReceipts";
private static final String SUGGEST_SMS_BLACKLIST = "android.suggestSmsBlacklist";
private static final String ANNOUNCEMENT_GROUPS = "android.announcementGroups";
private static final String MAX_GROUP_CALL_RING_SIZE = "global.calling.maxGroupCallRingSize";
private static final String GROUP_CALL_RINGING = "android.calling.groupCallRinging";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -117,7 +119,9 @@ public final class FeatureFlags {
SENDER_KEY,
RETRY_RECEIPTS,
SUGGEST_SMS_BLACKLIST,
ANNOUNCEMENT_GROUPS
ANNOUNCEMENT_GROUPS,
MAX_GROUP_CALL_RING_SIZE,
GROUP_CALL_RINGING
);
@VisibleForTesting
@@ -166,7 +170,9 @@ public final class FeatureFlags {
RETRY_RESPOND_MAX_AGE,
SUGGEST_SMS_BLACKLIST,
RETRY_RECEIPTS,
SENDER_KEY
SENDER_KEY,
MAX_GROUP_CALL_RING_SIZE,
GROUP_CALL_RINGING
);
/**
@@ -383,6 +389,16 @@ public final class FeatureFlags {
return getString(SUGGEST_SMS_BLACKLIST, "");
}
/** Max group size that can be use group call ringing. */
public static long maxGroupCallRingSize() {
return getLong(MAX_GROUP_CALL_RING_SIZE, 16);
}
/** Whether or not to show the group call ring toggle in the UI. */
public static boolean groupCallRinging() {
return getBoolean(GROUP_CALL_RINGING, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View File

@@ -52,9 +52,9 @@ public class CallNotificationBuilder {
builder.setContentText(context.getString(R.string.CallNotificationBuilder_connecting));
builder.setPriority(NotificationCompat.PRIORITY_MIN);
} else if (type == TYPE_INCOMING_RINGING) {
builder.setContentText(context.getString(R.string.NotificationBarManager__incoming_signal_call));
builder.addAction(getServiceNotificationAction(context, WebRtcCallService.denyCallIntent(context), R.drawable.ic_close_grey600_32dp, R.string.NotificationBarManager__deny_call));
builder.addAction(getActivityNotificationAction(context, WebRtcCallActivity.ANSWER_ACTION, R.drawable.ic_phone_grey600_32dp, R.string.NotificationBarManager__answer_call));
builder.setContentText(context.getString(recipient.isGroup() ? R.string.NotificationBarManager__incoming_signal_group_call : R.string.NotificationBarManager__incoming_signal_call));
builder.addAction(getServiceNotificationAction(context, WebRtcCallService.denyCallIntent(context), R.drawable.ic_close_grey600_32dp, R.string.NotificationBarManager__decline_call));
builder.addAction(getActivityNotificationAction(context, WebRtcCallActivity.ANSWER_ACTION, R.drawable.ic_phone_grey600_32dp, recipient.isGroup() ? R.string.NotificationBarManager__join_call : R.string.NotificationBarManager__answer_call));
if (callActivityRestricted()) {
builder.setFullScreenIntent(pendingIntent, true);

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_white"
android:pathData="M25.29,19.84A2.19,2.19 0,0 1,23 22L5,22a2.19,2.19 0,0 1,-2.25 -2.11c0,-1.31 0.91,-1.89 1.9,-2.33 2,-0.89 2.43,-3.35 2.85,-6C8,8.17 8.69,4 14,4s6,4.22 6.54,7.61c0.42,2.6 0.81,5.06 2.85,6C24.38,18 25.29,18.53 25.29,19.84ZM23.65,11.68a0.86,0.86 0,0 0,0.84 -0.9c-0.13,-3.84 -1.64,-7.11 -4.14,-9A0.88,0.88 0,1 0,19.3 3.2c2.07,1.55 3.33,4.34 3.44,7.64a0.88,0.88 0,0 0,0.88 0.84ZM5.21,10.84c0.11,-3.3 1.37,-6.09 3.44,-7.64A0.88,0.88 0,1 0,7.6 1.8c-2.5,1.87 -4,5.14 -4.14,9a0.86,0.86 0,0 0,0.84 0.9h0A0.88,0.88 0,0 0,5.21 10.84ZM11.42,23.5a0.5,0.5 0,0 0,-0.44 0.73,3.46 3.46,0 0,0 6,0 0.5,0.5 0,0 0,-0.44 -0.73Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/transparent_white_40"
android:pathData="M25.29,19.84A2.19,2.19 0,0 1,23 22L5,22a2.19,2.19 0,0 1,-2.25 -2.11c0,-1.31 0.91,-1.89 1.9,-2.33 2,-0.89 2.43,-3.35 2.85,-6C8,8.17 8.69,4 14,4s6,4.22 6.54,7.61c0.42,2.6 0.81,5.06 2.85,6C24.38,18 25.29,18.53 25.29,19.84ZM23.65,11.68a0.86,0.86 0,0 0,0.84 -0.9c-0.13,-3.84 -1.64,-7.11 -4.14,-9A0.88,0.88 0,1 0,19.3 3.2c2.07,1.55 3.33,4.34 3.44,7.64a0.88,0.88 0,0 0,0.88 0.84ZM5.21,10.84c0.11,-3.3 1.37,-6.09 3.44,-7.64A0.88,0.88 0,1 0,7.6 1.8c-2.5,1.87 -4,5.14 -4.14,9a0.86,0.86 0,0 0,0.84 0.9h0A0.88,0.88 0,0 0,5.21 10.84ZM11.42,23.5a0.5,0.5 0,0 0,-0.44 0.73,3.46 3.46,0 0,0 6,0 0.5,0.5 0,0 0,-0.44 -0.73Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_grey_75"
android:pathData="M25.29,19.84A2.19,2.19 0,0 1,23 22L5,22a2.19,2.19 0,0 1,-2.25 -2.11c0,-1.31 0.91,-1.89 1.9,-2.33 2,-0.89 2.43,-3.35 2.85,-6C8,8.17 8.69,4 14,4s6,4.22 6.54,7.61c0.42,2.6 0.81,5.06 2.85,6C24.38,18 25.29,18.53 25.29,19.84ZM23.65,11.68a0.86,0.86 0,0 0,0.84 -0.9c-0.13,-3.84 -1.64,-7.11 -4.14,-9A0.88,0.88 0,1 0,19.3 3.2c2.07,1.55 3.33,4.34 3.44,7.64a0.88,0.88 0,0 0,0.88 0.84ZM5.21,10.84c0.11,-3.3 1.37,-6.09 3.44,-7.64A0.88,0.88 0,1 0,7.6 1.8c-2.5,1.87 -4,5.14 -4.14,9a0.86,0.86 0,0 0,0.84 0.9h0A0.88,0.88 0,0 0,5.21 10.84ZM11.42,23.5a0.5,0.5 0,0 0,-0.44 0.73,3.46 3.46,0 0,0 6,0 0.5,0.5 0,0 0,-0.44 -0.73Z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/transparent_white_20" />
</shape>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="false">
<layer-list>
<item android:drawable="@drawable/webrtc_call_screen_circle_grey_disabled" />
<item android:bottom="14dp" android:drawable="@drawable/ic_ring_disabled_28" android:left="14dp" android:right="14dp" android:top="14dp" />
</layer-list>
</item>
<item android:state_checked="true">
<layer-list>
<item android:drawable="@drawable/circle_tintable" />
<item android:bottom="14dp" android:drawable="@drawable/ic_ring_grey_28" android:left="14dp" android:right="14dp" android:top="14dp" />
</layer-list>
</item>
<item>
<layer-list>
<item android:drawable="@drawable/webrtc_call_screen_circle_grey" />
<item android:bottom="14dp" android:drawable="@drawable/ic_ring_28" android:left="14dp" android:right="14dp" android:top="14dp" />
</layer-list>
</item>
</selector>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="false">
<layer-list>
<item android:bottom="4dp" android:drawable="@drawable/webrtc_call_screen_circle_grey_disabled" android:left="4dp" android:right="4dp" android:top="4dp" />
<item android:bottom="14dp" android:drawable="@drawable/ic_ring_disabled_28" android:left="14dp" android:right="14dp" android:top="14dp" />
</layer-list>
</item>
<item android:state_checked="true">
<layer-list>
<item android:bottom="4dp" android:drawable="@drawable/circle_tintable" android:left="4dp" android:right="4dp" android:top="4dp" />
<item android:bottom="14dp" android:drawable="@drawable/ic_ring_grey_28" android:left="14dp" android:right="14dp" android:top="14dp" />
</layer-list>
</item>
<item>
<layer-list>
<item android:bottom="4dp" android:drawable="@drawable/webrtc_call_screen_circle_grey" android:left="4dp" android:right="4dp" android:top="4dp" />
<item android:bottom="14dp" android:drawable="@drawable/ic_ring_28" android:left="14dp" android:right="14dp" android:top="14dp" />
</layer-list>
</item>
</selector>

View File

@@ -40,7 +40,8 @@
app:layout_constraintBottom_toTopOf="@id/fold_top_guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone">
<org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
android:id="@+id/call_screen_large_local_renderer"
@@ -171,7 +172,8 @@
app:layout_constraintBottom_toTopOf="@+id/fold_top_call_screen_guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone">
<androidx.cardview.widget.CardView
android:id="@+id/call_screen_pip"
@@ -226,6 +228,7 @@
android:id="@+id/call_screen_speaker_toggle_label"
android:layout_width="64dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="32dp"
android:clickable="false"
android:ellipsize="end"
@@ -328,7 +331,7 @@
android:stateListAnimator="@null"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/call_screen_button_labels_barrier"
app:layout_constraintEnd_toStartOf="@id/call_screen_end_call"
app:layout_constraintEnd_toStartOf="@id/call_screen_audio_ring_toggle"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/call_screen_video_toggle"
tools:visibility="visible" />
@@ -354,6 +357,42 @@
app:layout_constraintVertical_bias="0"
tools:visibility="visible" />
<org.thoughtcrime.securesms.components.AccessibleToggleButton
android:id="@+id/call_screen_audio_ring_toggle"
style="@style/WebRtcCallV2CompoundButton"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/webrtc_call_screen_ring_toggle"
android:stateListAnimator="@null"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/call_screen_button_labels_barrier"
app:layout_constraintEnd_toStartOf="@id/call_screen_end_call"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/call_screen_audio_mic_toggle"
tools:visibility="visible" />
<TextView
android:id="@+id/call_screen_audio_ring_toggle_label"
android:layout_width="64dp"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:maxLines="2"
android:text="@string/WebRtcCallView__ring"
android:textAppearance="@style/TextAppearance.Signal.Subtitle"
android:textColor="@color/core_white"
android:visibility="gone"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@id/call_screen_start_call_controls"
app:layout_constraintEnd_toEndOf="@id/call_screen_audio_ring_toggle"
app:layout_constraintStart_toStartOf="@id/call_screen_audio_ring_toggle"
app:layout_constraintTop_toBottomOf="@id/call_screen_audio_ring_toggle"
app:layout_constraintVertical_bias="0"
tools:visibility="visible" />
<ImageView
android:id="@+id/call_screen_end_call"
android:layout_width="56dp"
@@ -367,7 +406,7 @@
app:layout_constraintBottom_toTopOf="@id/call_screen_button_labels_barrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/call_screen_audio_mic_toggle"
app:layout_constraintStart_toEndOf="@id/call_screen_audio_ring_toggle"
app:srcCompat="@drawable/webrtc_call_screen_hangup"
tools:visibility="visible" />
@@ -397,7 +436,7 @@
android:layout_width="match_parent"
android:layout_height="1dp"
app:barrierDirection="top"
app:constraint_referenced_ids="call_screen_speaker_toggle_label,call_screen_camera_direction_toggle_label,call_screen_video_toggle_label,call_screen_audio_mic_toggle_label,call_screen_end_call_label" />
app:constraint_referenced_ids="call_screen_speaker_toggle_label,call_screen_camera_direction_toggle_label,call_screen_video_toggle_label,call_screen_audio_mic_toggle_label,call_screen_audio_ring_toggle_label,call_screen_end_call_label" />
<ImageView
android:id="@+id/call_screen_decline_call"
@@ -411,7 +450,7 @@
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/webrtc_call_screen_hangup"
tools:visibility="visible" />
tools:visibility="gone" />
<TextView
android:id="@+id/call_screen_decline_call_label"
@@ -424,7 +463,7 @@
app:layout_constraintEnd_toEndOf="@id/call_screen_decline_call"
app:layout_constraintStart_toStartOf="@id/call_screen_decline_call"
app:layout_constraintTop_toBottomOf="@id/call_screen_decline_call"
tools:visibility="visible" />
tools:visibility="gone" />
<ImageView
android:id="@+id/call_screen_answer_call"
@@ -438,7 +477,7 @@
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toEndOf="@id/call_screen_decline_call"
app:srcCompat="@drawable/webrtc_call_screen_answer"
tools:visibility="visible" />
tools:visibility="gone" />
<TextView
android:id="@+id/call_screen_answer_call_label"
@@ -451,7 +490,7 @@
app:layout_constraintEnd_toEndOf="@id/call_screen_answer_call"
app:layout_constraintStart_toStartOf="@id/call_screen_answer_call"
app:layout_constraintTop_toBottomOf="@id/call_screen_answer_call"
tools:visibility="visible" />
tools:visibility="gone" />
<ImageView
android:id="@+id/call_screen_answer_with_audio"
@@ -463,7 +502,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/webrtc_call_screen_answer_without_video"
tools:visibility="visible" />
tools:visibility="gone" />
<TextView
android:id="@+id/call_screen_answer_with_audio_label"
@@ -476,7 +515,7 @@
app:layout_constraintBottom_toTopOf="@id/call_screen_answer_call"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
tools:visibility="gone" />
<LinearLayout
android:id="@+id/call_screen_start_call_controls"
@@ -524,7 +563,7 @@
app:layout_constraintBottom_toTopOf="@id/call_screen_navigation_bar_guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
tools:visibility="gone" />
<ViewStub
android:id="@+id/call_screen_group_call_speaker_hint"

View File

@@ -973,11 +973,13 @@
<string name="NotificationBarManager_signal_call_in_progress">Signal call in progress</string>
<string name="NotificationBarManager__establishing_signal_call">Establishing Signal call</string>
<string name="NotificationBarManager__incoming_signal_call">Incoming Signal call</string>
<string name="NotificationBarManager__incoming_signal_group_call">Incoming Signal group call</string>
<string name="NotificationBarManager__stopping_signal_call_service">Stopping Signal call service</string>
<string name="NotificationBarManager__deny_call">Deny call</string>
<string name="NotificationBarManager__decline_call">Decline call</string>
<string name="NotificationBarManager__answer_call">Answer call</string>
<string name="NotificationBarManager__end_call">End call</string>
<string name="NotificationBarManager__cancel_call">Cancel call</string>
<string name="NotificationBarManager__join_call">Join call</string>
<!-- NotificationsMegaphone -->
<string name="NotificationsMegaphone_turn_on_notifications">Turn on Notifications?</string>
@@ -1398,6 +1400,7 @@
<string name="WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera">To call %1$s, Signal needs access to your camera</string>
<string name="WebRtcCallActivity__signal_s">Signal %1$s</string>
<string name="WebRtcCallActivity__calling">Calling…</string>
<string name="WebRtcCallActivity__group_is_too_large_to_ring_the_participants">Group is too large to ring the participants.</string>
<!-- WebRtcCallView -->
<string name="WebRtcCallView__signal_call">Signal Call</string>
@@ -1412,6 +1415,35 @@
<string name="WebRtcCallView__joining">Joining…</string>
<string name="WebRtcCallView__disconnected">Disconnected</string>
<string name="WebRtcCallView__signal_will_ring_s">Signal will ring %1$s</string>
<string name="WebRtcCallView__signal_will_ring_s_and_s">Signal will ring %1$s and %2$s</string>
<plurals name="WebRtcCallView__signal_will_ring_s_s_and_d_others">
<item quantity="one">Signal will ring %1$s, %2$s, and %3$d other</item>
<item quantity="other">Signal will ring %1$s, %2$s, and %3$d others</item>
</plurals>
<string name="WebRtcCallView__s_will_be_notified">%1$s will be notified</string>
<string name="WebRtcCallView__s_and_s_will_be_notified">%1$s and %2$s will be notified</string>
<plurals name="WebRtcCallView__s_s_and_d_others_will_be_notified">
<item quantity="one">%1$s, %2$s, and %3$d other will be notified</item>
<item quantity="other">%1$s, %2$s, and %3$d others will be notified</item>
</plurals>
<string name="WebRtcCallView__ringing_s">Ringing %1$s</string>
<string name="WebRtcCallView__ringing_s_and_s">Ringing %1$s and %2$s</string>
<plurals name="WebRtcCallView__ringing_s_s_and_d_others">
<item quantity="one">Ringing %1$s, %2$s, and %3$d other</item>
<item quantity="other">Ringing %1$s, %2$s, and %3$d others</item>
</plurals>
<string name="WebRtcCallView__s_is_calling_you">%1$s is calling you</string>
<string name="WebRtcCallView__s_is_calling_you_and_s">%1$s is calling you and %2$s</string>
<string name="WebRtcCallView__s_is_calling_you_s_and_s">%1$s is calling you, %2$s, and %3$s</string>
<plurals name="WebRtcCallView__s_is_calling_you_s_s_and_d_others">
<item quantity="one">%1$s is calling you, %2$s, %3$s, and %4$d other</item>
<item quantity="other">%1$s is calling you, %2$s, %3$s, and %4$d others</item>
</plurals>
<string name="WebRtcCallView__no_one_else_is_here">No one else is here</string>
<string name="WebRtcCallView__s_is_in_this_call">%1$s is in this call</string>
<string name="WebRtcCallView__s_are_in_this_call">%1$s are in this call</string>
@@ -1427,6 +1459,7 @@
<string name="WebRtcCallView__speaker">Speaker</string>
<string name="WebRtcCallView__camera">Camera</string>
<string name="WebRtcCallView__mute">Mute</string>
<string name="WebRtcCallView__ring">Ring</string>
<string name="WebRtcCallView__end_call">End call</string>
<!-- CallParticipantsListDialog -->

View File

@@ -558,8 +558,8 @@ dependencyVerification {
['org.signal:argon2:13.1',
'0f686ccff0d4842bfcc74d92e8dc780a5f159b9376e37a1189fabbcdac458bef'],
['org.signal:ringrtc-android:2.10.8',
'4c5b2a80fc905b58c96aa9e1637f47eb68cedf8a5147d25eebe43768487b784e'],
['org.signal:ringrtc-android:2.11.1',
'74be8f643a85df0a845ea9e9ccd235ece3865a4e380bec4b8ba0a732eaafefc5'],
['org.signal:zkgroup-android:0.7.0',
'52b172565bd01526e93ebf1796b834bdc449d4fe3422c1b827e49cb8d4f13fbd'],

View File

@@ -273,6 +273,27 @@ public class SignalServiceMessageSender {
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null);
}
public List<SendMessageResult> sendCallMessage(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
SignalServiceCallMessage message)
throws IOException
{
Content content = createCallContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.DEFAULT, Optional.absent());
return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null, null);
}
public List<SendMessageResult> sendCallMessage(DistributionId distributionId,
List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess,
SignalServiceCallMessage message)
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException
{
Content content = createCallContent(message);
return sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp().get(), content, ContentHint.IMPLICIT, message.getGroupId().get(), false);
}
/**
* Send an http request on behalf of the calling infrastructure.
*

View File

@@ -15,6 +15,8 @@ public class SignalServiceCallMessage {
private final Optional<OpaqueMessage> opaqueMessage;
private final Optional<Integer> destinationDeviceId;
private final boolean isMultiRing;
private final Optional<byte[]> groupId;
private final Optional<Long> timestamp;
private SignalServiceCallMessage(Optional<OfferMessage> offerMessage,
Optional<AnswerMessage> answerMessage,
@@ -24,6 +26,20 @@ public class SignalServiceCallMessage {
Optional<OpaqueMessage> opaqueMessage,
boolean isMultiRing,
Optional<Integer> destinationDeviceId)
{
this(offerMessage, answerMessage, iceUpdateMessages, hangupMessage, busyMessage, opaqueMessage, isMultiRing, destinationDeviceId, Optional.absent(), Optional.absent());
}
private SignalServiceCallMessage(Optional<OfferMessage> offerMessage,
Optional<AnswerMessage> answerMessage,
Optional<List<IceUpdateMessage>> iceUpdateMessages,
Optional<HangupMessage> hangupMessage,
Optional<BusyMessage> busyMessage,
Optional<OpaqueMessage> opaqueMessage,
boolean isMultiRing,
Optional<Integer> destinationDeviceId,
Optional<byte[]> groupId,
Optional<Long> timestamp)
{
this.offerMessage = offerMessage;
this.answerMessage = answerMessage;
@@ -33,6 +49,8 @@ public class SignalServiceCallMessage {
this.opaqueMessage = opaqueMessage;
this.isMultiRing = isMultiRing;
this.destinationDeviceId = destinationDeviceId;
this.groupId = groupId;
this.timestamp = timestamp;
}
public static SignalServiceCallMessage forOffer(OfferMessage offerMessage, boolean isMultiRing, Integer destinationDeviceId) {
@@ -115,6 +133,19 @@ public class SignalServiceCallMessage {
Optional.fromNullable(destinationDeviceId));
}
public static SignalServiceCallMessage forOutgoingGroupOpaque(byte[] groupId, long timestamp, OpaqueMessage opaqueMessage, boolean isMultiRing, Integer destinationDeviceId) {
return new SignalServiceCallMessage(Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.of(opaqueMessage),
isMultiRing,
Optional.fromNullable(destinationDeviceId),
Optional.of(groupId),
Optional.of(timestamp));
}
public static SignalServiceCallMessage empty() {
return new SignalServiceCallMessage(Optional.absent(),
@@ -122,7 +153,8 @@ public class SignalServiceCallMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(), false,
Optional.absent(),
false,
Optional.absent());
}
@@ -157,4 +189,12 @@ public class SignalServiceCallMessage {
public Optional<Integer> getDestinationDeviceId() {
return destinationDeviceId;
}
public Optional<byte[]> getGroupId() {
return groupId;
}
public Optional<Long> getTimestamp() {
return timestamp;
}
}

View File

@@ -98,6 +98,7 @@ message CallMessage {
message Opaque {
optional bytes data = 1;
reserved /* urgency */ 2;
}
optional Offer offer = 1;