kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add UX for handling CDS rate limits.
rodzic
8eb3a1906e
commit
c563ef27da
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
Ładowanie…
Reference in New Issue