mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 19:00:26 +01:00
Add UX for handling CDS rate limits.
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user