diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index 3f54711d5..b60bba3ac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -43,6 +43,7 @@ import org.greenrobot.eventbus.ThreadMode; import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow; import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState; +import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil; import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel; @@ -53,21 +54,25 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.WebRtcCallService; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.EllapsedTimeFormatter; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import org.whispersystems.signalservice.api.messages.calls.OfferMessage; +import java.util.List; + public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback { private static final String TAG = WebRtcCallActivity.class.getSimpleName(); - private static final int STANDARD_DELAY_FINISH = 1000; + private static final int STANDARD_DELAY_FINISH = 1000; public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION"; public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION"; @@ -138,9 +143,11 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe EventBus.getDefault().unregister(this); } - CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); - if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) { - finish(); + if (!viewModel.isCallingStarted()) { + CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); + if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) { + finish(); + } } } @@ -151,11 +158,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe EventBus.getDefault().unregister(this); - CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); - if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) { - Intent intent = new Intent(this, WebRtcCallService.class); - intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL); - startService(intent); + if (!viewModel.isCallingStarted()) { + CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); + if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) { + Intent intent = new Intent(this, WebRtcCallService.class); + intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL); + startService(intent); + } } } @@ -231,6 +240,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe viewModel.getCallTime().observe(this, this::handleCallTime); viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants); viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate); + viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent); callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> { CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); @@ -245,30 +255,35 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe } private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) { + if (event instanceof WebRtcCallViewModel.Event.StartCall) { + startCall(((WebRtcCallViewModel.Event.StartCall)event).isVideoCall()); + return; + } else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) { + SafetyNumberChangeDialog.showForGroupCall(getSupportFragmentManager(), ((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords()); + return; + } + if (isInPipMode()) { return; } - switch (event) { - case SHOW_VIDEO_TOOLTIP: - if (videoTooltip == null) { - videoTooltip = TooltipPopup.forTarget(callScreen.getVideoTooltipTarget()) - .setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine)) - .setTextColor(ContextCompat.getColor(this, R.color.core_white)) - .setText(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video) - .setOnDismissListener(() -> viewModel.onDismissedVideoTooltip()) - .show(TooltipPopup.POSITION_ABOVE); - return; - } - break; - case DISMISS_VIDEO_TOOLTIP: - if (videoTooltip != null) { - videoTooltip.dismiss(); - videoTooltip = null; - } - break; - default: - throw new IllegalArgumentException("Unknown event: " + event); + if (event instanceof WebRtcCallViewModel.Event.ShowVideoTooltip) { + if (videoTooltip == null) { + videoTooltip = TooltipPopup.forTarget(callScreen.getVideoTooltipTarget()) + .setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine)) + .setTextColor(ContextCompat.getColor(this, R.color.core_white)) + .setText(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video) + .setOnDismissListener(() -> viewModel.onDismissedVideoTooltip()) + .show(TooltipPopup.POSITION_ABOVE); + return; + } + } else if (event instanceof WebRtcCallViewModel.Event.DismissVideoTooltip) { + if (videoTooltip != null) { + videoTooltip.dismiss(); + videoTooltip = null; + } + } else { + throw new IllegalArgumentException("Unknown event: " + event); } } @@ -479,14 +494,28 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe SafetyNumberChangeDialog.showForCall(getSupportFragmentManager(), recipient.getId()); } - @Override - public void onSendAnywayAfterSafetyNumberChange() { - Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class); - intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL) - .putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId())) - .putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.AUDIO_CALL.getCode()); + public void handleSafetyNumberChangeEvent(@NonNull WebRtcCallViewModel.SafetyNumberChangeEvent safetyNumberChangeEvent) { + if (Util.hasItems(safetyNumberChangeEvent.getRecipientIds())) { + if (safetyNumberChangeEvent.isInPipMode()) { + GroupCallSafetyNumberChangeNotificationUtil.showNotification(this, viewModel.getRecipient().get()); + } else { + GroupCallSafetyNumberChangeNotificationUtil.cancelNotification(this, viewModel.getRecipient().get()); + SafetyNumberChangeDialog.showForDuringGroupCall(getSupportFragmentManager(), safetyNumberChangeEvent.getRecipientIds()); + } + } + } - startService(intent); + @Override + public void onSendAnywayAfterSafetyNumberChange(@NonNull List changedRecipients) { + CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); + if (state.getGroupCallState().isConnected()) { + Intent intent = new Intent(this, WebRtcCallService.class); + intent.setAction(WebRtcCallService.ACTION_GROUP_APPROVE_SAFETY_CHANGE) + .putExtra(WebRtcCallService.EXTRA_RECIPIENT_IDS, RecipientId.toSerializedList(changedRecipients)); + startService(intent); + } else { + startCall(state.getLocalParticipant().isVideoEnabled()); + } } @Override @@ -494,7 +523,19 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe @Override public void onCanceled() { - handleTerminate(viewModel.getRecipient().get(), HangupMessage.Type.NORMAL); + CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); + if (state != null && state.getGroupCallState().isNotIdle()) { + if (state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) { + Intent intent = new Intent(this, WebRtcCallService.class); + intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL); + startService(intent); + finish(); + } else { + handleEndCall(); + } + } else { + handleTerminate(viewModel.getRecipient().get(), HangupMessage.Type.NORMAL); + } } private boolean isSystemPipEnabledAndAvailable() { @@ -550,19 +591,23 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe } } + private void startCall(boolean isVideoCall) { + enableVideoIfAvailable = isVideoCall; + + Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class); + intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL) + .putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId())) + .putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, (isVideoCall ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL).getCode()); + startService(intent); + + MessageSender.onMessageSent(); + } + private final class ControlsListener implements WebRtcCallView.ControlsListener { @Override public void onStartCall(boolean isVideoCall) { - enableVideoIfAvailable = isVideoCall; - - Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class); - intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL) - .putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId())) - .putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, (isVideoCall ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL).getCode()); - startService(intent); - - MessageSender.onMessageSent(); + viewModel.startCall(isVideoCall); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/GroupCallSafetyNumberChangeNotificationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/GroupCallSafetyNumberChangeNotificationUtil.java new file mode 100644 index 000000000..188e91d01 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/GroupCallSafetyNumberChangeNotificationUtil.java @@ -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()); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallRepository.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallRepository.java index 8de090def..78a85495b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallRepository.java @@ -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 consumer) { + SignalExecutors.BOUNDED.execute(() -> { + IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context); + List 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)); + }); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java index e1180fcac..f66374f86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java @@ -689,8 +689,4 @@ public class WebRtcCallView extends FrameLayout { void onShowParticipantsList(); void onPageChanged(@NonNull CallParticipantsState.SelectedPage page); } - - public interface EventListener { - void onPotentialLayoutChange(); - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java index 79174772d..7b0b99c9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java @@ -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 = new MutableLiveData<>(Recipient.UNKNOWN.live()); private final MutableLiveData participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE); private final SingleLiveEvent callParticipantListUpdate = new SingleLiveEvent<>(); + private final MutableLiveData> identityChangedRecipients = new MutableLiveData<>(Collections.emptyList()); + private final LiveData 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 previousParticipantsList = Collections.emptyList(); + private boolean callingStarted = false; - private final WebRtcCallRepository repository = new WebRtcCallRepository(); + private final WebRtcCallRepository repository = new WebRtcCallRepository(ApplicationDependencies.getApplication()); public LiveData getMicrophoneEnabled() { return Transformations.distinctUntilChanged(microphoneEnabled); @@ -79,6 +86,10 @@ public class WebRtcCallViewModel extends ViewModel { return callParticipantListUpdate; } + public LiveData 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 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 identityRecords; + + public ShowGroupCallSafetyNumberChange(@NonNull List identityRecords) { + this.identityRecords = identityRecords; + } + + public @NonNull List getIdentityRecords() { + return identityRecords; + } + } + } + + public static class SafetyNumberChangeEvent { + private final boolean isInPipMode; + private final Collection recipientIds; + + private SafetyNumberChangeEvent(boolean isInPipMode, @NonNull Collection recipientIds) { + this.isInPipMode = isInPipMode; + this.recipientIds = recipientIds; + } + + public boolean isInPipMode() { + return isInPipMode; + } + + public @NonNull Collection getRecipientIds() { + return recipientIds; + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index e9854fc04..78ca66b27 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -1410,7 +1410,7 @@ public class ConversationActivity extends PassphraseRequiredActivity } @Override - public void onSendAnywayAfterSafetyNumberChange() { + public void onSendAnywayAfterSafetyNumberChange(@NonNull List changedRecipients) { initializeIdentityRecords().addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean result) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java index a323827ec..d43978802 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java @@ -4,7 +4,6 @@ import android.app.Activity; import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; -import android.telecom.Call; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -13,6 +12,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LiveData; @@ -30,16 +30,18 @@ import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.recipients.RecipientId; +import java.util.Collection; import java.util.List; public final class SafetyNumberChangeDialog extends DialogFragment implements SafetyNumberChangeAdapter.Callbacks { public static final String SAFETY_NUMBER_DIALOG = "SAFETY_NUMBER"; - private static final String RECIPIENT_IDS_EXTRA = "recipient_ids"; - private static final String MESSAGE_ID_EXTRA = "message_id"; - private static final String MESSAGE_TYPE_EXTRA = "message_type"; - private static final String IS_CALL_EXTRA = "is_call"; + private static final String RECIPIENT_IDS_EXTRA = "recipient_ids"; + private static final String MESSAGE_ID_EXTRA = "message_id"; + private static final String MESSAGE_TYPE_EXTRA = "message_type"; + private static final String CONTINUE_TEXT_RESOURCE_EXTRA = "continue_text_resource"; + private static final String CANCEL_TEXT_RESOURCE_EXTRA = "cancel_text_resource"; private SafetyNumberChangeViewModel viewModel; private SafetyNumberChangeAdapter adapter; @@ -54,6 +56,7 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa Bundle arguments = new Bundle(); arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0])); + arguments.putInt(CONTINUE_TEXT_RESOURCE_EXTRA, R.string.safety_number_change_dialog__send_anyway); SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog(); fragment.setArguments(arguments); fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG); @@ -69,6 +72,7 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0])); arguments.putLong(MESSAGE_ID_EXTRA, messageRecord.getId()); arguments.putString(MESSAGE_TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT); + arguments.putInt(CONTINUE_TEXT_RESOURCE_EXTRA, R.string.safety_number_change_dialog__send_anyway); SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog(); fragment.setArguments(arguments); fragment.show(fragmentActivity.getSupportFragmentManager(), SAFETY_NUMBER_DIALOG); @@ -77,7 +81,43 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa public static void showForCall(@NonNull FragmentManager fragmentManager, @NonNull RecipientId recipientId) { Bundle arguments = new Bundle(); arguments.putStringArray(RECIPIENT_IDS_EXTRA, new String[] { recipientId.serialize() }); - arguments.putBoolean(IS_CALL_EXTRA, true); + arguments.putInt(CONTINUE_TEXT_RESOURCE_EXTRA, R.string.safety_number_change_dialog__call_anyway); + SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog(); + fragment.setArguments(arguments); + fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG); + } + + public static void showForGroupCall(@NonNull FragmentManager fragmentManager, @NonNull List identityRecords) { + List ids = Stream.of(identityRecords) + .filterNot(IdentityDatabase.IdentityRecord::isFirstUse) + .map(record -> record.getRecipientId().serialize()) + .distinct() + .toList(); + + Bundle arguments = new Bundle(); + arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0])); + arguments.putInt(CONTINUE_TEXT_RESOURCE_EXTRA, R.string.safety_number_change_dialog__join_call); + SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog(); + fragment.setArguments(arguments); + fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG); + } + + public static void showForDuringGroupCall(@NonNull FragmentManager fragmentManager, @NonNull Collection recipientIds) { + Fragment previous = fragmentManager.findFragmentByTag(SAFETY_NUMBER_DIALOG); + if (previous != null) { + ((SafetyNumberChangeDialog) previous).updateRecipients(recipientIds); + return; + } + + List ids = Stream.of(recipientIds) + .map(RecipientId::serialize) + .distinct() + .toList(); + + Bundle arguments = new Bundle(); + arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0])); + arguments.putInt(CONTINUE_TEXT_RESOURCE_EXTRA, R.string.safety_number_change_dialog__continue_call); + arguments.putInt(CANCEL_TEXT_RESOURCE_EXTRA, R.string.safety_number_change_dialog__leave_call); SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog(); fragment.setArguments(arguments); fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG); @@ -105,7 +145,8 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa @Override public @NonNull Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - boolean isCall = requireArguments().getBoolean(IS_CALL_EXTRA, false); + int continueText = requireArguments().getInt(CONTINUE_TEXT_RESOURCE_EXTRA, android.R.string.ok); + int cancelText = requireArguments().getInt(CANCEL_TEXT_RESOURCE_EXTRA, android.R.string.cancel); dialogView = LayoutInflater.from(requireActivity()).inflate(R.layout.safety_number_change_dialog, null); @@ -115,13 +156,17 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa builder.setTitle(R.string.safety_number_change_dialog__safety_number_changes) .setView(dialogView) - .setPositiveButton(isCall ? R.string.safety_number_change_dialog__call_anyway : R.string.safety_number_change_dialog__send_anyway, this::handleSendAnyway) - .setNegativeButton(android.R.string.cancel, this::handleCancel); + .setCancelable(false) + .setPositiveButton(continueText, this::handleSendAnyway) + .setNegativeButton(cancelText, this::handleCancel); + + setCancelable(false); return builder.create(); } - @Override public void onDestroyView() { + @Override + public void onDestroyView() { dialogView = null; super.onDestroyView(); } @@ -134,6 +179,10 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa list.setLayoutManager(new LinearLayoutManager(requireContext())); } + private void updateRecipients(Collection recipientIds) { + viewModel.updateRecipients(recipientIds); + } + private void handleSendAnyway(DialogInterface dialogInterface, int which) { Activity activity = getActivity(); Callback callback; @@ -149,9 +198,9 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa @Override public void onChanged(TrustAndVerifyResult result) { if (callback != null) { - switch (result) { + switch (result.getResult()) { case TRUST_AND_VERIFY: - callback.onSendAnywayAfterSafetyNumberChange(); + callback.onSendAnywayAfterSafetyNumberChange(result.getChangedRecipients()); break; case TRUST_VERIFY_AND_RESEND: callback.onMessageResentAfterSafetyNumberChange(); @@ -177,7 +226,7 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa } public interface Callback { - void onSendAnywayAfterSafetyNumberChange(); + void onSendAnywayAfterSafetyNumberChange(@NonNull List changedRecipients); void onMessageResentAfterSafetyNumberChange(); void onCanceled(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java index 0c438f98d..20a766885 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java @@ -11,15 +11,12 @@ import androidx.lifecycle.MutableLiveData; import com.annimon.stream.Stream; import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore; -import org.thoughtcrime.securesms.database.Database; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord; import org.thoughtcrime.securesms.database.MessageDatabase; -import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; -import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; @@ -29,6 +26,7 @@ import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.SignalProtocolAddress; +import java.util.Collection; import java.util.List; import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK; @@ -43,12 +41,6 @@ final class SafetyNumberChangeRepository { this.context = context.getApplicationContext(); } - @NonNull LiveData getSafetyNumberChangeState(@NonNull List recipientIds, @Nullable Long messageId, @Nullable String messageType) { - MutableLiveData liveData = new MutableLiveData<>(); - SignalExecutors.BOUNDED.execute(() -> liveData.postValue(getSafetyNumberChangeStateInternal(recipientIds, messageId, messageType))); - return liveData; - } - @NonNull LiveData trustOrVerifyChangedRecipients(@NonNull List changedRecipients) { MutableLiveData liveData = new MutableLiveData<>(); SignalExecutors.BOUNDED.execute(() -> liveData.postValue(trustOrVerifyChangedRecipientsInternal(changedRecipients))); @@ -62,7 +54,7 @@ final class SafetyNumberChangeRepository { } @WorkerThread - private @NonNull SafetyNumberChangeState getSafetyNumberChangeStateInternal(@NonNull List recipientIds, @Nullable Long messageId, @Nullable String messageType) { + public @NonNull SafetyNumberChangeState getSafetyNumberChangeState(@NonNull Collection recipientIds, @Nullable Long messageId, @Nullable String messageType) { MessageRecord messageRecord = null; if (messageId != null && messageType != null) { messageRecord = getMessageRecord(messageId, messageType); @@ -112,7 +104,7 @@ final class SafetyNumberChangeRepository { } } - return TrustAndVerifyResult.TRUST_AND_VERIFY; + return TrustAndVerifyResult.trustAndVerify(changedRecipients); } @WorkerThread @@ -130,7 +122,7 @@ final class SafetyNumberChangeRepository { processOutgoingMessageRecord(changedRecipients, messageRecord); } - return TrustAndVerifyResult.TRUST_VERIFY_AND_RESEND; + return TrustAndVerifyResult.trustVerifyAndResend(changedRecipients, messageRecord); } @WorkerThread diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeViewModel.java index 54344a9ab..5151acea5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeViewModel.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.conversation.ui.error; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; @@ -10,22 +11,26 @@ import androidx.lifecycle.ViewModelProvider; import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeRepository.SafetyNumberChangeState; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; +import java.util.Collection; import java.util.List; import java.util.Objects; public final class SafetyNumberChangeViewModel extends ViewModel { - private final SafetyNumberChangeRepository safetyNumberChangeRepository; - private final LiveData safetyNumberChangeState; + private final SafetyNumberChangeRepository safetyNumberChangeRepository; + private final MutableLiveData> recipientIds; + private final LiveData safetyNumberChangeState; private SafetyNumberChangeViewModel(@NonNull List recipientIds, @Nullable Long messageId, @Nullable String messageType, - SafetyNumberChangeRepository safetyNumberChangeRepository) + @NonNull SafetyNumberChangeRepository safetyNumberChangeRepository) { this.safetyNumberChangeRepository = safetyNumberChangeRepository; - safetyNumberChangeState = this.safetyNumberChangeRepository.getSafetyNumberChangeState(recipientIds, messageId, messageType); + this.recipientIds = new MutableLiveData<>(recipientIds); + this.safetyNumberChangeState = LiveDataUtil.mapAsync(this.recipientIds, ids -> this.safetyNumberChangeRepository.getSafetyNumberChangeState(ids, messageId, messageType)); } @NonNull LiveData> getChangedRecipients() { @@ -41,6 +46,10 @@ public final class SafetyNumberChangeViewModel extends ViewModel { } } + void updateRecipients(Collection recipientIds) { + this.recipientIds.setValue(recipientIds); + } + public static final class Factory implements ViewModelProvider.Factory { private final List recipientIds; private final Long messageId; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/TrustAndVerifyResult.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/TrustAndVerifyResult.java index 37b4c1313..cbbd3b371 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/TrustAndVerifyResult.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/TrustAndVerifyResult.java @@ -1,7 +1,53 @@ package org.thoughtcrime.securesms.conversation.ui.error; -public enum TrustAndVerifyResult { - TRUST_AND_VERIFY, - TRUST_VERIFY_AND_RESEND, - UNKNOWN +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.annimon.stream.Stream; + +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.recipients.RecipientId; + +import java.util.List; + +/** + * Result of trust/verify after safety number change. + */ +public class TrustAndVerifyResult { + + private final List changedRecipients; + private final MessageRecord messageRecord; + private final Result result; + + static TrustAndVerifyResult trustAndVerify(@NonNull List changedRecipients) { + return new TrustAndVerifyResult(changedRecipients, null, Result.TRUST_AND_VERIFY); + } + + static TrustAndVerifyResult trustVerifyAndResend(@NonNull List changedRecipients, @NonNull MessageRecord messageRecord) { + return new TrustAndVerifyResult(changedRecipients, messageRecord, Result.TRUST_VERIFY_AND_RESEND); + } + + TrustAndVerifyResult(@NonNull List changedRecipients, @Nullable MessageRecord messageRecord, @NonNull Result result) { + this.changedRecipients = Stream.of(changedRecipients).map(changedRecipient -> changedRecipient.getRecipient().getId()).toList(); + this.messageRecord = messageRecord; + this.result = result; + } + + public @NonNull List getChangedRecipients() { + return changedRecipients; + } + + public @Nullable MessageRecord getMessageRecord() { + return messageRecord; + } + + public @NonNull Result getResult() { + return result; + } + + public enum Result { + TRUST_AND_VERIFY, + TRUST_VERIFY_AND_RESEND, + UNKNOWN + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java index 8f98de178..d72c342a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.java @@ -7,9 +7,11 @@ import com.annimon.stream.Stream; import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.ringrtc.CameraState; import java.util.List; +import java.util.Set; public class WebRtcViewModel { @@ -91,6 +93,7 @@ public class WebRtcViewModel { private final CallParticipant localParticipant; private final List remoteParticipants; + private final Set identityChangedRecipients; public WebRtcViewModel(@NonNull State state, @NonNull GroupCallState groupState, @@ -101,15 +104,17 @@ public class WebRtcViewModel { boolean isMicrophoneEnabled, boolean isRemoteVideoOffer, long callConnectedTime, - @NonNull List remoteParticipants) + @NonNull List remoteParticipants, + @NonNull Set identityChangedRecipients) { - this.state = state; - this.groupState = groupState; - this.recipient = recipient; - this.isBluetoothAvailable = isBluetoothAvailable; - this.isRemoteVideoOffer = isRemoteVideoOffer; - this.callConnectedTime = callConnectedTime; - this.remoteParticipants = remoteParticipants; + this.state = state; + this.groupState = groupState; + this.recipient = recipient; + this.isBluetoothAvailable = isBluetoothAvailable; + this.isRemoteVideoOffer = isRemoteVideoOffer; + this.callConnectedTime = callConnectedTime; + this.remoteParticipants = remoteParticipants; + this.identityChangedRecipients = identityChangedRecipients; localParticipant = CallParticipant.createLocal(localCameraState, localSink != null ? localSink : new BroadcastVideoSink(null), isMicrophoneEnabled); } @@ -150,7 +155,12 @@ public class WebRtcViewModel { return remoteParticipants; } - @Override public @NonNull String toString() { + public @NonNull Set getIdentityChangedParticipants() { + return identityChangedRecipients; + } + + @Override + public @NonNull String toString() { return "WebRtcViewModel{" + "state=" + state + ", recipient=" + recipient.getId() + @@ -159,6 +169,7 @@ public class WebRtcViewModel { ", callConnectedTime=" + callConnectedTime + ", localParticipant=" + localParticipant + ", remoteParticipants=" + remoteParticipants + + ", identityChangedRecipients=" + identityChangedRecipients + '}'; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java index 4aa73bff2..54d501b9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -138,6 +138,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, public static final String EXTRA_GROUP_CALL_UPDATE_SENDER = "group_call_update_sender"; public static final String EXTRA_GROUP_CALL_UPDATE_GROUP = "group_call_update_group"; public static final String EXTRA_GROUP_CALL_ERA_ID = "era_id"; + public static final String EXTRA_RECIPIENT_IDS = "recipient_ids"; public static final String ACTION_PRE_JOIN_CALL = "CALL_PRE_JOIN"; public static final String ACTION_CANCEL_PRE_JOIN_CALL = "CANCEL_PRE_JOIN_CALL"; @@ -203,6 +204,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer, public static final String ACTION_GROUP_CALL_ENDED = "GROUP_CALL_ENDED"; public static final String ACTION_GROUP_CALL_UPDATE_MESSAGE = "GROUP_CALL_UPDATE_MESSAGE"; public static final String ACTION_GROUP_CALL_PEEK = "GROUP_CALL_PEEK"; + public static final String ACTION_GROUP_MESSAGE_SENT_ERROR = "GROUP_MESSAGE_SENT_ERROR"; + public static final String ACTION_GROUP_APPROVE_SAFETY_CHANGE = "GROUP_APPROVE_SAFETY_CHANGE"; public static final int BUSY_TONE_LENGTH = 2000; @@ -436,7 +439,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer, state.getLocalDeviceState().isMicrophoneEnabled(), state.getCallSetupState().isRemoteVideoOffer(), state.getCallInfoState().getCallConnectedTime(), - state.getCallInfoState().getRemoteCallParticipants())); + state.getCallInfoState().getRemoteCallParticipants(), + state.getCallInfoState().getIdentityChangedRecipients())); } private @NonNull ListenableFutureTask sendMessage(@NonNull final RemotePeer remotePeer, @@ -669,7 +673,36 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } public void sendOpaqueCallMessage(@NonNull UUID uuid, @NonNull SignalServiceCallMessage opaqueMessage) { - sendMessage(new RemotePeer(RecipientId.from(uuid, null)), opaqueMessage); + RecipientId recipientId = RecipientId.from(uuid, null); + ListenableFutureTask listenableFutureTask = sendMessage(new RemotePeer(recipientId), opaqueMessage); + listenableFutureTask.addListener(new FutureTaskListener() { + @Override + public void onSuccess(Boolean result) { + // intentionally left blank + } + + @Override + public void onFailure(ExecutionException exception) { + Throwable error = exception.getCause(); + + Log.i(TAG, "sendOpaqueCallMessage onFailure: ", error); + + Intent intent = new Intent(WebRtcCallService.this, WebRtcCallService.class); + intent.setAction(ACTION_GROUP_MESSAGE_SENT_ERROR); + + WebRtcViewModel.State state = WebRtcViewModel.State.NETWORK_FAILURE; + + if (error instanceof UntrustedIdentityException) { + intent.putExtra(EXTRA_ERROR_IDENTITY_KEY, new IdentityKeyParcelable(((UntrustedIdentityException) error).getIdentityKey())); + state = WebRtcViewModel.State.UNTRUSTED_IDENTITY; + } + + intent.putExtra(EXTRA_ERROR_CALL_STATE, state); + intent.putExtra(EXTRA_REMOTE_PEER, new RemotePeer(recipientId)); + + startService(intent); + } + }); } public void sendGroupCallMessage(@NonNull Recipient recipient, @Nullable String groupCallEraId) { @@ -739,13 +772,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } public void updateGroupCallUpdateMessage(@NonNull RecipientId groupId, @Nullable String groupCallEraId, @NonNull Collection joinedMembers, boolean isCallFull) { - DatabaseFactory.getSmsDatabase(this).insertOrUpdateGroupCall(groupId, - Recipient.self().getId(), - System.currentTimeMillis(), - null, - groupCallEraId, - joinedMembers, - isCallFull); + SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getSmsDatabase(this).insertOrUpdateGroupCall(groupId, + Recipient.self().getId(), + System.currentTimeMillis(), + null, + groupCallEraId, + joinedMembers, + isCallFull)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java index fb1b8a943..9b1cd3af8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java @@ -9,16 +9,21 @@ import com.annimon.stream.Stream; 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; import org.thoughtcrime.securesms.groups.GroupManager; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.ringrtc.RemotePeer; 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 org.whispersystems.signalservice.api.messages.calls.OpaqueMessage; import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; @@ -201,6 +206,47 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor { return currentState; } + @Override + protected @NonNull WebRtcServiceState handleGroupMessageSentError(@NonNull WebRtcServiceState currentState, + @NonNull RemotePeer remotePeer, + @NonNull WebRtcViewModel.State errorCallState, + @NonNull Optional identityKey) + { + Log.w(tag, "handleGroupMessageSentError(): error: " + errorCallState); + + if (errorCallState == WebRtcViewModel.State.UNTRUSTED_IDENTITY) { + return currentState.builder() + .changeCallInfoState() + .addIdentityChangedRecipient(remotePeer.getId()) + .build(); + } + + return currentState; + } + + protected @NonNull WebRtcServiceState handleGroupApproveSafetyNumberChange(@NonNull WebRtcServiceState currentState, + @NonNull List recipientIds) + { + Log.i(tag, "handleGroupApproveSafetyNumberChange():"); + + GroupCall groupCall = currentState.getCallInfoState().getGroupCall(); + + if (groupCall != null) { + currentState = currentState.builder() + .changeCallInfoState() + .removeIdentityChangedRecipients(recipientIds) + .build(); + + try { + groupCall.resendMediaKeys(); + } catch (CallException e) { + return groupCallFailure(currentState, "Unable to resend media keys", e); + } + } + + return currentState; + } + @Override protected @NonNull WebRtcServiceState handleGroupCallEnded(@NonNull WebRtcServiceState currentState, int groupCallHash, @NonNull GroupCall.GroupCallEndReason groupCallEndReason) { Log.i(tag, "handleGroupCallEnded(): reason: " + groupCallEndReason); @@ -269,6 +315,8 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor { WebRtcVideoUtil.deinitializeVideo(currentState); + GroupCallSafetyNumberChangeNotificationUtil.cancelNotification(context, currentState.getCallInfoState().getCallRecipient()); + return new WebRtcServiceState(new IdleActionProcessor(webRtcInteractor)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java index 31369d92f..b69f699c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.ringrtc.CallState; import org.thoughtcrime.securesms.ringrtc.CameraState; @@ -60,11 +61,13 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_ import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_SIGNALING_FAILURE; import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_TIMEOUT; import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_FLIP_CAMERA; +import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_APPROVE_SAFETY_CHANGE; import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_CALL_ENDED; import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_CALL_PEEK; import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_CALL_UPDATE_MESSAGE; import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED; import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_LOCAL_DEVICE_STATE_CHANGED; +import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_MESSAGE_SENT_ERROR; import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_REMOTE_DEVICE_STATE_CHANGED; import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF; import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_REQUEST_UPDATE_MEMBERS; @@ -108,6 +111,7 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ANSWER_ import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_BLUETOOTH; import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_IS_ALWAYS_TURN; import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_MUTE; +import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_RECIPIENT_IDS; import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_RESULT_RECEIVER; import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_SPEAKER; import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.AnswerMetadata; @@ -240,6 +244,8 @@ public abstract class WebRtcActionProcessor { case ACTION_GROUP_CALL_ENDED: return handleGroupCallEnded(currentState, getGroupCallHash(intent), getGroupCallEndReason(intent)); case ACTION_GROUP_CALL_UPDATE_MESSAGE: return handleGroupCallUpdateMessage(currentState, GroupCallUpdateMetadata.fromIntent(intent)); case ACTION_GROUP_CALL_PEEK: return handleGroupCallPeek(currentState, getRemotePeer(intent)); + case ACTION_GROUP_MESSAGE_SENT_ERROR: return handleGroupMessageSentError(currentState, getRemotePeer(intent), getErrorCallState(intent), getErrorIdentityKey(intent)); + case ACTION_GROUP_APPROVE_SAFETY_CHANGE: return handleGroupApproveSafetyNumberChange(currentState, RecipientId.fromSerializedList(intent.getStringExtra(EXTRA_RECIPIENT_IDS))); case ACTION_HTTP_SUCCESS: return handleHttpSuccess(currentState, HttpData.fromIntent(intent)); case ACTION_HTTP_FAILURE: return handleHttpFailure(currentState, HttpData.fromIntent(intent)); @@ -734,6 +740,22 @@ public abstract class WebRtcActionProcessor { return currentState; } + protected @NonNull WebRtcServiceState handleGroupMessageSentError(@NonNull WebRtcServiceState currentState, + @NonNull RemotePeer remotePeer, + @NonNull WebRtcViewModel.State errorCallState, + @NonNull Optional identityKey) + { + Log.i(tag, "handleGroupMessageSentError not processed"); + return currentState; + } + + protected @NonNull WebRtcServiceState handleGroupApproveSafetyNumberChange(@NonNull WebRtcServiceState currentState, + @NonNull List recipientIds) + { + Log.i(tag, "handleGroupApproveSafetyNumberChange not processed"); + return currentState; + } + //endregion protected @NonNull WebRtcServiceState handleHttpSuccess(@NonNull WebRtcServiceState currentState, @NonNull HttpData httpData) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java index 08be439ef..2a84a543d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.java @@ -8,15 +8,18 @@ import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.CallParticipantId; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; /** * General state of ongoing calls. @@ -31,13 +34,30 @@ public class CallInfoState { RemotePeer activePeer; GroupCall groupCall; WebRtcViewModel.GroupCallState groupState; + Set identityChangedRecipients; public CallInfoState() { - this(WebRtcViewModel.State.IDLE, Recipient.UNKNOWN, -1, Collections.emptyMap(), Collections.emptyMap(), null, null, WebRtcViewModel.GroupCallState.IDLE); + this(WebRtcViewModel.State.IDLE, + Recipient.UNKNOWN, + -1, + Collections.emptyMap(), + Collections.emptyMap(), + null, + null, + WebRtcViewModel.GroupCallState.IDLE, + Collections.emptySet()); } public CallInfoState(@NonNull CallInfoState toCopy) { - this(toCopy.callState, toCopy.callRecipient, toCopy.callConnectedTime, toCopy.remoteParticipants, toCopy.peerMap, toCopy.activePeer, toCopy.groupCall, toCopy.groupState); + this(toCopy.callState, + toCopy.callRecipient, + toCopy.callConnectedTime, + toCopy.remoteParticipants, + toCopy.peerMap, + toCopy.activePeer, + toCopy.groupCall, + toCopy.groupState, + toCopy.identityChangedRecipients); } public CallInfoState(@NonNull WebRtcViewModel.State callState, @@ -47,16 +67,18 @@ public class CallInfoState { @NonNull Map peerMap, @Nullable RemotePeer activePeer, @Nullable GroupCall groupCall, - @NonNull WebRtcViewModel.GroupCallState groupState) + @NonNull WebRtcViewModel.GroupCallState groupState, + @NonNull Set identityChangedRecipients) { - this.callState = callState; - this.callRecipient = callRecipient; - this.callConnectedTime = callConnectedTime; - this.remoteParticipants = new LinkedHashMap<>(remoteParticipants); - this.peerMap = new HashMap<>(peerMap); - this.activePeer = activePeer; - this.groupCall = groupCall; - this.groupState = groupState; + this.callState = callState; + this.callRecipient = callRecipient; + this.callConnectedTime = callConnectedTime; + this.remoteParticipants = new LinkedHashMap<>(remoteParticipants); + this.peerMap = new HashMap<>(peerMap); + this.activePeer = activePeer; + this.groupCall = groupCall; + this.groupState = groupState; + this.identityChangedRecipients = new HashSet<>(identityChangedRecipients); } public @NonNull Recipient getCallRecipient() { @@ -110,4 +132,8 @@ public class CallInfoState { public @NonNull WebRtcViewModel.GroupCallState getGroupCallState() { return groupState; } + + public @NonNull Set getIdentityChangedRecipients() { + return identityChangedRecipients; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java index 99ef75a81..311054874 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java @@ -9,12 +9,15 @@ import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.CallParticipantId; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.ringrtc.Camera; import org.thoughtcrime.securesms.ringrtc.CameraState; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.webrtc.WebRtcActionProcessor; import org.webrtc.EglBase; +import java.util.Collection; + /** * Builder that creates a new {@link WebRtcServiceState} from an existing one and allows * changes to all normally immutable data. @@ -243,5 +246,15 @@ public class WebRtcServiceStateBuilder { toBuild.groupState = groupState; return this; } + + public @NonNull CallInfoStateBuilder addIdentityChangedRecipient(@NonNull RecipientId id) { + toBuild.identityChangedRecipients.add(id); + return this; + } + + public @NonNull CallInfoStateBuilder removeIdentityChangedRecipients(@NonNull Collection ids) { + toBuild.identityChangedRecipients.removeAll(ids); + return this; + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b35e0d70d..d2097c2d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1797,6 +1797,8 @@ Declined on a linked device. Busy on a linked device. + Someone has joined this call with a safety number that has changed. + The safety number for your conversation with %1$s has changed. This could either mean that someone is trying to intercept your communication, or that %2$s simply re-installed Signal. You may wish to verify your safety number with this contact. @@ -1958,6 +1960,9 @@ Safety Number Changes Send anyway Call anyway + Join call + Continue call + Leave call The following people may have reinstalled or changed devices. Verify your safety number with them to ensure privacy. View Previous verified