diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index 9e4609ed66..fb1d2907f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -91,8 +91,11 @@ import org.thoughtcrime.securesms.components.webrtc.v2.CallPermissionsDialogCont import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.events.WebRtcViewModel; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity; import org.thoughtcrime.securesms.permissions.Permissions; +import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment; +import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent; import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -116,7 +119,6 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -127,7 +129,7 @@ import io.reactivex.rxjava3.disposables.Disposable; import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE; -public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback, ReactWithAnyEmojiBottomSheetDialogFragment.Callback { +public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback, ReactWithAnyEmojiBottomSheetDialogFragment.Callback, RecaptchaProofBottomSheetFragment.Callback { private static final String TAG = Log.tag(WebRtcCallActivity.class); @@ -263,6 +265,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan public void onResume() { Log.i(TAG, "onResume()"); super.onResume(); + EventBus.getDefault().register(this); + initializeScreenshotSecurity(); if (!EventBus.getDefault().isRegistered(this)) { @@ -287,6 +291,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan enterPipOnResume = false; enterPipModeIfPossible(); } + + if (SignalStore.rateLimit().needsRecaptcha()) { + RecaptchaProofBottomSheetFragment.show(getSupportFragmentManager()); + } } @Override @@ -303,6 +311,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan Log.i(TAG, "onPause"); super.onPause(); + EventBus.getDefault().unregister(this); + if (!callPermissionsDialogController.isAskingForPermission() && !viewModel.isCallStarting() && !isChangingConfigurations()) { CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) { @@ -345,6 +355,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan EventBus.getDefault().unregister(this); } + @Subscribe(threadMode = ThreadMode.MAIN) + public void onRecaptchaRequiredEvent(RecaptchaRequiredEvent recaptchaRequiredEvent) { + RecaptchaProofBottomSheetFragment.show(getSupportFragmentManager()); + } + @SuppressLint("MissingSuperCall") @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { @@ -1071,6 +1086,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan callOverflowPopupWindow.dismiss(); } + @Override + public void onProofCompleted() { + AppDependencies.getSignalCallManager().resendMediaKeys(); + } + private final class ControlsListener implements WebRtcCallView.ControlsListener { @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java index 9fa17f3887..609861a6d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java @@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMessage; +import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -215,7 +216,12 @@ public class IndividualSendJob extends PushSendJob { database.markAsSentFailed(messageId); RetrieveProfileJob.enqueue(recipientId); } catch (ProofRequiredException e) { - handleProofRequiredException(context, e, SignalDatabase.threads().getRecipientForThreadId(threadId), threadId, messageId, true); + ProofRequiredExceptionHandler.Result result = ProofRequiredExceptionHandler.handle(context, e, SignalDatabase.threads().getRecipientForThreadId(threadId), threadId, messageId); + if (result.isRetry()) { + throw new RetryLaterException(); + } else { + throw e; + } } SignalLocalMetrics.IndividualMessageSend.onJobFinished(messageId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java index ccfb593321..70eb94ff6f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.messages.GroupSendUtil; import org.thoughtcrime.securesms.net.NotPushRegisteredException; +import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -22,6 +23,7 @@ import org.whispersystems.signalservice.api.crypto.ContentHint; 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.push.exceptions.ProofRequiredException; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; import java.io.IOException; @@ -170,7 +172,8 @@ public class ProfileKeySendJob extends BaseJob { .withTimestamp(System.currentTimeMillis()) .withProfileKey(Recipient.self().resolve().getProfileKey()); - List results = GroupSendUtil.sendUnresendableDataMessage(context, null, destinations, false, ContentHint.IMPLICIT, dataMessage.build(), false); + List results = GroupSendUtil.sendUnresendableDataMessage(context, null, destinations, false, ContentHint.IMPLICIT, dataMessage.build(), false); + ProofRequiredException proofRequired = Stream.of(results).filter(r -> r.getProofRequiredFailure() != null).findLast().map(SendMessageResult::getProofRequiredFailure).orElse(null); GroupSendJobHelper.SendResult groupResult = GroupSendJobHelper.getCompletedSends(destinations, results); @@ -178,6 +181,11 @@ public class ProfileKeySendJob extends BaseJob { SignalDatabase.recipients().markUnregistered(unregistered); } + if (proofRequired != null) { + Log.d(TAG, "Notifying the user they were rate limited."); + ProofRequiredExceptionHandler.handle(context, proofRequired, null, -1L, -1L); + } + return groupResult.completed; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index f6981d9970..ccd169b7a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.messages.StorySendUtil; import org.thoughtcrime.securesms.mms.MessageGroupContext; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMessage; +import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -456,7 +457,12 @@ public final class PushGroupSendJob extends PushSendJob { SignalDatabase.groupReceipts().setUnidentified(successUnidentifiedStatus, messageId); if (proofRequired != null) { - handleProofRequiredException(context, proofRequired, groupRecipient, threadId, messageId, true); + ProofRequiredExceptionHandler.Result result = ProofRequiredExceptionHandler.handle(context, proofRequired, groupRecipient, threadId, messageId); + if (result.isRetry()) { + throw new RetryLaterException(); + } else { + throw proofRequired; + } } if (existingNetworkFailures.isEmpty() && existingIdentityMismatches.isEmpty()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index b5c8e5ad8f..1a32053aeb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -575,84 +575,6 @@ public abstract class PushSendJob extends SendJob { } } - protected static void handleProofRequiredException(@NonNull Context context, @NonNull ProofRequiredException proofRequired, @Nullable Recipient recipient, long threadId, long messageId, boolean isMms) - throws ProofRequiredException, RetryLaterException - { - Log.w(TAG, "[Proof Required] Options: " + proofRequired.getOptions()); - - try { - if (proofRequired.getOptions().contains(ProofRequiredException.Option.PUSH_CHALLENGE)) { - AppDependencies.getSignalServiceAccountManager().requestRateLimitPushChallenge(); - Log.i(TAG, "[Proof Required] Successfully requested a challenge. Waiting up to " + PUSH_CHALLENGE_TIMEOUT + " ms."); - - boolean success = new PushChallengeRequest(PUSH_CHALLENGE_TIMEOUT).blockUntilSuccess(); - - if (success) { - Log.i(TAG, "Successfully responded to a push challenge. Retrying message send."); - throw new RetryLaterException(1); - } else { - Log.w(TAG, "Failed to respond to the push challenge in time. Falling back."); - } - } - } catch (NonSuccessfulResponseCodeException e) { - Log.w(TAG, "[Proof Required] Could not request a push challenge (" + e.getCode() + "). Falling back.", e); - } catch (IOException e) { - Log.w(TAG, "[Proof Required] Network error when requesting push challenge. Retrying later."); - throw new RetryLaterException(e); - } - - Log.w(TAG, "[Proof Required] Marking message as rate-limited. (id: " + messageId + ", mms: " + isMms + ", thread: " + threadId + ")"); - if (isMms) { - SignalDatabase.messages().markAsRateLimited(messageId); - } else { - SignalDatabase.messages().markAsRateLimited(messageId); - } - - if (proofRequired.getOptions().contains(ProofRequiredException.Option.CAPTCHA)) { - Log.i(TAG, "[Proof Required] CAPTCHA required."); - SignalStore.rateLimit().markNeedsRecaptcha(proofRequired.getToken()); - - if (recipient != null) { - ParentStoryId.GroupReply groupReply = SignalDatabase.messages().getParentStoryIdForGroupReply(messageId); - AppDependencies.getMessageNotifier().notifyProofRequired(context, recipient, ConversationId.fromThreadAndReply(threadId, groupReply)); - } else { - Log.w(TAG, "[Proof Required] No recipient! Couldn't notify."); - } - } - - throw proofRequired; - } - protected abstract void onPushSend() throws Exception; - public static class PushChallengeRequest { - private final long timeout; - private final CountDownLatch latch; - private final EventBus eventBus; - - private PushChallengeRequest(long timeout) { - this.timeout = timeout; - this.latch = new CountDownLatch(1); - this.eventBus = EventBus.getDefault(); - } - - public boolean blockUntilSuccess() { - eventBus.register(this); - - try { - return latch.await(timeout, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - Log.w(TAG, "[Proof Required] Interrupted?", e); - return false; - } finally { - eventBus.unregister(this); - } - } - - @Subscribe(threadMode = ThreadMode.POSTING) - public void onSuccessReceived(SubmitRateLimitPushChallengeJob.SuccessEvent event) { - Log.i(TAG, "[Proof Required] Received a successful result!"); - latch.countDown(); - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ratelimit/ProofRequiredExceptionHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/ratelimit/ProofRequiredExceptionHandler.kt new file mode 100644 index 0000000000..2e97681ced --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ratelimit/ProofRequiredExceptionHandler.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.ratelimit + +import android.content.Context +import androidx.annotation.WorkerThread +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.model.ParentStoryId +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobs.SubmitRateLimitPushChallengeJob.SuccessEvent +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.notifications.v2.ConversationId +import org.thoughtcrime.securesms.recipients.Recipient +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException +import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException +import java.io.IOException +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * Reusable ProofRequiredException handling code. + */ +object ProofRequiredExceptionHandler { + + private val TAG = Log.tag(ProofRequiredExceptionHandler::class) + private val PUSH_CHALLENGE_TIMEOUT: Duration = 10.seconds + + /** + * Handles the given exception, updating state as necessary. + */ + @JvmStatic + @WorkerThread + fun handle(context: Context, proofRequired: ProofRequiredException, recipient: Recipient?, threadId: Long, messageId: Long): Result { + Log.w(TAG, "[Proof Required] Options: ${proofRequired.options}") + + try { + if (ProofRequiredException.Option.PUSH_CHALLENGE in proofRequired.options) { + AppDependencies.signalServiceAccountManager.requestRateLimitPushChallenge() + Log.i(TAG, "[Proof Required] Successfully requested a challenge. Waiting up to $PUSH_CHALLENGE_TIMEOUT ms.") + + val success = PushChallengeRequest(PUSH_CHALLENGE_TIMEOUT).blockUntilSuccess() + + if (success) { + Log.i(TAG, "Successfully responded to a push challenge. Retrying message send.") + return Result.RETRY_NOW + } else { + Log.w(TAG, "Failed to respond to the push challeng in time. Falling back.") + } + } + } catch (e: NonSuccessfulResponseCodeException) { + Log.w(TAG, "[Proof Required] Could not request a push challenge (${e.code}). Falling back.", e) + } catch (e: IOException) { + Log.w(TAG, "[Proof Required] Network error when requesting push challenge. Retrying later.") + return Result.RETRY_LATER + } + + if (messageId > 0) { + Log.w(TAG, "[Proof Required] Marking message as rate-limited. (id: $messageId, thread: $threadId)") + SignalDatabase.messages.markAsRateLimited(messageId) + } + + if (ProofRequiredException.Option.CAPTCHA in proofRequired.options) { + Log.i(TAG, "[Proof Required] CAPTCHA required.") + SignalStore.rateLimit.markNeedsRecaptcha(proofRequired.token) + + if (recipient != null && messageId > -1L) { + val groupReply: ParentStoryId.GroupReply? = SignalDatabase.messages.getParentStoryIdForGroupReply(messageId) + AppDependencies.messageNotifier.notifyProofRequired(context, recipient, ConversationId.fromThreadAndReply(threadId, groupReply)) + } else { + Log.w(TAG, "[Proof Required] No recipient! Couldn't notify.") + } + } + + return Result.RETHROW + } + + enum class Result { + /** + * The challenge was successful and the message send can be retried immediately. + */ + RETRY_NOW, + + /** + * The challenge failed due to a network error and should be scheduled to retry with some offset. + */ + RETRY_LATER, + + /** + * The caller should rethrow the original error. + */ + RETHROW; + + fun isRetry() = this != RETHROW + } + + private class PushChallengeRequest(val timeout: Duration) { + private val latch = CountDownLatch(1) + private val eventBus = EventBus.getDefault() + + fun blockUntilSuccess(): Boolean { + eventBus.register(this) + + return try { + latch.await(timeout.inWholeMilliseconds, TimeUnit.MILLISECONDS) + } catch (e: InterruptedException) { + Log.w(TAG, "[Proof Required] Interrupted?", e) + false + } finally { + eventBus.unregister(this) + } + } + + @Subscribe(threadMode = ThreadMode.POSTING) + fun onSuccessReceived(event: SuccessEvent) { + Log.i(TAG, "[Proof Required] Received a successful result!") + latch.countDown() + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ratelimit/RecaptchaProofActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ratelimit/RecaptchaProofActivity.java index 7c1bcd6508..8ca2d37c97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ratelimit/RecaptchaProofActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ratelimit/RecaptchaProofActivity.java @@ -10,8 +10,11 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Toast; +import androidx.activity.result.contract.ActivityResultContract; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import org.signal.core.util.concurrent.SimpleTask; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.PassphraseRequiredActivity; @@ -20,7 +23,6 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.Util; -import org.signal.core.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; @@ -36,10 +38,6 @@ public class RecaptchaProofActivity extends PassphraseRequiredActivity { private final DynamicTheme dynamicTheme = new DynamicTheme(); - public static @NonNull Intent getIntent(@NonNull Context context) { - return new Intent(context, RecaptchaProofActivity.class); - } - @Override protected void onPreCreate() { dynamicTheme.onCreate(this); @@ -120,6 +118,7 @@ public class RecaptchaProofActivity extends PassphraseRequiredActivity { if (result.clearState) { Log.i(TAG, "Considering the response sufficient to clear the slate."); SignalStore.rateLimit().onProofAccepted(); + setResult(RESULT_OK); } if (!result.success) { @@ -140,4 +139,17 @@ public class RecaptchaProofActivity extends PassphraseRequiredActivity { this.success = success; } } + + public static class RecaptchaProofContract extends ActivityResultContract { + + @Override + public @NonNull Intent createIntent(@NonNull Context context, Void unused) { + return new Intent(context, RecaptchaProofActivity.class); + } + + @Override + public Boolean parseResult(int resultCode, @Nullable Intent intent) { + return resultCode == RESULT_OK; + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ratelimit/RecaptchaProofBottomSheetFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ratelimit/RecaptchaProofBottomSheetFragment.java index a87d1eaf44..a58012b758 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ratelimit/RecaptchaProofBottomSheetFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ratelimit/RecaptchaProofBottomSheetFragment.java @@ -1,10 +1,12 @@ package org.thoughtcrime.securesms.ratelimit; +import android.app.Activity; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; @@ -24,6 +26,8 @@ public final class RecaptchaProofBottomSheetFragment extends BottomSheetDialogFr private static final String TAG = Log.tag(RecaptchaProofBottomSheetFragment.class); + private ActivityResultLauncher launcher; + public static void show(@NonNull FragmentManager manager) { new RecaptchaProofBottomSheetFragment().show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); } @@ -38,11 +42,25 @@ public final class RecaptchaProofBottomSheetFragment extends BottomSheetDialogFr public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.recaptcha_required_bottom_sheet, container, false); - view.findViewById(R.id.recaptcha_sheet_ok_button).setOnClickListener(v -> { + Activity activity = requireActivity(); + final Callback callback; + + if (activity instanceof Callback) { + callback = (Callback) activity; + } else { + callback = null; + } + + launcher = registerForActivityResult(new RecaptchaProofActivity.RecaptchaProofContract(), (isOk) -> { + if (isOk && callback != null) { + callback.onProofCompleted(); + } + dismissAllowingStateLoss(); - startActivity(RecaptchaProofActivity.getIntent(requireContext())); }); + view.findViewById(R.id.recaptcha_sheet_ok_button).setOnClickListener(v -> launcher.launch(null)); + return view; } @@ -62,4 +80,12 @@ public final class RecaptchaProofBottomSheetFragment extends BottomSheetDialogFr Log.i(TAG, "Ignoring repeat show."); } } + + /** + * Optional callback interface to be invoked when the user successfully completes a push challenge. + * This is expected to be implemented on the activity which is displaying this fragment. + */ + public interface Callback { + void onProofCompleted(); + } } 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 ce7e9d4d6f..dcfca8f826 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 @@ -331,4 +331,18 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor { return terminateGroupCall(currentState); } + + @Override + protected @NonNull WebRtcServiceState handleResendMediaKeys(@NonNull WebRtcServiceState currentState) { + GroupCall groupCall = currentState.getCallInfoState().getGroupCall(); + if (groupCall != null) { + try { + currentState.getCallInfoState().getGroupCall().resendMediaKeys(); + } catch (CallException e) { + return groupCallFailure(currentState, "Unable to resend media keys", e); + } + } + + return currentState; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index 6ba3c7ba49..0bf8150208 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -14,7 +14,6 @@ import androidx.annotation.Nullable; import com.annimon.stream.Stream; import org.greenrobot.eventbus.EventBus; -import org.signal.core.util.ListUtil; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.util.Pair; @@ -56,6 +55,7 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.messages.GroupSendUtil; import org.thoughtcrime.securesms.notifications.v2.ConversationId; +import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -85,6 +85,7 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.ServiceId.ACI; +import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.internal.push.SyncMessage; @@ -801,6 +802,11 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. Log.i(TAG, "onSendCallMessage onFailure: ", e); RetrieveProfileJob.enqueue(recipient.getId()); process((s, p) -> p.handleGroupMessageSentError(s, Collections.singletonList(recipient.getId()), UNTRUSTED_IDENTITY)); + } catch (ProofRequiredException e) { + Log.i(TAG, "onSendCallMessage onFailure: ", e); + ProofRequiredExceptionHandler.handle(context, e, recipient, -1L, -1L); + process((s, p) -> p.handleResendMediaKeys(s)); + process((s, p) -> p.handleGroupMessageSentError(s, Collections.singletonList(recipient.getId()), NETWORK_FAILURE)); } catch (IOException e) { Log.i(TAG, "onSendCallMessage onFailure: ", e); process((s, p) -> p.handleGroupMessageSentError(s, Collections.singletonList(recipient.getId()), NETWORK_FAILURE)); @@ -1147,6 +1153,10 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. isCallFull)); } + public void resendMediaKeys() { + process((s, p) -> p.handleResendMediaKeys(s)); + } + public void sendCallMessage(@NonNull final RemotePeer remotePeer, @NonNull final SignalServiceCallMessage callMessage) { @@ -1170,6 +1180,11 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. UNTRUSTED_IDENTITY, Optional.ofNullable(e.getIdentityKey()))); } catch (IOException e) { + if (e instanceof ProofRequiredException) { + ProofRequiredExceptionHandler.handle(context, (ProofRequiredException) e, null, -1L, -1L); + process((s, p) -> p.handleResendMediaKeys(s)); + } + processSendMessageFailureWithChangeDetection(remotePeer, (s, p) -> p.handleMessageSentError(s, remotePeer.getCallId(), 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 18ee543951..a9499e1912 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 @@ -793,6 +793,11 @@ public abstract class WebRtcActionProcessor { return currentState; } + protected @NonNull WebRtcServiceState handleResendMediaKeys(@NonNull WebRtcServiceState currentState) { + Log.i(tag, "handleResendMediaKeys not processed"); + return currentState; + } + protected @NonNull WebRtcServiceState handleReceivedOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.OpaqueMessageMetadata opaqueMessageMetadata) { Log.i(tag, "handleReceivedOpaqueMessage():");