mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 12:38:33 +00:00
Add Small Group Ringing support.
This commit is contained in:
committed by
Alex Hart
parent
5787a5f68a
commit
db7272730e
@@ -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'
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.ComparatorCompat;
|
||||
import com.annimon.stream.OptionalLong;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.collections.ParticipantCollection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the state of all participants, remote and local, combined with view state
|
||||
* needed to properly render the participants. The view state primarily consists of
|
||||
* if we are in System PIP mode and if we should show our video for an outgoing call.
|
||||
*/
|
||||
public final class CallParticipantsState {
|
||||
|
||||
private static final int SMALL_GROUP_MAX = 6;
|
||||
|
||||
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
|
||||
WebRtcViewModel.GroupCallState.IDLE,
|
||||
new ParticipantCollection(SMALL_GROUP_MAX),
|
||||
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(), false),
|
||||
CallParticipant.EMPTY,
|
||||
WebRtcLocalRenderState.GONE,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
OptionalLong.empty(),
|
||||
WebRtcControls.FoldableState.flat());
|
||||
|
||||
private final WebRtcViewModel.State callState;
|
||||
private final WebRtcViewModel.GroupCallState groupCallState;
|
||||
private final ParticipantCollection remoteParticipants;
|
||||
private final CallParticipant localParticipant;
|
||||
private final CallParticipant focusedParticipant;
|
||||
private final WebRtcLocalRenderState localRenderState;
|
||||
private final boolean isInPipMode;
|
||||
private final boolean showVideoForOutgoing;
|
||||
private final boolean isViewingFocusedParticipant;
|
||||
private final OptionalLong remoteDevicesCount;
|
||||
private final WebRtcControls.FoldableState foldableState;
|
||||
|
||||
public CallParticipantsState(@NonNull WebRtcViewModel.State callState,
|
||||
@NonNull WebRtcViewModel.GroupCallState groupCallState,
|
||||
@NonNull ParticipantCollection remoteParticipants,
|
||||
@NonNull CallParticipant localParticipant,
|
||||
@NonNull CallParticipant focusedParticipant,
|
||||
@NonNull WebRtcLocalRenderState localRenderState,
|
||||
boolean isInPipMode,
|
||||
boolean showVideoForOutgoing,
|
||||
boolean isViewingFocusedParticipant,
|
||||
OptionalLong remoteDevicesCount,
|
||||
@NonNull WebRtcControls.FoldableState foldableState)
|
||||
{
|
||||
this.callState = callState;
|
||||
this.groupCallState = groupCallState;
|
||||
this.remoteParticipants = remoteParticipants;
|
||||
this.localParticipant = localParticipant;
|
||||
this.localRenderState = localRenderState;
|
||||
this.focusedParticipant = focusedParticipant;
|
||||
this.isInPipMode = isInPipMode;
|
||||
this.showVideoForOutgoing = showVideoForOutgoing;
|
||||
this.isViewingFocusedParticipant = isViewingFocusedParticipant;
|
||||
this.remoteDevicesCount = remoteDevicesCount;
|
||||
this.foldableState = foldableState;
|
||||
}
|
||||
|
||||
public @NonNull WebRtcViewModel.State getCallState() {
|
||||
return callState;
|
||||
}
|
||||
|
||||
public @NonNull WebRtcViewModel.GroupCallState getGroupCallState() {
|
||||
return groupCallState;
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getGridParticipants() {
|
||||
return remoteParticipants.getGridParticipants();
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getListParticipants() {
|
||||
List<CallParticipant> listParticipants = new ArrayList<>();
|
||||
|
||||
if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) {
|
||||
listParticipants.addAll(getAllRemoteParticipants());
|
||||
listParticipants.remove(focusedParticipant);
|
||||
} else {
|
||||
listParticipants.addAll(remoteParticipants.getListParticipants());
|
||||
}
|
||||
|
||||
if (foldableState.isFlat()) {
|
||||
listParticipants.add(CallParticipant.EMPTY);
|
||||
}
|
||||
|
||||
Collections.reverse(listParticipants);
|
||||
|
||||
return listParticipants;
|
||||
}
|
||||
|
||||
public @NonNull String getRemoteParticipantsDescription(@NonNull Context context) {
|
||||
switch (remoteParticipants.size()) {
|
||||
case 0:
|
||||
return context.getString(R.string.WebRtcCallView__no_one_else_is_here);
|
||||
case 1: {
|
||||
if (callState == WebRtcViewModel.State.CALL_PRE_JOIN && groupCallState.isNotIdle()) {
|
||||
return context.getString(remoteParticipants.get(0).isSelf() ? R.string.WebRtcCallView__s_are_in_this_call
|
||||
: R.string.WebRtcCallView__s_is_in_this_call,
|
||||
remoteParticipants.get(0).getShortRecipientDisplayName(context));
|
||||
} else {
|
||||
if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) {
|
||||
return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context));
|
||||
} else {
|
||||
return remoteParticipants.get(0).getRecipientDisplayName(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
case 2: {
|
||||
if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) {
|
||||
return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context));
|
||||
} else {
|
||||
return context.getString(R.string.WebRtcCallView__s_and_s_are_in_this_call,
|
||||
remoteParticipants.get(0).getShortRecipientDisplayName(context),
|
||||
remoteParticipants.get(1).getShortRecipientDisplayName(context));
|
||||
}
|
||||
}
|
||||
default: {
|
||||
if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) {
|
||||
return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context));
|
||||
} else {
|
||||
int others = remoteParticipants.size() - 2;
|
||||
return context.getResources().getQuantityString(R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call,
|
||||
others,
|
||||
remoteParticipants.get(0).getShortRecipientDisplayName(context),
|
||||
remoteParticipants.get(1).getShortRecipientDisplayName(context),
|
||||
others);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
|
||||
return remoteParticipants.getAllParticipants();
|
||||
}
|
||||
|
||||
public @NonNull CallParticipant getLocalParticipant() {
|
||||
return localParticipant;
|
||||
}
|
||||
|
||||
public @NonNull CallParticipant getFocusedParticipant() {
|
||||
return focusedParticipant;
|
||||
}
|
||||
|
||||
public @NonNull WebRtcLocalRenderState getLocalRenderState() {
|
||||
return localRenderState;
|
||||
}
|
||||
|
||||
public boolean isFolded() {
|
||||
return foldableState.isFolded();
|
||||
}
|
||||
|
||||
public boolean isLargeVideoGroup() {
|
||||
return getAllRemoteParticipants().size() > SMALL_GROUP_MAX;
|
||||
}
|
||||
|
||||
public boolean isInPipMode() {
|
||||
return isInPipMode;
|
||||
}
|
||||
|
||||
public boolean isViewingFocusedParticipant() {
|
||||
return isViewingFocusedParticipant;
|
||||
}
|
||||
|
||||
public boolean needsNewRequestSizes() {
|
||||
if (groupCallState.isNotIdle()) {
|
||||
return Stream.of(getAllRemoteParticipants()).anyMatch(p -> p.getVideoSink().needsNewRequestingSize());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull OptionalLong getRemoteDevicesCount() {
|
||||
return remoteDevicesCount;
|
||||
}
|
||||
|
||||
public @NonNull OptionalLong getParticipantCount() {
|
||||
boolean includeSelf = groupCallState == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
|
||||
|
||||
return remoteDevicesCount.map(l -> l + (includeSelf ? 1L : 0L))
|
||||
.or(() -> includeSelf ? OptionalLong.of(1L) : OptionalLong.empty());
|
||||
}
|
||||
|
||||
public boolean isIncomingRing() {
|
||||
return callState == WebRtcViewModel.State.CALL_INCOMING;
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState,
|
||||
@NonNull WebRtcViewModel webRtcViewModel,
|
||||
boolean enableVideo)
|
||||
{
|
||||
boolean newShowVideoForOutgoing = oldState.showVideoForOutgoing;
|
||||
if (enableVideo) {
|
||||
newShowVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
|
||||
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
|
||||
newShowVideoForOutgoing = false;
|
||||
}
|
||||
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(webRtcViewModel.getLocalParticipant(),
|
||||
oldState.isInPipMode,
|
||||
newShowVideoForOutgoing,
|
||||
webRtcViewModel.getGroupState().isNotIdle(),
|
||||
webRtcViewModel.getState(),
|
||||
webRtcViewModel.getRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
return new CallParticipantsState(webRtcViewModel.getState(),
|
||||
webRtcViewModel.getGroupState(),
|
||||
oldState.remoteParticipants.getNext(webRtcViewModel.getRemoteParticipants()),
|
||||
webRtcViewModel.getLocalParticipant(),
|
||||
getFocusedParticipant(webRtcViewModel.getRemoteParticipants()),
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
newShowVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant,
|
||||
webRtcViewModel.getRemoteDevicesCount(),
|
||||
oldState.foldableState);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, boolean isInPip) {
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
isInPip,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
oldState.focusedParticipant,
|
||||
localRenderState,
|
||||
isInPip,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.remoteDevicesCount,
|
||||
oldState.foldableState);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState setExpanded(@NonNull CallParticipantsState oldState, boolean expanded) {
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant,
|
||||
expanded);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
oldState.focusedParticipant,
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.remoteDevicesCount,
|
||||
oldState.foldableState);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) {
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
selectedPage == SelectedPage.FOCUSED,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
oldState.focusedParticipant,
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
selectedPage == SelectedPage.FOCUSED,
|
||||
oldState.remoteDevicesCount,
|
||||
oldState.foldableState);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull WebRtcControls.FoldableState foldableState) {
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
oldState.focusedParticipant,
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.remoteDevicesCount,
|
||||
foldableState);
|
||||
}
|
||||
|
||||
private static @NonNull WebRtcLocalRenderState determineLocalRenderMode(@NonNull CallParticipant localParticipant,
|
||||
boolean isInPip,
|
||||
boolean showVideoForOutgoing,
|
||||
boolean isNonIdleGroupCall,
|
||||
@NonNull WebRtcViewModel.State callState,
|
||||
int numberOfRemoteParticipants,
|
||||
boolean isViewingFocusedParticipant,
|
||||
boolean isExpanded)
|
||||
{
|
||||
boolean displayLocal = (numberOfRemoteParticipants == 0 || !isInPip) && (isNonIdleGroupCall || localParticipant.isVideoEnabled());
|
||||
WebRtcLocalRenderState localRenderState = WebRtcLocalRenderState.GONE;
|
||||
|
||||
if (isExpanded && (localParticipant.isVideoEnabled() || isNonIdleGroupCall)) {
|
||||
return WebRtcLocalRenderState.EXPANDED;
|
||||
} else if (displayLocal || showVideoForOutgoing) {
|
||||
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
|
||||
localRenderState = WebRtcLocalRenderState.SMALLER_RECTANGLE;
|
||||
} else if (numberOfRemoteParticipants == 1) {
|
||||
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
|
||||
} else {
|
||||
localRenderState = localParticipant.isVideoEnabled() ? WebRtcLocalRenderState.LARGE : WebRtcLocalRenderState.LARGE_NO_VIDEO;
|
||||
}
|
||||
} else if (callState != WebRtcViewModel.State.CALL_INCOMING && callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
|
||||
localRenderState = localParticipant.isVideoEnabled() ? WebRtcLocalRenderState.LARGE : WebRtcLocalRenderState.LARGE_NO_VIDEO;
|
||||
}
|
||||
} else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
localRenderState = WebRtcLocalRenderState.LARGE_NO_VIDEO;
|
||||
}
|
||||
|
||||
return localRenderState;
|
||||
}
|
||||
|
||||
private static @NonNull CallParticipant getFocusedParticipant(@NonNull List<CallParticipant> participants) {
|
||||
List<CallParticipant> participantsByLastSpoke = new ArrayList<>(participants);
|
||||
Collections.sort(participantsByLastSpoke, ComparatorCompat.reversed((p1, p2) -> Long.compare(p1.getLastSpoke(), p2.getLastSpoke())));
|
||||
|
||||
return participantsByLastSpoke.isEmpty() ? CallParticipant.EMPTY
|
||||
: participantsByLastSpoke.stream()
|
||||
.filter(CallParticipant::isScreenSharing)
|
||||
.findAny().orElse(participantsByLastSpoke.get(0));
|
||||
}
|
||||
|
||||
public enum SelectedPage {
|
||||
GRID,
|
||||
FOCUSED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.annimon.stream.OptionalLong
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls.FoldableState
|
||||
import org.thoughtcrime.securesms.events.CallParticipant
|
||||
import org.thoughtcrime.securesms.events.CallParticipant.Companion.createLocal
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState
|
||||
import org.thoughtcrime.securesms.service.webrtc.collections.ParticipantCollection
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Represents the state of all participants, remote and local, combined with view state
|
||||
* needed to properly render the participants. The view state primarily consists of
|
||||
* if we are in System PIP mode and if we should show our video for an outgoing call.
|
||||
*/
|
||||
data class CallParticipantsState(
|
||||
val callState: WebRtcViewModel.State = WebRtcViewModel.State.CALL_DISCONNECTED,
|
||||
val groupCallState: WebRtcViewModel.GroupCallState = WebRtcViewModel.GroupCallState.IDLE,
|
||||
private val remoteParticipants: ParticipantCollection = ParticipantCollection(SMALL_GROUP_MAX),
|
||||
val localParticipant: CallParticipant = createLocal(CameraState.UNKNOWN, BroadcastVideoSink(), false),
|
||||
val focusedParticipant: CallParticipant = CallParticipant.EMPTY,
|
||||
val localRenderState: WebRtcLocalRenderState = WebRtcLocalRenderState.GONE,
|
||||
val isInPipMode: Boolean = false,
|
||||
private val showVideoForOutgoing: Boolean = false,
|
||||
val isViewingFocusedParticipant: Boolean = false,
|
||||
val remoteDevicesCount: OptionalLong = OptionalLong.empty(),
|
||||
private val foldableState: FoldableState = FoldableState.flat(),
|
||||
val isInOutgoingRingingMode: Boolean = false,
|
||||
val ringGroup: Boolean = false,
|
||||
val ringerRecipient: Recipient = Recipient.UNKNOWN,
|
||||
val groupMembers: List<GroupMemberEntry.FullMember> = emptyList()
|
||||
) {
|
||||
|
||||
val allRemoteParticipants: List<CallParticipant> = remoteParticipants.allParticipants
|
||||
val isFolded: Boolean = foldableState.isFolded
|
||||
val isLargeVideoGroup: Boolean = allRemoteParticipants.size > SMALL_GROUP_MAX
|
||||
val isIncomingRing: Boolean = callState == WebRtcViewModel.State.CALL_INCOMING
|
||||
|
||||
val gridParticipants: List<CallParticipant>
|
||||
get() {
|
||||
return remoteParticipants.gridParticipants
|
||||
}
|
||||
|
||||
val listParticipants: List<CallParticipant>
|
||||
get() {
|
||||
val listParticipants: MutableList<CallParticipant> = mutableListOf()
|
||||
if (isViewingFocusedParticipant && allRemoteParticipants.size > 1) {
|
||||
listParticipants.addAll(allRemoteParticipants)
|
||||
listParticipants.remove(focusedParticipant)
|
||||
} else {
|
||||
listParticipants.addAll(remoteParticipants.listParticipants)
|
||||
}
|
||||
if (foldableState.isFlat) {
|
||||
listParticipants.add(CallParticipant.EMPTY)
|
||||
}
|
||||
listParticipants.reverse()
|
||||
return listParticipants
|
||||
}
|
||||
|
||||
val participantCount: OptionalLong
|
||||
get() {
|
||||
val includeSelf = groupCallState == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED
|
||||
return remoteDevicesCount.map { l: Long -> l + if (includeSelf) 1L else 0L }
|
||||
.or { if (includeSelf) OptionalLong.of(1L) else OptionalLong.empty() }
|
||||
}
|
||||
|
||||
fun getPreJoinGroupDescription(context: Context): String? {
|
||||
if (callState != WebRtcViewModel.State.CALL_PRE_JOIN || groupCallState.isIdle) {
|
||||
return null
|
||||
}
|
||||
|
||||
return if (remoteParticipants.isEmpty) {
|
||||
describeGroupMembers(
|
||||
context = context,
|
||||
oneParticipant = if (ringGroup) R.string.WebRtcCallView__signal_will_ring_s else R.string.WebRtcCallView__s_will_be_notified,
|
||||
twoParticipants = if (ringGroup) R.string.WebRtcCallView__signal_will_ring_s_and_s else R.string.WebRtcCallView__s_and_s_will_be_notified,
|
||||
multipleParticipants = if (ringGroup) R.plurals.WebRtcCallView__signal_will_ring_s_s_and_d_others else R.plurals.WebRtcCallView__s_s_and_d_others_will_be_notified,
|
||||
members = groupMembers
|
||||
)
|
||||
} else {
|
||||
when (remoteParticipants.size()) {
|
||||
0 -> context.getString(R.string.WebRtcCallView__no_one_else_is_here)
|
||||
1 -> context.getString(if (remoteParticipants[0].isSelf) R.string.WebRtcCallView__s_are_in_this_call else R.string.WebRtcCallView__s_is_in_this_call, remoteParticipants[0].getShortRecipientDisplayName(context))
|
||||
2 -> context.getString(
|
||||
R.string.WebRtcCallView__s_and_s_are_in_this_call,
|
||||
remoteParticipants[0].getShortRecipientDisplayName(context),
|
||||
remoteParticipants[1].getShortRecipientDisplayName(context)
|
||||
)
|
||||
else -> {
|
||||
val others = remoteParticipants.size() - 2
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call,
|
||||
others,
|
||||
remoteParticipants[0].getShortRecipientDisplayName(context),
|
||||
remoteParticipants[1].getShortRecipientDisplayName(context),
|
||||
others
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getOutgoingRingingGroupDescription(context: Context): String? {
|
||||
if (callState == WebRtcViewModel.State.CALL_CONNECTED &&
|
||||
groupCallState == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED &&
|
||||
isInOutgoingRingingMode
|
||||
) {
|
||||
return describeGroupMembers(
|
||||
context = context,
|
||||
oneParticipant = R.string.WebRtcCallView__ringing_s,
|
||||
twoParticipants = R.string.WebRtcCallView__ringing_s_and_s,
|
||||
multipleParticipants = R.plurals.WebRtcCallView__ringing_s_s_and_d_others,
|
||||
members = groupMembers
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun getIncomingRingingGroupDescription(context: Context): String? {
|
||||
if (callState == WebRtcViewModel.State.CALL_INCOMING &&
|
||||
groupCallState == WebRtcViewModel.GroupCallState.RINGING &&
|
||||
ringerRecipient.hasUuid()
|
||||
) {
|
||||
val ringerName = ringerRecipient.getShortDisplayName(context)
|
||||
val membersWithoutYouOrRinger: List<GroupMemberEntry.FullMember> = groupMembers.filterNot { it.member.isSelf || ringerRecipient.requireUuid() == it.member.uuid.orNull() }
|
||||
|
||||
return when (membersWithoutYouOrRinger.size) {
|
||||
0 -> context.getString(R.string.WebRtcCallView__s_is_calling_you, ringerName)
|
||||
1 -> context.getString(
|
||||
R.string.WebRtcCallView__s_is_calling_you_and_s,
|
||||
ringerName,
|
||||
membersWithoutYouOrRinger[0].member.getShortDisplayName(context)
|
||||
)
|
||||
2 -> context.getString(
|
||||
R.string.WebRtcCallView__s_is_calling_you_s_and_s,
|
||||
ringerName,
|
||||
membersWithoutYouOrRinger[0].member.getShortDisplayName(context),
|
||||
membersWithoutYouOrRinger[1].member.getShortDisplayName(context)
|
||||
)
|
||||
else -> {
|
||||
val others = membersWithoutYouOrRinger.size - 2
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.WebRtcCallView__s_is_calling_you_s_s_and_d_others,
|
||||
others,
|
||||
ringerName,
|
||||
membersWithoutYouOrRinger[0].member.getShortDisplayName(context),
|
||||
membersWithoutYouOrRinger[1].member.getShortDisplayName(context),
|
||||
others
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun needsNewRequestSizes(): Boolean {
|
||||
return if (groupCallState.isNotIdle) {
|
||||
allRemoteParticipants.any { it.videoSink.needsNewRequestingSize() }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SMALL_GROUP_MAX = 6
|
||||
|
||||
@JvmField
|
||||
val MAX_OUTGOING_GROUP_RING_DURATION = TimeUnit.MINUTES.toMillis(1)
|
||||
|
||||
@JvmField
|
||||
val STARTING_STATE = CallParticipantsState()
|
||||
|
||||
@JvmStatic
|
||||
fun update(
|
||||
oldState: CallParticipantsState,
|
||||
webRtcViewModel: WebRtcViewModel,
|
||||
enableVideo: Boolean
|
||||
): CallParticipantsState {
|
||||
|
||||
var newShowVideoForOutgoing: Boolean = oldState.showVideoForOutgoing
|
||||
if (enableVideo) {
|
||||
newShowVideoForOutgoing = webRtcViewModel.state == WebRtcViewModel.State.CALL_OUTGOING
|
||||
} else if (webRtcViewModel.state != WebRtcViewModel.State.CALL_OUTGOING) {
|
||||
newShowVideoForOutgoing = false
|
||||
}
|
||||
|
||||
val isInOutgoingRingingMode = if (oldState.isInOutgoingRingingMode) {
|
||||
webRtcViewModel.callConnectedTime + MAX_OUTGOING_GROUP_RING_DURATION > System.currentTimeMillis() && webRtcViewModel.remoteParticipants.size == 0
|
||||
} else {
|
||||
oldState.ringGroup &&
|
||||
webRtcViewModel.callConnectedTime + MAX_OUTGOING_GROUP_RING_DURATION > System.currentTimeMillis() &&
|
||||
webRtcViewModel.remoteParticipants.size == 0 &&
|
||||
oldState.callState == WebRtcViewModel.State.CALL_OUTGOING &&
|
||||
webRtcViewModel.state == WebRtcViewModel.State.CALL_CONNECTED
|
||||
}
|
||||
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(
|
||||
oldState = oldState,
|
||||
localParticipant = webRtcViewModel.localParticipant,
|
||||
showVideoForOutgoing = newShowVideoForOutgoing,
|
||||
isNonIdleGroupCall = webRtcViewModel.groupState.isNotIdle,
|
||||
callState = webRtcViewModel.state,
|
||||
numberOfRemoteParticipants = webRtcViewModel.remoteParticipants.size
|
||||
)
|
||||
|
||||
return oldState.copy(
|
||||
callState = webRtcViewModel.state,
|
||||
groupCallState = webRtcViewModel.groupState,
|
||||
remoteParticipants = oldState.remoteParticipants.getNext(webRtcViewModel.remoteParticipants),
|
||||
localParticipant = webRtcViewModel.localParticipant,
|
||||
focusedParticipant = getFocusedParticipant(webRtcViewModel.remoteParticipants),
|
||||
localRenderState = localRenderState,
|
||||
showVideoForOutgoing = newShowVideoForOutgoing,
|
||||
remoteDevicesCount = webRtcViewModel.remoteDevicesCount,
|
||||
ringGroup = webRtcViewModel.shouldRingGroup(),
|
||||
isInOutgoingRingingMode = isInOutgoingRingingMode,
|
||||
ringerRecipient = webRtcViewModel.ringerRecipient
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun update(oldState: CallParticipantsState, isInPip: Boolean): CallParticipantsState {
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState, isInPip = isInPip)
|
||||
|
||||
return oldState.copy(localRenderState = localRenderState, isInPipMode = isInPip)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setExpanded(oldState: CallParticipantsState, expanded: Boolean): CallParticipantsState {
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState, isExpanded = expanded)
|
||||
|
||||
return oldState.copy(localRenderState = localRenderState)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun update(oldState: CallParticipantsState, selectedPage: SelectedPage): CallParticipantsState {
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState, isViewingFocusedParticipant = selectedPage == SelectedPage.FOCUSED)
|
||||
|
||||
return oldState.copy(localRenderState = localRenderState, isViewingFocusedParticipant = selectedPage == SelectedPage.FOCUSED)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun update(oldState: CallParticipantsState, foldableState: FoldableState): CallParticipantsState {
|
||||
val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState)
|
||||
|
||||
return oldState.copy(localRenderState = localRenderState, foldableState = foldableState)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun update(oldState: CallParticipantsState, groupMembers: List<GroupMemberEntry.FullMember>): CallParticipantsState {
|
||||
return oldState.copy(groupMembers = groupMembers)
|
||||
}
|
||||
|
||||
private fun determineLocalRenderMode(
|
||||
oldState: CallParticipantsState,
|
||||
localParticipant: CallParticipant = oldState.localParticipant,
|
||||
isInPip: Boolean = oldState.isInPipMode,
|
||||
showVideoForOutgoing: Boolean = oldState.showVideoForOutgoing,
|
||||
isNonIdleGroupCall: Boolean = oldState.groupCallState.isNotIdle,
|
||||
callState: WebRtcViewModel.State = oldState.callState,
|
||||
numberOfRemoteParticipants: Int = oldState.allRemoteParticipants.size,
|
||||
isViewingFocusedParticipant: Boolean = oldState.isViewingFocusedParticipant,
|
||||
isExpanded: Boolean = oldState.localRenderState == WebRtcLocalRenderState.EXPANDED
|
||||
): WebRtcLocalRenderState {
|
||||
|
||||
val displayLocal: Boolean = (numberOfRemoteParticipants == 0 || !isInPip) && (isNonIdleGroupCall || localParticipant.isVideoEnabled)
|
||||
var localRenderState: WebRtcLocalRenderState = WebRtcLocalRenderState.GONE
|
||||
|
||||
if (isExpanded && (localParticipant.isVideoEnabled || isNonIdleGroupCall)) {
|
||||
return WebRtcLocalRenderState.EXPANDED
|
||||
} else if (displayLocal || showVideoForOutgoing) {
|
||||
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
localRenderState = if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
|
||||
WebRtcLocalRenderState.SMALLER_RECTANGLE
|
||||
} else if (numberOfRemoteParticipants == 1) {
|
||||
WebRtcLocalRenderState.SMALL_RECTANGLE
|
||||
} else {
|
||||
if (localParticipant.isVideoEnabled) WebRtcLocalRenderState.LARGE else WebRtcLocalRenderState.LARGE_NO_VIDEO
|
||||
}
|
||||
} else if (callState != WebRtcViewModel.State.CALL_INCOMING && callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
|
||||
localRenderState = if (localParticipant.isVideoEnabled) WebRtcLocalRenderState.LARGE else WebRtcLocalRenderState.LARGE_NO_VIDEO
|
||||
}
|
||||
} else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
localRenderState = WebRtcLocalRenderState.LARGE_NO_VIDEO
|
||||
}
|
||||
return localRenderState
|
||||
}
|
||||
|
||||
private fun getFocusedParticipant(participants: List<CallParticipant>): CallParticipant {
|
||||
val participantsByLastSpoke: List<CallParticipant> = participants.sortedByDescending(CallParticipant::lastSpoke)
|
||||
|
||||
return if (participantsByLastSpoke.isEmpty()) {
|
||||
CallParticipant.EMPTY
|
||||
} else {
|
||||
participantsByLastSpoke.firstOrNull(CallParticipant::isScreenSharing) ?: participantsByLastSpoke[0]
|
||||
}
|
||||
}
|
||||
|
||||
private fun describeGroupMembers(
|
||||
context: Context,
|
||||
@StringRes oneParticipant: Int,
|
||||
@StringRes twoParticipants: Int,
|
||||
@PluralsRes multipleParticipants: Int,
|
||||
members: List<GroupMemberEntry.FullMember>
|
||||
): String {
|
||||
val membersWithoutYou: List<GroupMemberEntry.FullMember> = members.filterNot { it.member.isSelf }
|
||||
|
||||
return when (membersWithoutYou.size) {
|
||||
0 -> ""
|
||||
1 -> context.getString(
|
||||
oneParticipant,
|
||||
membersWithoutYou[0].member.getShortDisplayName(context)
|
||||
)
|
||||
2 -> context.getString(
|
||||
twoParticipants,
|
||||
membersWithoutYou[0].member.getShortDisplayName(context),
|
||||
membersWithoutYou[1].member.getShortDisplayName(context)
|
||||
)
|
||||
else -> {
|
||||
val others = membersWithoutYou.size - 2
|
||||
context.resources.getQuantityString(
|
||||
multipleParticipants,
|
||||
others,
|
||||
membersWithoutYou[0].member.getShortDisplayName(context),
|
||||
membersWithoutYou[1].member.getShortDisplayName(context),
|
||||
others
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SelectedPage {
|
||||
GRID, FOCUSED
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,8 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
private ImageView answer;
|
||||
private ImageView cameraDirectionToggle;
|
||||
private TextView cameraDirectionToggleLabel;
|
||||
private AccessibleToggleButton ringToggle;
|
||||
private TextView ringToggleLabel;
|
||||
private PictureInPictureGestureHelper pictureInPictureGestureHelper;
|
||||
private ImageView hangup;
|
||||
private TextView hangupLabel;
|
||||
@@ -171,6 +173,8 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
answer = findViewById(R.id.call_screen_answer_call);
|
||||
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
|
||||
cameraDirectionToggleLabel = findViewById(R.id.call_screen_camera_direction_toggle_label);
|
||||
ringToggle = findViewById(R.id.call_screen_audio_ring_toggle);
|
||||
ringToggleLabel = findViewById(R.id.call_screen_audio_ring_toggle_label);
|
||||
hangup = findViewById(R.id.call_screen_end_call);
|
||||
hangupLabel = findViewById(R.id.call_screen_end_call_label);
|
||||
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
|
||||
@@ -239,6 +243,10 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
runIfNonNull(controlsListener, listener -> listener.onMicChanged(isOn));
|
||||
});
|
||||
|
||||
ringToggle.setOnCheckedChangeListener((v, isOn) -> {
|
||||
runIfNonNull(controlsListener, listener -> listener.onRingGroupChanged(isOn, ringToggle.isActivated()));
|
||||
});
|
||||
|
||||
cameraDirectionToggle.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCameraDirectionChanged));
|
||||
|
||||
hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed));
|
||||
@@ -358,8 +366,14 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode(), isPortrait, isLandscapeEnabled));
|
||||
}
|
||||
|
||||
if (state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN && state.getGroupCallState().isNotIdle()) {
|
||||
status.setText(state.getRemoteParticipantsDescription(getContext()));
|
||||
if (state.getGroupCallState().isNotIdle()) {
|
||||
if (state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
status.setText(state.getPreJoinGroupDescription(getContext()));
|
||||
} else if (state.getCallState() == WebRtcViewModel.State.CALL_CONNECTED && state.isInOutgoingRingingMode()) {
|
||||
status.setText(state.getOutgoingRingingGroupDescription(getContext()));
|
||||
} else if (state.getGroupCallState().isRinging()) {
|
||||
status.setText(state.getIncomingRingingGroupDescription(getContext()));
|
||||
}
|
||||
}
|
||||
|
||||
if (state.getGroupCallState().isNotIdle()) {
|
||||
@@ -641,6 +655,11 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
fullScreenShade.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayRingToggle()) {
|
||||
visibleViewSet.add(ringToggle);
|
||||
visibleViewSet.add(ringToggleLabel);
|
||||
}
|
||||
|
||||
if (webRtcControls.isFadeOutEnabled()) {
|
||||
if (!controls.isFadeOutEnabled()) {
|
||||
scheduleFadeOut();
|
||||
@@ -947,6 +966,7 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle);
|
||||
videoToggle.setBackgroundResource(R.drawable.webrtc_call_screen_video_toggle);
|
||||
audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle);
|
||||
ringToggle.setBackgroundResource(R.drawable.webrtc_call_screen_ring_toggle);
|
||||
}
|
||||
|
||||
private void updateButtonStateForSmallButtons() {
|
||||
@@ -955,6 +975,7 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle_small);
|
||||
videoToggle.setBackgroundResource(R.drawable.webrtc_call_screen_video_toggle_small);
|
||||
audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle_small);
|
||||
ringToggle.setBackgroundResource(R.drawable.webrtc_call_screen_ring_toggle_small);
|
||||
}
|
||||
|
||||
private boolean showParticipantsList() {
|
||||
@@ -968,6 +989,14 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public void setRingGroup(boolean shouldRingGroup) {
|
||||
ringToggle.setChecked(shouldRingGroup, false);
|
||||
}
|
||||
|
||||
public void enableRingGroup(boolean enabled) {
|
||||
ringToggle.setActivated(enabled);
|
||||
}
|
||||
|
||||
public interface ControlsListener {
|
||||
void onStartCall(boolean isVideoCall);
|
||||
void onCancelStartCall();
|
||||
@@ -985,5 +1014,6 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
void onShowParticipantsList();
|
||||
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
|
||||
void onLocalPictureInPictureClicked();
|
||||
void onRingGroupChanged(boolean ringGroup, boolean ringingAllowed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
@@ -29,6 +30,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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{" +
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
9
app/src/main/res/drawable/ic_ring_28.xml
Normal file
9
app/src/main/res/drawable/ic_ring_28.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_ring_disabled_28.xml
Normal file
9
app/src/main/res/drawable/ic_ring_disabled_28.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/ic_ring_grey_28.xml
Normal file
9
app/src/main/res/drawable/ic_ring_grey_28.xml
Normal 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>
|
||||
@@ -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>
|
||||
21
app/src/main/res/drawable/webrtc_call_screen_ring_toggle.xml
Normal file
21
app/src/main/res/drawable/webrtc_call_screen_ring_toggle.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ message CallMessage {
|
||||
|
||||
message Opaque {
|
||||
optional bytes data = 1;
|
||||
reserved /* urgency */ 2;
|
||||
}
|
||||
|
||||
optional Offer offer = 1;
|
||||
|
||||
Reference in New Issue
Block a user