Add UX for handling CDS rate limits.

main
Greyson Parrelli 2022-11-10 10:51:21 -05:00
rodzic 8eb3a1906e
commit c563ef27da
21 zmienionych plików z 570 dodań i 18 usunięć

Wyświetl plik

@ -51,7 +51,9 @@ public class EmojiEditText extends AppCompatEditText {
}
});
EditTextExtensionsKt.setIncognitoKeyboardEnabled(this, TextSecurePreferences.isIncognitoKeyboardEnabled(context));
if (!isInEditMode()) {
EditTextExtensionsKt.setIncognitoKeyboardEnabled(this, TextSecurePreferences.isIncognitoKeyboardEnabled(context));
}
}
public void insertEmoji(String emoji) {

Wyświetl plik

@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.components.reminder
import android.content.Context
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import kotlin.time.Duration.Companion.days
/**
* Reminder shown when CDS is in a permanent error state, preventing us from doing a sync.
*/
class CdsPermanentErrorReminder(context: Context) : Reminder(null, context.getString(R.string.reminder_cds_permanent_error_body)) {
init {
addAction(
Action(
context.getString(R.string.reminder_cds_permanent_error_learn_more),
R.id.reminder_action_cds_permanent_error_learn_more
)
)
}
override fun isDismissable(): Boolean {
return false
}
override fun getImportance(): Importance {
return Importance.ERROR
}
companion object {
/**
* Even if we're not truly "permanently blocked", if the time until we're unblocked is long enough, we'd rather show the permanent error message than
* telling the user to wait for 3 months or something.
*/
val PERMANENT_TIME_CUTOFF = 30.days.inWholeMilliseconds
@JvmStatic
fun isEligible(): Boolean {
val timeUntilUnblock = SignalStore.misc().cdsBlockedUtil - System.currentTimeMillis()
return SignalStore.misc().isCdsBlocked && timeUntilUnblock >= PERMANENT_TIME_CUTOFF
}
}
}

Wyświetl plik

@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.components.reminder
import android.content.Context
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**
* Reminder shown when CDS is rate-limited, preventing us from temporarily doing a refresh.
*/
class CdsTemporyErrorReminder(context: Context) : Reminder(null, context.getString(R.string.reminder_cds_warning_body)) {
init {
addAction(
Action(
context.getString(R.string.reminder_cds_warning_learn_more),
R.id.reminder_action_cds_temporary_error_learn_more
)
)
}
override fun isDismissable(): Boolean {
return false
}
override fun getImportance(): Importance {
return Importance.ERROR
}
companion object {
@JvmStatic
fun isEligible(): Boolean {
val timeUntilUnblock = SignalStore.misc().cdsBlockedUtil - System.currentTimeMillis()
return SignalStore.misc().isCdsBlocked && timeUntilUnblock < CdsPermanentErrorReminder.PERMANENT_TIME_CUTOFF
}
}
}

Wyświetl plik

@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.contacts.sync
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.FragmentManager
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
import org.thoughtcrime.securesms.databinding.CdsPermanentErrorBottomSheetBinding
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CommunicationActions
/**
* Bottom sheet shown when CDS is in a permanent error state, preventing us from doing a sync.
*/
class CdsPermanentErrorBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() {
private lateinit var binding: CdsPermanentErrorBottomSheetBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = CdsPermanentErrorBottomSheetBinding.inflate(inflater.cloneInContext(ContextThemeWrapper(inflater.context, themeResId)), container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.learnMoreButton.setOnClickListener {
CommunicationActions.openBrowserLink(requireContext(), "https://support.signal.org/hc/articles/360007319011#android_contacts_error")
}
binding.settingsButton.setOnClickListener {
val intent = Intent().apply {
action = Intent.ACTION_VIEW
data = android.provider.ContactsContract.Contacts.CONTENT_URI
}
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.CdsPermanentErrorBottomSheet_no_contacts_toast, Toast.LENGTH_SHORT).show()
}
}
}
companion object {
@JvmStatic
fun show(fragmentManager: FragmentManager) {
val fragment = CdsPermanentErrorBottomSheet()
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}

Wyświetl plik

@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.contacts.sync
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.FragmentManager
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
import org.thoughtcrime.securesms.databinding.CdsTemporaryErrorBottomSheetBinding
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import kotlin.time.Duration.Companion.milliseconds
/**
* Bottom sheet shown when CDS is rate-limited, preventing us from temporarily doing a refresh.
*/
class CdsTemporaryErrorBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() {
private lateinit var binding: CdsTemporaryErrorBottomSheetBinding
override val peekHeightPercentage: Float = 0.75f
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = CdsTemporaryErrorBottomSheetBinding.inflate(inflater.cloneInContext(ContextThemeWrapper(inflater.context, themeResId)), container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val days: Int = (SignalStore.misc().cdsBlockedUtil - System.currentTimeMillis()).milliseconds.inWholeDays.toInt()
binding.timeText.text = resources.getQuantityString(R.plurals.CdsTemporaryErrorBottomSheet_body1, days, days)
binding.learnMoreButton.setOnClickListener {
CommunicationActions.openBrowserLink(requireContext(), "https://support.signal.org/hc/articles/360007319011#android_contacts_error")
}
binding.settingsButton.setOnClickListener {
val intent = Intent().apply {
action = Intent.ACTION_VIEW
data = android.provider.ContactsContract.Contacts.CONTENT_URI
}
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.CdsPermanentErrorBottomSheet_no_contacts_toast, Toast.LENGTH_SHORT).show()
}
}
}
companion object {
@JvmStatic
fun show(fragmentManager: FragmentManager) {
val fragment = CdsTemporaryErrorBottomSheet()
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}

Wyświetl plik

@ -17,13 +17,17 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException
import org.whispersystems.signalservice.api.services.CdsiV2Service
import java.io.IOException
import java.util.Optional
import java.util.concurrent.Callable
import java.util.concurrent.Future
import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.seconds
/**
* Performs the CDS refresh using the V2 interface (either CDSH or CDSI) that returns both PNIs and ACIs.
@ -113,6 +117,12 @@ object ContactDiscoveryRefreshV2 {
return ContactDiscovery.RefreshResult(emptySet(), emptyMap())
}
if (newE164s.size > FeatureFlags.cdsHardLimit()) {
Log.w(TAG, "[$tag] Number of new contacts (${newE164s.size.roundedString()} > hard limit (${FeatureFlags.cdsHardLimit()}! Failing and marking ourselves as permanently blocked.")
SignalStore.misc().markCdsPermanentlyBlocked()
throw IOException("New contacts over the CDS hard limit!")
}
val token: ByteArray? = if (previousE164s.isNotEmpty() && !isPartialRefresh) SignalStore.misc().cdsToken else null
stopwatch.split("preamble")
@ -137,14 +147,22 @@ object ContactDiscoveryRefreshV2 {
}
stopwatch.split("cds-db")
}
} catch (e: NonSuccessfulResponseCodeException) {
if (e.code == 4101) {
Log.w(TAG, "Our token was invalid! Only thing we can do now is clear our local state :(")
SignalStore.misc().cdsToken = null
SignalDatabase.cds.clearAll()
}
} catch (e: CdsiResourceExhaustedException) {
Log.w(TAG, "CDS resource exhausted! Can try again in ${e.retryAfterSeconds} seconds.")
SignalStore.misc().cdsBlockedUtil = System.currentTimeMillis() + e.retryAfterSeconds.seconds.inWholeMilliseconds
throw e
} catch (e: CdsiInvalidTokenException) {
Log.w(TAG, "Our token was invalid! Only thing we can do now is clear our local state :(")
SignalStore.misc().cdsToken = null
SignalDatabase.cds.clearAll()
throw e
}
if (!isPartialRefresh && SignalStore.misc().isCdsBlocked) {
Log.i(TAG, "Successfully made a request while blocked -- clearing blocked state.")
SignalStore.misc().clearCdsBlocked()
}
Log.d(TAG, "[$tag] Used ${response.quotaUsedDebugOnly} quota.")
stopwatch.split("network-post-token")
@ -264,4 +282,9 @@ object ContactDiscoveryRefreshV2 {
}
.toSet()
}
private fun Int.roundedString(): String {
val nearestThousand = (this.toDouble() / 1000).roundToInt()
return "~${nearestThousand}k"
}
}

Wyświetl plik

@ -94,6 +94,8 @@ import org.thoughtcrime.securesms.components.menu.ActionItem;
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
import org.thoughtcrime.securesms.components.reminder.CdsPermanentErrorReminder;
import org.thoughtcrime.securesms.components.reminder.CdsTemporyErrorReminder;
import org.thoughtcrime.securesms.components.reminder.DozeReminder;
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
import org.thoughtcrime.securesms.components.reminder.OutdatedBuildReminder;
@ -106,6 +108,8 @@ import org.thoughtcrime.securesms.components.settings.app.notifications.manual.N
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView;
import org.thoughtcrime.securesms.contacts.sync.CdsPermanentErrorBottomSheet;
import org.thoughtcrime.securesms.contacts.sync.CdsTemporaryErrorBottomSheet;
import org.thoughtcrime.securesms.conversation.ConversationFragment;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
@ -616,6 +620,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
private void onReminderAction(@IdRes int reminderActionId) {
if (reminderActionId == R.id.reminder_action_update_now) {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
} else if (reminderActionId == R.id.reminder_action_cds_temporary_error_learn_more) {
CdsTemporaryErrorBottomSheet.show(getChildFragmentManager());
} else if (reminderActionId == R.id.reminder_action_cds_permanent_error_learn_more) {
CdsPermanentErrorBottomSheet.show(getChildFragmentManager());
}
}
@ -875,6 +883,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
return Optional.of((new PushRegistrationReminder(context)));
} else if (DozeReminder.isEligible(context)) {
return Optional.of(new DozeReminder(context));
} else if (CdsTemporyErrorReminder.isEligible()) {
return Optional.of(new CdsTemporyErrorReminder(context));
} else if (CdsPermanentErrorReminder.isEligible()) {
return Optional.of(new CdsPermanentErrorReminder(context));
} else {
return Optional.<Reminder>empty();
}

Wyświetl plik

@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNum
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
public final class MiscellaneousValues extends SignalStoreValues {
@ -23,6 +24,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
private static final String CENSORSHIP_SERVICE_REACHABLE = "misc.censorship.service_reachable";
private static final String LAST_GV2_PROFILE_CHECK_TIME = "misc.last_gv2_profile_check_time";
private static final String CDS_TOKEN = "misc.cds_token";
private static final String CDS_BLOCKED_UNTIL = "misc.cds_blocked_until";
private static final String LAST_FCM_FOREGROUND_TIME = "misc.last_fcm_foreground_time";
private static final String LAST_FOREGROUND_TIME = "misc.last_foreground_time";
private static final String PNI_INITIALIZED_DEVICES = "misc.pni_initialized_devices";
@ -166,6 +168,42 @@ public final class MiscellaneousValues extends SignalStoreValues {
.commit();
}
/**
* Marks the time at which we think the next CDS request will succeed. This should be taken from the service response.
*/
public void setCdsBlockedUtil(long time) {
putLong(CDS_BLOCKED_UNTIL, time);
}
/**
* Indicates that a CDS request will never succeed at the current contact count.
*/
public void markCdsPermanentlyBlocked() {
putLong(CDS_BLOCKED_UNTIL, Long.MAX_VALUE);
}
/**
* Clears any rate limiting state related to CDS.
*/
public void clearCdsBlocked() {
setCdsBlockedUtil(0);
}
/**
* Whether or not we expect the next CDS request to succeed.
*/
public boolean isCdsBlocked() {
return getCdsBlockedUtil() > 0;
}
/**
* This represents the next time we think we'll be able to make a successful CDS request. If it is before this time, we expect the request will fail
* (assuming the user still has the same number of new E164s).
*/
public long getCdsBlockedUtil() {
return getLong(CDS_BLOCKED_UNTIL, 0);
}
public long getLastFcmForegroundServiceTime() {
return getLong(LAST_FCM_FOREGROUND_TIME, 0);
}

Wyświetl plik

@ -25,8 +25,15 @@ public class DirectoryRefreshListener extends PersistentAlarmManagerListener {
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(true));
}
long interval = TimeUnit.SECONDS.toMillis(FeatureFlags.cdsRefreshIntervalSeconds());
long newTime = System.currentTimeMillis() + interval;
long newTime;
if (SignalStore.misc().isCdsBlocked()) {
newTime = Math.min(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(6),
SignalStore.misc().getCdsBlockedUtil());
} else {
newTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(FeatureFlags.cdsRefreshIntervalSeconds());
TextSecurePreferences.setDirectoryRefreshTime(context, newTime);
}
TextSecurePreferences.setDirectoryRefreshTime(context, newTime);

Wyświetl plik

@ -106,6 +106,7 @@ public final class FeatureFlags {
public static final String GOOGLE_PAY_DISABLED_REGIONS = "global.donations.gpayDisabledRegions";
public static final String CREDIT_CARD_DISABLED_REGIONS = "global.donations.ccDisabledRegions";
public static final String PAYPAL_DISABLED_REGIONS = "global.donations.paypalDisabledRegions";
private static final String CDS_HARD_LIMIT = "android.cds.hardLimit";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@ -163,7 +164,9 @@ public final class FeatureFlags {
KEEP_MUTED_CHATS_ARCHIVED,
GOOGLE_PAY_DISABLED_REGIONS,
CREDIT_CARD_DISABLED_REGIONS,
PAYPAL_DISABLED_REGIONS
PAYPAL_DISABLED_REGIONS,
KEEP_MUTED_CHATS_ARCHIVED,
CDS_HARD_LIMIT
);
@VisibleForTesting
@ -227,7 +230,8 @@ public final class FeatureFlags {
SMS_EXPORT_MEGAPHONE_DELAY_DAYS,
CREDIT_CARD_PAYMENTS,
PAYMENTS_REQUEST_ACTIVATE_FLOW,
KEEP_MUTED_CHATS_ARCHIVED
KEEP_MUTED_CHATS_ARCHIVED,
CDS_HARD_LIMIT
);
/**
@ -589,6 +593,13 @@ public final class FeatureFlags {
return getString(PAYPAL_DISABLED_REGIONS, "*");
}
/**
* If the user has more than this number of contacts, the CDS request will certainly be rejected, so we must fail.
*/
public static int cdsHardLimit() {
return getInteger(CDS_HARD_LIMIT, 50_000);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

Wyświetl plik

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,3.431C12.19,3.431 12.663,3.484 12.949,3.968L22.388,19.954C22.674,20.439 22.484,20.868 22.388,21.03C22.293,21.191 22.01,21.568 21.438,21.568H2.561C1.99,21.568 1.706,21.191 1.611,21.03C1.516,20.868 1.326,20.439 1.612,19.954L11.05,3.968C11.337,3.484 11.809,3.431 12,3.431ZM12,2C11.14,2 10.279,2.417 9.787,3.251L0.348,19.237C-0.639,20.909 0.591,23 2.561,23H21.439C23.409,23 24.639,20.908 23.652,19.237L14.213,3.25C13.721,2.416 12.861,2 12,2ZM11.949,16.875C11.142,16.888 10.5,17.541 10.514,18.332C10.528,19.122 11.193,19.752 12,19.739C12.807,19.725 13.448,19.073 13.435,18.282C13.421,17.491 12.755,16.861 11.949,16.875ZM11.267,14.541C11.267,14.937 11.594,15.259 12,15.259C12.405,15.259 12.733,14.938 12.733,14.541C12.733,14.537 12.731,14.534 12.731,14.531L13.095,8.651H13.084C13.084,8.064 12.599,7.588 12,7.588C11.401,7.588 10.915,8.064 10.915,8.651H10.905L11.269,14.531C11.269,14.531 11.267,14.537 11.267,14.541Z"
android:fillColor="#000000"/>
</vector>

Wyświetl plik

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingRight="28dp"
android:paddingLeft="28dp"
android:clipToPadding="false">
<ImageView
android:id="@+id/handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/bottom_sheet_handle" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_error_triangle_24" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="@string/CdsPermanentErrorBottomSheet_title"
android:textAppearance="@style/Signal.Text.TitleLarge" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textColor="@color/signal_colorOnSurfaceVariant"
android:text="@string/CdsPermanentErrorBottomSheet_body" />
<com.google.android.material.button.MaterialButton
android:id="@+id/learn_more_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:layout_marginLeft="-12dp"
android:text="@string/CdsPermanentErrorBottomSheet_learn_more"
style="@style/Widget.Signal.Button.TextButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/settings_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginBottom="28dp"
android:text="@string/CdsPermanentErrorBottomSheet_contacts_button"
style="@style/Signal.Widget.Button.Medium.Tonal" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

Wyświetl plik

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingRight="28dp"
android:paddingLeft="28dp">
<ImageView
android:id="@+id/handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/bottom_sheet_handle" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_error_triangle_24" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="@string/CdsTemporaryErrorBottomSheet_title"
android:textAppearance="@style/Signal.Text.TitleLarge" />
<TextView
android:id="@+id/time_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textColor="@color/signal_colorOnSurfaceVariant"
tools:text="@plurals/CdsTemporaryErrorBottomSheet_body1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textColor="@color/signal_colorOnSurfaceVariant"
android:text="@string/CdsTemporaryErrorBottomSheet_body2" />
<com.google.android.material.button.MaterialButton
android:id="@+id/learn_more_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:layout_marginLeft="-12dp"
android:text="@string/CdsPermanentErrorBottomSheet_learn_more"
style="@style/Widget.Signal.Button.TextButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/settings_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginBottom="28dp"
android:text="@string/CdsPermanentErrorBottomSheet_contacts_button"
style="@style/Signal.Widget.Button.Medium.Tonal" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

Wyświetl plik

@ -22,7 +22,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/section_header_action"
style="@style/Widget.Signal.Button.Small"
style="@style/Signal.Widget.Button.Large.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"

Wyświetl plik

@ -15,6 +15,9 @@
<item name="reminder_action_not_now" type="id" />
<item name="reminder_action_turn_off" type="id" />
<item name="reminder_action_cds_temporary_error_learn_more" type="id" />
<item name="reminder_action_cds_permanent_error_learn_more" type="id" />
<item name="status_bar_guideline" type="id" />
<item name="navigation_bar_guideline" type="id" />

Wyświetl plik

@ -3201,6 +3201,14 @@
<string name="reminder_header_push_text">Upgrade your communication experience.</string>
<string name="reminder_header_service_outage_text">Signal is experiencing technical difficulties. We are working hard to restore service as quickly as possible.</string>
<string name="reminder_header_progress">%1$d%%</string>
<!-- Body text of a banner that will show at the top of the chat list when we temporarily cannot process the user's contacts -->
<string name="reminder_cds_warning_body">Signal\'s private contact discovery temporarily can\'t process your phone\'s contacts.</string>
<!-- Label for a button in a banner to learn more about why we temporarily can't process the user's contacts -->
<string name="reminder_cds_warning_learn_more">Learn more</string>
<!-- Body text of a banner that will show at the top of the chat list when the user has so many contacts that we cannot ever process them -->
<string name="reminder_cds_permanent_error_body">Signal\'s private contact discovery can\'t process your phone\'s contacts.</string>
<!-- Label for a button in a banner to learn more about why we cannot process the user's contacts -->
<string name="reminder_cds_permanent_error_learn_more">Learn more</string>
<!-- media_preview -->
<string name="media_preview__save_title">Save</string>
@ -5523,6 +5531,31 @@
<!-- StripePaymentInProgressFragment -->
<string name="StripePaymentInProgressFragment__cancelling">Cancelling…</string>
<!-- The title of a bottom sheet dialog that tells the user we temporarily can't process their contacts. -->
<string name="CdsTemporaryErrorBottomSheet_title">Too many contacts have been processed</string>
<!-- The first part of the body text in a bottom sheet dialog that tells the user we temporarily can't process their contacts. The placeholder represents the number of days the user will have to wait until they can again. -->
<plurals name="CdsTemporaryErrorBottomSheet_body1">
<item quantity="one">Another attempt to process your contacts will be made within %1$d day.</item>
<item quantity="other">Another attempt to process your contacts will be made within %1$d days.</item>
</plurals>
<!-- The second part of the body text in a bottom sheet dialog that advises the user to remove contacts from their phone to fix the issue. -->
<string name="CdsTemporaryErrorBottomSheet_body2">To resolve this issue sooner, you can consider removing contacts or accounts on your phone that are syncing a lot of contacts.</string>
<!-- A button label in a bottom sheet that will navigate the user to their contacts settings. -->
<string name="CdsTemporaryErrorBottomSheet_contacts_button">Open contacts</string>
<!-- A toast that will be shown if we are unable to open the user's default contacts app. -->
<string name="CdsTemporaryErrorBottomSheet_no_contacts_toast">No contacts app found</string>
<!-- The title of a bottom sheet dialog that tells the user we can't process their contacts. -->
<string name="CdsPermanentErrorBottomSheet_title">Your contacts can\'t be processed</string>
<!-- The first part of the body text in a bottom sheet dialog that tells the user we can't process their contacts. -->
<string name="CdsPermanentErrorBottomSheet_body">The number of contacts on your phone exceeds the limit Signal can process. To find contacts on Signal, consider removing contacts or accounts on your phone that are syncing a lot of contacts.</string>
<!-- The first part of the body text in a bottom sheet dialog that tells the user we can't process their contacts. -->
<string name="CdsPermanentErrorBottomSheet_learn_more">Learn more</string>
<!-- A button label in a bottom sheet that will navigate the user to their contacts settings. -->
<string name="CdsPermanentErrorBottomSheet_contacts_button">Open contacts</string>
<!-- A toast that will be shown if we are unable to open the user's default contacts app. -->
<string name="CdsPermanentErrorBottomSheet_no_contacts_toast">No contacts app found</string>
<!-- EOF -->
</resources>

Wyświetl plik

@ -0,0 +1,16 @@
package org.whispersystems.signalservice.api.push.exceptions;
/**
* Indicates that something about our request was wrong. Could be:
* - Over 50k new contacts
* - Missing version byte prefix
* - Missing credentials
* - E164s are not a multiple of 8 bytes
* - Something else?
*/
public class CdsiInvalidArgumentException extends NonSuccessfulResponseCodeException {
public CdsiInvalidArgumentException() {
super(4003);
}
}

Wyświetl plik

@ -0,0 +1,11 @@
package org.whispersystems.signalservice.api.push.exceptions;
/**
* Indicates that you provided a bad token to CDSI.
*/
public class CdsiInvalidTokenException extends NonSuccessfulResponseCodeException {
public CdsiInvalidTokenException() {
super(4101);
}
}

Wyświetl plik

@ -0,0 +1,18 @@
package org.whispersystems.signalservice.api.push.exceptions;
/**
* A 4008 responses from CDSI indicating we've exhausted our quota.
*/
public class CdsiResourceExhaustedException extends NonSuccessfulResponseCodeException {
private final int retryAfterSeconds;
public CdsiResourceExhaustedException(int retryAfterSeconds) {
super(4008);
this.retryAfterSeconds = retryAfterSeconds;
}
public int getRetryAfterSeconds() {
return retryAfterSeconds;
}
}

Wyświetl plik

@ -8,7 +8,10 @@ import org.signal.libsignal.cds2.Cds2CommunicationFailureException;
import org.signal.libsignal.protocol.logging.Log;
import org.signal.libsignal.protocol.util.Pair;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException;
import org.whispersystems.signalservice.api.util.Tls12SocketFactory;
import org.whispersystems.signalservice.api.util.TlsProxySocketFactory;
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@ -22,9 +25,9 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
@ -90,6 +93,9 @@ final class CdsiSocket {
.addHeader("Authorization", basicAuth(username, password))
.build();
AtomicInteger retryAfterSeconds = new AtomicInteger(0);
WebSocket webSocket = okhttp.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
@ -128,6 +134,12 @@ final class CdsiSocket {
case WAITING_FOR_TOKEN:
ClientResponse tokenResponse = ClientResponse.parseFrom(client.establishedRecv(bytes.toByteArray()));
if (tokenResponse.getRetryAfterSecs() > 0) {
Log.w(TAG, "Got a retry-after (" + tokenResponse.getRetryAfterSecs() + "), meaning we hit the rate limit. (WAITING_FOR_TOKEN)");
throw new CdsiResourceExhaustedException(tokenResponse.getRetryAfterSecs());
}
if (tokenResponse.getToken().isEmpty()) {
throw new IOException("No token! Cannot continue!");
}
@ -143,6 +155,13 @@ final class CdsiSocket {
break;
case WAITING_FOR_RESPONSE:
ClientResponse dataResponse = ClientResponse.parseFrom(client.establishedRecv(bytes.toByteArray()));
if (dataResponse.getRetryAfterSecs() > 0) {
Log.w(TAG, "Got a retry-after (" + dataResponse.getRetryAfterSecs() + "), meaning we hit the rate limit. (WAITING_FOR_RESPONSE)");
throw new CdsiResourceExhaustedException(dataResponse.getRetryAfterSecs());
}
emitter.onNext(ClientResponse.parseFrom(client.establishedRecv(bytes.toByteArray())));
break;
@ -172,7 +191,13 @@ final class CdsiSocket {
Log.w(TAG, "Remote side is closing with non-normal code " + code);
webSocket.close(1000, "Remote closed with code " + code);
stage.set(Stage.FAILED);
emitter.tryOnError(new NonSuccessfulResponseCodeException(code));
if (code == 4003) {
emitter.tryOnError(new CdsiInvalidArgumentException());
} else if (code == 4101) {
emitter.tryOnError(new CdsiInvalidTokenException());
} else {
emitter.tryOnError(new NonSuccessfulResponseCodeException(code));
}
}
}

Wyświetl plik

@ -40,14 +40,14 @@ public final class CdsiV2Service {
private static final UUID EMPTY_UUID = new UUID(0, 0);
private static final int RESPONSE_ITEM_SIZE = 8 + 16 + 16; // 1 uint64 + 2 UUIDs
private final CdsiSocket cdshSocket;
private final CdsiSocket cdsiSocket;
public CdsiV2Service(SignalServiceConfiguration configuration, String mrEnclave) {
this.cdshSocket = new CdsiSocket(configuration, mrEnclave);
this.cdsiSocket = new CdsiSocket(configuration, mrEnclave);
}
public Single<ServiceResponse<Response>> getRegisteredUsers(String username, String password, Request request, Consumer<byte[]> tokenSaver) {
return cdshSocket
return cdsiSocket
.connect(username, password, buildClientRequest(request), tokenSaver)
.map(CdsiV2Service::parseEntries)
.collect(Collectors.toList())