Handle safety number changes in a group call context.

This commit is contained in:
Cody Henthorne
2020-12-04 15:24:18 -05:00
committed by Greyson Parrelli
parent 112782ccaf
commit 42d61518b3
17 changed files with 578 additions and 121 deletions

View File

@@ -0,0 +1,47 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.recipients.Recipient;
/**
* Utility for showing and hiding safety number change notifications during a group call.
*/
public final class GroupCallSafetyNumberChangeNotificationUtil {
public static final String GROUP_CALLING_NOTIFICATION_TAG = "group_calling";
private GroupCallSafetyNumberChangeNotificationUtil() {
}
public static void showNotification(@NonNull Context context, @NonNull Recipient recipient) {
Intent contentIntent = new Intent(context, WebRtcCallActivity.class);
contentIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, 0);
Notification safetyNumberChangeNotification = new NotificationCompat.Builder(context, NotificationChannels.CALLS)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(recipient.getDisplayName(context))
.setContentText(context.getString(R.string.GroupCallSafetyNumberChangeNotification__someone_has_joined_this_call_with_a_safety_number_that_has_changed))
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.GroupCallSafetyNumberChangeNotification__someone_has_joined_this_call_with_a_safety_number_that_has_changed)))
.setContentIntent(pendingIntent)
.build();
NotificationManagerCompat.from(context).notify(GROUP_CALLING_NOTIFICATION_TAG, recipient.hashCode(), safetyNumberChangeNotification);
}
public static void cancelNotification(@NonNull Context context, @NonNull Recipient recipient) {
NotificationManagerCompat.from(context).cancel(GROUP_CALLING_NOTIFICATION_TAG, recipient.hashCode());
}
}

View File

@@ -1,19 +1,35 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.media.AudioManager;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.Collections;
import java.util.List;
class WebRtcCallRepository {
private final Context context;
private final AudioManager audioManager;
WebRtcCallRepository() {
WebRtcCallRepository(@NonNull Context context) {
this.context = context;
this.audioManager = ServiceUtil.getAudioManager(ApplicationDependencies.getApplication());
}
WebRtcAudioOutput getAudioOutput() {
@NonNull WebRtcAudioOutput getAudioOutput() {
if (audioManager.isBluetoothScoOn()) {
return WebRtcAudioOutput.HEADSET;
} else if (audioManager.isSpeakerphoneOn()) {
@@ -22,4 +38,20 @@ class WebRtcCallRepository {
return WebRtcAudioOutput.HANDSET;
}
}
@WorkerThread
void getIdentityRecords(@NonNull Recipient recipient, @NonNull Consumer<IdentityRecordList> consumer) {
SignalExecutors.BOUNDED.execute(() -> {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
List<Recipient> recipients;
if (recipient.isGroup()) {
recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
} else {
recipients = Collections.singletonList(recipient);
}
consumer.accept(identityDatabase.getIdentities(recipients));
});
}
}

View File

@@ -689,8 +689,4 @@ public class WebRtcCallView extends FrameLayout {
void onShowParticipantsList();
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
}
public interface EventListener {
void onPotentialLayoutChange();
}
}

View File

@@ -12,15 +12,19 @@ import androidx.lifecycle.ViewModel;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.IdentityDatabase;
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.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -35,6 +39,8 @@ public class WebRtcCallViewModel extends ViewModel {
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
private final MutableLiveData<CallParticipantsState> participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE);
private final SingleLiveEvent<CallParticipantListUpdate> callParticipantListUpdate = new SingleLiveEvent<>();
private final MutableLiveData<Collection<RecipientId>> identityChangedRecipients = new MutableLiveData<>(Collections.emptyList());
private final LiveData<SafetyNumberChangeEvent> safetyNumberChangeEvent = LiveDataUtil.combineLatest(isInPipMode, identityChangedRecipients, SafetyNumberChangeEvent::new);
private boolean canDisplayTooltipIfNeeded = true;
private boolean hasEnabledLocalVideo = false;
@@ -44,8 +50,9 @@ public class WebRtcCallViewModel extends ViewModel {
private Runnable elapsedTimeRunnable = this::handleTick;
private boolean canEnterPipMode = false;
private List<CallParticipant> previousParticipantsList = Collections.emptyList();
private boolean callingStarted = false;
private final WebRtcCallRepository repository = new WebRtcCallRepository();
private final WebRtcCallRepository repository = new WebRtcCallRepository(ApplicationDependencies.getApplication());
public LiveData<Boolean> getMicrophoneEnabled() {
return Transformations.distinctUntilChanged(microphoneEnabled);
@@ -79,6 +86,10 @@ public class WebRtcCallViewModel extends ViewModel {
return callParticipantListUpdate;
}
public LiveData<SafetyNumberChangeEvent> getSafetyNumberChangeEvent() {
return safetyNumberChangeEvent;
}
public boolean canEnterPipMode() {
return canEnterPipMode;
}
@@ -87,6 +98,10 @@ public class WebRtcCallViewModel extends ViewModel {
return answerWithVideoAvailable;
}
public boolean isCallingStarted() {
return callingStarted;
}
@MainThread
public void setIsInPipMode(boolean isInPipMode) {
this.isInPipMode.setValue(isInPipMode);
@@ -123,6 +138,8 @@ public class WebRtcCallViewModel extends ViewModel {
}
previousParticipantsList = webRtcViewModel.getRemoteParticipants();
identityChangedRecipients.setValue(webRtcViewModel.getIdentityChangedParticipants());
}
updateWebRtcControls(webRtcViewModel.getState(),
@@ -146,13 +163,13 @@ public class WebRtcCallViewModel extends ViewModel {
if (localParticipant.getCameraState().isEnabled()) {
canDisplayTooltipIfNeeded = false;
hasEnabledLocalVideo = true;
events.setValue(Event.DISMISS_VIDEO_TOOLTIP);
events.setValue(new Event.DismissVideoTooltip());
}
// If remote video is enabled and we a) haven't shown our video and b) have not dismissed the popup
if (canDisplayTooltipIfNeeded && webRtcViewModel.isRemoteVideoEnabled() && !hasEnabledLocalVideo) {
canDisplayTooltipIfNeeded = false;
events.setValue(Event.SHOW_VIDEO_TOOLTIP);
events.setValue(new Event.ShowVideoTooltip());
}
}
@@ -259,8 +276,74 @@ public class WebRtcCallViewModel extends ViewModel {
cancelTimer();
}
public enum Event {
SHOW_VIDEO_TOOLTIP,
DISMISS_VIDEO_TOOLTIP
public void startCall(boolean isVideoCall) {
callingStarted = true;
Recipient recipient = getRecipient().get();
if (recipient.isGroup()) {
repository.getIdentityRecords(recipient, identityRecords -> {
if (identityRecords.isUntrusted(false) || identityRecords.isUnverified(false)) {
List<IdentityDatabase.IdentityRecord> records = identityRecords.getUnverifiedRecords();
records.addAll(identityRecords.getUntrustedRecords());
events.postValue(new Event.ShowGroupCallSafetyNumberChange(records));
} else {
events.postValue(new Event.StartCall(isVideoCall));
}
});
} else {
events.postValue(new Event.StartCall(isVideoCall));
}
}
public static abstract class Event {
private Event() {
}
public static class ShowVideoTooltip extends Event {
}
public static class DismissVideoTooltip extends Event {
}
public static class StartCall extends Event {
private final boolean isVideoCall;
public StartCall(boolean isVideoCall) {
this.isVideoCall = isVideoCall;
}
public boolean isVideoCall() {
return isVideoCall;
}
}
public static class ShowGroupCallSafetyNumberChange extends Event {
private final List<IdentityDatabase.IdentityRecord> identityRecords;
public ShowGroupCallSafetyNumberChange(@NonNull List<IdentityDatabase.IdentityRecord> identityRecords) {
this.identityRecords = identityRecords;
}
public @NonNull List<IdentityDatabase.IdentityRecord> getIdentityRecords() {
return identityRecords;
}
}
}
public static class SafetyNumberChangeEvent {
private final boolean isInPipMode;
private final Collection<RecipientId> recipientIds;
private SafetyNumberChangeEvent(boolean isInPipMode, @NonNull Collection<RecipientId> recipientIds) {
this.isInPipMode = isInPipMode;
this.recipientIds = recipientIds;
}
public boolean isInPipMode() {
return isInPipMode;
}
public @NonNull Collection<RecipientId> getRecipientIds() {
return recipientIds;
}
}
}