Ensure rate limit dialog appears during calls.

This commit is contained in:
Alex Hart 2024-10-23 14:15:42 -03:00 committed by GitHub
parent 6673293e29
commit 9fa04e03fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 253 additions and 91 deletions

View file

@ -91,8 +91,11 @@ import org.thoughtcrime.securesms.components.webrtc.v2.CallPermissionsDialogCont
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog; import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity; import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
import org.thoughtcrime.securesms.permissions.Permissions; 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.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; 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.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; 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; 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); private static final String TAG = Log.tag(WebRtcCallActivity.class);
@ -263,6 +265,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
public void onResume() { public void onResume() {
Log.i(TAG, "onResume()"); Log.i(TAG, "onResume()");
super.onResume(); super.onResume();
EventBus.getDefault().register(this);
initializeScreenshotSecurity(); initializeScreenshotSecurity();
if (!EventBus.getDefault().isRegistered(this)) { if (!EventBus.getDefault().isRegistered(this)) {
@ -287,6 +291,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
enterPipOnResume = false; enterPipOnResume = false;
enterPipModeIfPossible(); enterPipModeIfPossible();
} }
if (SignalStore.rateLimit().needsRecaptcha()) {
RecaptchaProofBottomSheetFragment.show(getSupportFragmentManager());
}
} }
@Override @Override
@ -303,6 +311,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
Log.i(TAG, "onPause"); Log.i(TAG, "onPause");
super.onPause(); super.onPause();
EventBus.getDefault().unregister(this);
if (!callPermissionsDialogController.isAskingForPermission() && !viewModel.isCallStarting() && !isChangingConfigurations()) { if (!callPermissionsDialogController.isAskingForPermission() && !viewModel.isCallStarting() && !isChangingConfigurations()) {
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) { if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
@ -345,6 +355,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
EventBus.getDefault().unregister(this); EventBus.getDefault().unregister(this);
} }
@Subscribe(threadMode = ThreadMode.MAIN)
public void onRecaptchaRequiredEvent(RecaptchaRequiredEvent recaptchaRequiredEvent) {
RecaptchaProofBottomSheetFragment.show(getSupportFragmentManager());
}
@SuppressLint("MissingSuperCall") @SuppressLint("MissingSuperCall")
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
@ -1071,6 +1086,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
callOverflowPopupWindow.dismiss(); callOverflowPopupWindow.dismiss();
} }
@Override
public void onProofCompleted() {
AppDependencies.getSignalCallManager().resendMediaKeys();
}
private final class ControlsListener implements WebRtcCallView.ControlsListener { private final class ControlsListener implements WebRtcCallView.ControlsListener {
@Override @Override

View file

@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMessage; import org.thoughtcrime.securesms.mms.OutgoingMessage;
import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
@ -215,7 +216,12 @@ public class IndividualSendJob extends PushSendJob {
database.markAsSentFailed(messageId); database.markAsSentFailed(messageId);
RetrieveProfileJob.enqueue(recipientId); RetrieveProfileJob.enqueue(recipientId);
} catch (ProofRequiredException e) { } 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); SignalLocalMetrics.IndividualMessageSend.onJobFinished(messageId);

View file

@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.messages.GroupSendUtil; import org.thoughtcrime.securesms.messages.GroupSendUtil;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; 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.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
import java.io.IOException; import java.io.IOException;
@ -170,7 +172,8 @@ public class ProfileKeySendJob extends BaseJob {
.withTimestamp(System.currentTimeMillis()) .withTimestamp(System.currentTimeMillis())
.withProfileKey(Recipient.self().resolve().getProfileKey()); .withProfileKey(Recipient.self().resolve().getProfileKey());
List<SendMessageResult> results = GroupSendUtil.sendUnresendableDataMessage(context, null, destinations, false, ContentHint.IMPLICIT, dataMessage.build(), false); List<SendMessageResult> 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); GroupSendJobHelper.SendResult groupResult = GroupSendJobHelper.getCompletedSends(destinations, results);
@ -178,6 +181,11 @@ public class ProfileKeySendJob extends BaseJob {
SignalDatabase.recipients().markUnregistered(unregistered); 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; return groupResult.completed;
} }

View file

@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.messages.StorySendUtil;
import org.thoughtcrime.securesms.mms.MessageGroupContext; import org.thoughtcrime.securesms.mms.MessageGroupContext;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMessage; import org.thoughtcrime.securesms.mms.OutgoingMessage;
import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
@ -456,7 +457,12 @@ public final class PushGroupSendJob extends PushSendJob {
SignalDatabase.groupReceipts().setUnidentified(successUnidentifiedStatus, messageId); SignalDatabase.groupReceipts().setUnidentified(successUnidentifiedStatus, messageId);
if (proofRequired != null) { 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()) { if (existingNetworkFailures.isEmpty() && existingIdentityMismatches.isEmpty()) {

View file

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

View file

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

View file

@ -10,8 +10,11 @@ import android.webkit.WebView;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
import android.widget.Toast; import android.widget.Toast;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.PassphraseRequiredActivity; 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.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.signal.core.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
@ -36,10 +38,6 @@ public class RecaptchaProofActivity extends PassphraseRequiredActivity {
private final DynamicTheme dynamicTheme = new DynamicTheme(); private final DynamicTheme dynamicTheme = new DynamicTheme();
public static @NonNull Intent getIntent(@NonNull Context context) {
return new Intent(context, RecaptchaProofActivity.class);
}
@Override @Override
protected void onPreCreate() { protected void onPreCreate() {
dynamicTheme.onCreate(this); dynamicTheme.onCreate(this);
@ -120,6 +118,7 @@ public class RecaptchaProofActivity extends PassphraseRequiredActivity {
if (result.clearState) { if (result.clearState) {
Log.i(TAG, "Considering the response sufficient to clear the slate."); Log.i(TAG, "Considering the response sufficient to clear the slate.");
SignalStore.rateLimit().onProofAccepted(); SignalStore.rateLimit().onProofAccepted();
setResult(RESULT_OK);
} }
if (!result.success) { if (!result.success) {
@ -140,4 +139,17 @@ public class RecaptchaProofActivity extends PassphraseRequiredActivity {
this.success = success; this.success = success;
} }
} }
public static class RecaptchaProofContract extends ActivityResultContract<Void, Boolean> {
@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;
}
}
} }

View file

@ -1,10 +1,12 @@
package org.thoughtcrime.securesms.ratelimit; package org.thoughtcrime.securesms.ratelimit;
import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment; 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 static final String TAG = Log.tag(RecaptchaProofBottomSheetFragment.class);
private ActivityResultLauncher<Void> launcher;
public static void show(@NonNull FragmentManager manager) { public static void show(@NonNull FragmentManager manager) {
new RecaptchaProofBottomSheetFragment().show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); 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) { 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 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(); dismissAllowingStateLoss();
startActivity(RecaptchaProofActivity.getIntent(requireContext()));
}); });
view.findViewById(R.id.recaptcha_sheet_ok_button).setOnClickListener(v -> launcher.launch(null));
return view; return view;
} }
@ -62,4 +80,12 @@ public final class RecaptchaProofBottomSheetFragment extends BottomSheetDialogFr
Log.i(TAG, "Ignoring repeat show."); 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();
}
} }

View file

@ -331,4 +331,18 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
return terminateGroupCall(currentState); 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;
}
} }

View file

@ -14,7 +14,6 @@ import androidx.annotation.Nullable;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.ListUtil;
import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.util.Pair; 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.keyvalue.SignalStore;
import org.thoughtcrime.securesms.messages.GroupSendUtil; import org.thoughtcrime.securesms.messages.GroupSendUtil;
import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.notifications.v2.ConversationId;
import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; 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.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.ServiceId.ACI; 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.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.internal.push.SyncMessage; 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); Log.i(TAG, "onSendCallMessage onFailure: ", e);
RetrieveProfileJob.enqueue(recipient.getId()); RetrieveProfileJob.enqueue(recipient.getId());
process((s, p) -> p.handleGroupMessageSentError(s, Collections.singletonList(recipient.getId()), UNTRUSTED_IDENTITY)); 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) { } catch (IOException e) {
Log.i(TAG, "onSendCallMessage onFailure: ", e); Log.i(TAG, "onSendCallMessage onFailure: ", e);
process((s, p) -> p.handleGroupMessageSentError(s, Collections.singletonList(recipient.getId()), NETWORK_FAILURE)); 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)); isCallFull));
} }
public void resendMediaKeys() {
process((s, p) -> p.handleResendMediaKeys(s));
}
public void sendCallMessage(@NonNull final RemotePeer remotePeer, public void sendCallMessage(@NonNull final RemotePeer remotePeer,
@NonNull final SignalServiceCallMessage callMessage) @NonNull final SignalServiceCallMessage callMessage)
{ {
@ -1170,6 +1180,11 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
UNTRUSTED_IDENTITY, UNTRUSTED_IDENTITY,
Optional.ofNullable(e.getIdentityKey()))); Optional.ofNullable(e.getIdentityKey())));
} catch (IOException e) { } catch (IOException e) {
if (e instanceof ProofRequiredException) {
ProofRequiredExceptionHandler.handle(context, (ProofRequiredException) e, null, -1L, -1L);
process((s, p) -> p.handleResendMediaKeys(s));
}
processSendMessageFailureWithChangeDetection(remotePeer, processSendMessageFailureWithChangeDetection(remotePeer,
(s, p) -> p.handleMessageSentError(s, (s, p) -> p.handleMessageSentError(s,
remotePeer.getCallId(), remotePeer.getCallId(),

View file

@ -793,6 +793,11 @@ public abstract class WebRtcActionProcessor {
return currentState; 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) { protected @NonNull WebRtcServiceState handleReceivedOpaqueMessage(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.OpaqueMessageMetadata opaqueMessageMetadata) {
Log.i(tag, "handleReceivedOpaqueMessage():"); Log.i(tag, "handleReceivedOpaqueMessage():");