Add a "connectivity warning" bottom sheet.

This commit is contained in:
Greyson Parrelli
2024-07-19 16:17:04 -04:00
committed by Nicholas Tinsley
parent 44b2c62a0e
commit f1ba947a59
16 changed files with 312 additions and 51 deletions

View File

@@ -19,8 +19,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.donations.StripeApi;
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment;
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment;
import org.thoughtcrime.securesms.components.DeviceSpecificNotificationBottomSheet;
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment;
import org.thoughtcrime.securesms.components.ConnectivityWarningBottomSheet;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
@@ -119,11 +120,18 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
case PROMPT_GENERAL_BATTERY_SAVER_DIALOG:
PromptBatterySaverDialogFragment.show(getSupportFragmentManager());
break;
case PROMPT_CONNECTIVITY_WARNING:
ConnectivityWarningBottomSheet.show(getSupportFragmentManager());
break;
case PROMPT_DEBUGLOGS_FOR_NOTIFICATIONS:
DebugLogsPromptDialogFragment.show(this, DebugLogsPromptDialogFragment.Purpose.NOTIFICATIONS);
break;
case PROMPT_DEBUGLOGS_FOR_CRASH:
DebugLogsPromptDialogFragment.show(this, DebugLogsPromptDialogFragment.Purpose.CRASH);
break;
case PROMPT_DEBUGLOGS_FOR_CONNECTIVITY_WARNING:
DebugLogsPromptDialogFragment.show(this, DebugLogsPromptDialogFragment.Purpose.CONNECTIVITY_WARNING);
break;
}
}

View File

@@ -0,0 +1,101 @@
package org.thoughtcrime.securesms.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentManager
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
/**
* A bottom sheet that warns the user when they haven't been able to connect to the websocket for some time.
*/
class ConnectivityWarningBottomSheet : ComposeBottomSheetDialogFragment() {
override val peekHeightPercentage: Float = 0.66f
companion object {
@JvmStatic
fun show(fragmentManager: FragmentManager) {
if (fragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) == null) {
ConnectivityWarningBottomSheet().show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
SignalStore.misc.lastConnectivityWarningTime = System.currentTimeMillis()
}
}
}
@Composable
override fun SheetContent() {
Sheet(
onDismiss = { dismissAllowingStateLoss() }
)
}
}
@Composable
private fun Sheet(onDismiss: () -> Unit = {}) {
return Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth().wrapContentSize(Alignment.Center)
) {
BottomSheets.Handle()
Icon(
painterResource(id = R.drawable.ic_connectivity_warning),
contentDescription = null,
tint = Color.Unspecified,
modifier = Modifier.padding(top = 32.dp, bottom = 8.dp)
)
Text(
text = stringResource(id = R.string.ConnectivityWarningBottomSheet_title),
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
)
Text(
text = stringResource(id = R.string.ConnectivityWarningBottomSheet_body),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 24.dp)
)
Row(
modifier = Modifier.padding(top = 60.dp, bottom = 24.dp, start = 24.dp, end = 24.dp)
) {
Buttons.MediumTonal(
onClick = onDismiss,
modifier = Modifier.padding(end = 12.dp)
) {
Text(stringResource(id = R.string.ConnectivityWarningBottomSheet_dismiss_button))
}
}
}
}
@SignalPreview
@Composable
private fun ConnectivityWarningSheetPreview() {
Previews.BottomSheetPreview {
Sheet()
}
}

View File

@@ -15,7 +15,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import org.signal.core.util.ResourceUtil
import org.signal.core.util.concurrent.LifecycleDisposable
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.databinding.PromptLogsBottomSheetBinding
@@ -50,6 +49,7 @@ class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragmen
when (purpose) {
Purpose.NOTIFICATIONS -> SignalStore.uiHints.lastNotificationLogsPrompt = System.currentTimeMillis()
Purpose.CRASH -> SignalStore.uiHints.lastCrashPrompt = System.currentTimeMillis()
Purpose.CONNECTIVITY_WARNING -> SignalStore.misc.lastConnectivityWarningTime = System.currentTimeMillis()
}
}
}
@@ -85,6 +85,9 @@ class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragmen
Purpose.CRASH -> {
binding.title.setText(R.string.PromptLogsSlowNotificationsDialog__title_crash)
}
Purpose.CONNECTIVITY_WARNING -> {
binding.title.setText(R.string.PromptLogsSlowNotificationsDialog__title_connectivity_warning)
}
}
binding.submit.setOnClickListener {
@@ -137,8 +140,9 @@ class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragmen
}
val category = when (purpose) {
Purpose.NOTIFICATIONS -> ResourceUtil.getEnglishResources(requireContext()).getString(R.string.DebugLogsPromptDialogFragment__slow_notifications_category)
Purpose.CRASH -> ResourceUtil.getEnglishResources(requireContext()).getString(R.string.DebugLogsPromptDialogFragment__crash_category)
Purpose.NOTIFICATIONS -> "Slow notifications"
Purpose.CRASH -> "Crash"
Purpose.CONNECTIVITY_WARNING -> "Connectivity"
}
return SupportEmailUtil.generateSupportEmailBody(
@@ -177,17 +181,12 @@ class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragmen
enum class Purpose(val serialized: Int) {
NOTIFICATIONS(1),
CRASH(2);
CRASH(2),
CONNECTIVITY_WARNING(3);
companion object {
fun deserialize(serialized: Int): Purpose {
for (value in values()) {
if (value.serialized == serialized) {
return value
}
}
throw IllegalArgumentException("Invalid value: $serialized")
return entries.firstOrNull { it.serialized == serialized } ?: throw IllegalArgumentException("Invalid value: $serialized")
}
}
}

View File

@@ -63,7 +63,7 @@ class DeviceSpecificNotificationBottomSheet : ComposeBottomSheetDialogFragment()
}
@Composable
fun DeviceSpecificSheet(onContinue: () -> Unit = {}, onDismiss: () -> Unit = {}) {
private fun DeviceSpecificSheet(onContinue: () -> Unit = {}, onDismiss: () -> Unit = {}) {
return Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth().wrapContentSize(Alignment.Center)
@@ -111,7 +111,7 @@ fun DeviceSpecificSheet(onContinue: () -> Unit = {}, onDismiss: () -> Unit = {})
@SignalPreview
@Composable
fun DeviceSpecificSheetPreview() {
private fun DeviceSpecificSheetPreview() {
Previews.BottomSheetPreview {
DeviceSpecificSheet()
}

View File

@@ -122,7 +122,7 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer
priority = TextSecurePreferences.getNotificationPriority(AppDependencies.application),
troubleshootNotifications = if (calculateSlowNotifications) {
(SlowNotificationHeuristics.isBatteryOptimizationsOn() && SlowNotificationHeuristics.isHavingDelayedNotifications()) ||
SlowNotificationHeuristics.showCondition() == DeviceSpecificNotificationConfig.ShowCondition.ALWAYS
SlowNotificationHeuristics.getDeviceSpecificShowCondition() == DeviceSpecificNotificationConfig.ShowCondition.ALWAYS
} else if (currentState != null) {
currentState.messageNotificationsState.troubleshootNotifications
} else {

View File

@@ -112,7 +112,12 @@ object CrashConfig {
return false
}
val partsPerMillion = (1_000_000 * percent).toInt()
if (percent <= 0f || percent > 100f) {
return false
}
val fraction = percent / 100
val partsPerMillion = (1_000_000 * fraction).toInt()
val bucket = BucketingUtil.bucket(RemoteConfig.CRASH_PROMPT_CONFIG, aci.rawUuid, 1_000_000)
return partsPerMillion > bucket
}

View File

@@ -38,6 +38,8 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto
private const val NEXT_DATABASE_ANALYSIS_TIME = "misc.next_database_analysis_time"
private const val LOCK_SCREEN_ATTEMPT_COUNT = "misc.lock_screen_attempt_count"
private const val LAST_NETWORK_RESET_TIME = "misc.last_network_reset_time"
private const val LAST_WEBSOCKET_CONNECT_TIME = "misc.last_websocket_connect_time"
private const val LAST_CONNECTIVITY_WARNING_TIME = "misc.last_connectivity_warning_time"
}
public override fun onFirstEverAppLaunch() {
@@ -261,4 +263,14 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto
}
var lastNetworkResetDueToStreamResets: Long by longValue(LAST_NETWORK_RESET_TIME, 0L)
/**
* The last time you successfully connected to the websocket.
*/
var lastWebSocketConnectTime: Long by longValue(LAST_WEBSOCKET_CONNECT_TIME, System.currentTimeMillis())
/**
* The last time we prompted the user regarding a [org.thoughtcrime.securesms.util.ConnectivityWarning].
*/
var lastConnectivityWarningTime: Long by longValue(LAST_CONNECTIVITY_WARNING_TIME, 0)
}

View File

@@ -378,6 +378,10 @@ class IncomingMessageObserver(private val context: Application) {
// Any state change at all means that we are not drained
decryptionDrained = false
if (state == WebSocketConnectionState.CONNECTED) {
SignalStore.misc.lastWebSocketConnectTime = System.currentTimeMillis()
}
}
signalWebSocket.connect()

View File

@@ -29,7 +29,7 @@ object DeviceSpecificNotificationConfig {
*/
data class Config(
@JsonProperty val model: String = "",
@JsonProperty val showConditionCode: String = "has-slow-notifications",
@JsonProperty val showConditionCode: String = ShowCondition.NONE.code,
@JsonProperty val link: String = GENERAL_SUPPORT_URL,
@JsonProperty val localePercent: String = "*",
@JsonProperty val version: Int = 0
@@ -43,10 +43,11 @@ object DeviceSpecificNotificationConfig {
enum class ShowCondition(val code: String) {
ALWAYS("always"),
HAS_BATTERY_OPTIMIZATION_ON("has-battery-optimization-on"),
HAS_SLOW_NOTIFICATIONS("has-slow-notifications");
HAS_SLOW_NOTIFICATIONS("has-slow-notifications"),
NONE("none");
companion object {
fun fromCode(code: String) = values().firstOrNull { it.code == code } ?: HAS_SLOW_NOTIFICATIONS
fun fromCode(code: String) = entries.firstOrNull { it.code == code } ?: NONE
}
}

View File

@@ -63,7 +63,7 @@ object SlowNotificationHeuristics {
}
@JvmStatic
fun shouldPromptUserForLogs(): Boolean {
fun shouldPromptUserForDelayedNotificationLogs(): Boolean {
if (!LocaleRemoteConfig.isDelayedNotificationPromptEnabled() || SignalStore.uiHints.hasDeclinedToShareNotificationLogs()) {
return false
}
@@ -143,11 +143,11 @@ object SlowNotificationHeuristics {
return true
}
fun showCondition(): DeviceSpecificNotificationConfig.ShowCondition {
fun getDeviceSpecificShowCondition(): DeviceSpecificNotificationConfig.ShowCondition {
return DeviceSpecificNotificationConfig.currentConfig.showCondition
}
fun shouldShowDialog(): Boolean {
fun shouldShowDeviceSpecificDialog(): Boolean {
return LocaleRemoteConfig.isDeviceSpecificNotificationEnabled() && SignalStore.uiHints.lastSupportVersionSeen < DeviceSpecificNotificationConfig.currentConfig.version
}

View File

@@ -15,6 +15,9 @@ import io.reactivex.rxjava3.subjects.BehaviorSubject
import org.thoughtcrime.securesms.crash.CrashConfig
import org.thoughtcrime.securesms.database.LogDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.DeviceSpecificNotificationConfig.ShowCondition
import org.thoughtcrime.securesms.util.ConnectivityWarning
import org.thoughtcrime.securesms.util.NetworkUtil
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.days
@@ -45,34 +48,48 @@ class VitalsViewModel(private val context: Application) : AndroidViewModel(conte
private fun checkHeuristics(): Single<State> {
return Single.fromCallable {
var state = State.NONE
when (SlowNotificationHeuristics.showCondition()) {
DeviceSpecificNotificationConfig.ShowCondition.ALWAYS -> {
if (SlowNotificationHeuristics.shouldShowDialog()) {
state = State.PROMPT_SPECIFIC_BATTERY_SAVER_DIALOG
}
}
DeviceSpecificNotificationConfig.ShowCondition.HAS_BATTERY_OPTIMIZATION_ON -> {
if (SlowNotificationHeuristics.shouldShowDialog() && SlowNotificationHeuristics.isBatteryOptimizationsOn()) {
state = State.PROMPT_SPECIFIC_BATTERY_SAVER_DIALOG
}
}
DeviceSpecificNotificationConfig.ShowCondition.HAS_SLOW_NOTIFICATIONS -> {
if (SlowNotificationHeuristics.isHavingDelayedNotifications() && SlowNotificationHeuristics.shouldPromptBatterySaver()) {
state = State.PROMPT_GENERAL_BATTERY_SAVER_DIALOG
} else if (SlowNotificationHeuristics.isHavingDelayedNotifications() && SlowNotificationHeuristics.shouldPromptUserForLogs()) {
state = State.PROMPT_DEBUGLOGS_FOR_NOTIFICATIONS
} else if (LogDatabase.getInstance(context).crashes.anyMatch(patterns = CrashConfig.patterns, promptThreshold = System.currentTimeMillis() - 14.days.inWholeMilliseconds)) {
val timeSinceLastPrompt = System.currentTimeMillis() - SignalStore.uiHints.lastCrashPrompt
val deviceSpecificCondition = SlowNotificationHeuristics.getDeviceSpecificShowCondition()
if (timeSinceLastPrompt > 1.days.inWholeMilliseconds) {
state = State.PROMPT_DEBUGLOGS_FOR_CRASH
}
}
if (deviceSpecificCondition == ShowCondition.ALWAYS && SlowNotificationHeuristics.shouldShowDeviceSpecificDialog()) {
return@fromCallable State.PROMPT_SPECIFIC_BATTERY_SAVER_DIALOG
}
if (deviceSpecificCondition == ShowCondition.HAS_BATTERY_OPTIMIZATION_ON && SlowNotificationHeuristics.shouldShowDeviceSpecificDialog() && SlowNotificationHeuristics.isBatteryOptimizationsOn()) {
return@fromCallable State.PROMPT_SPECIFIC_BATTERY_SAVER_DIALOG
}
if (deviceSpecificCondition == ShowCondition.HAS_SLOW_NOTIFICATIONS && SlowNotificationHeuristics.shouldPromptBatterySaver()) {
return@fromCallable State.PROMPT_SPECIFIC_BATTERY_SAVER_DIALOG
}
if (SlowNotificationHeuristics.isHavingDelayedNotifications() && SlowNotificationHeuristics.shouldPromptBatterySaver()) {
return@fromCallable State.PROMPT_GENERAL_BATTERY_SAVER_DIALOG
}
if (SlowNotificationHeuristics.isHavingDelayedNotifications() && SlowNotificationHeuristics.shouldPromptUserForDelayedNotificationLogs()) {
return@fromCallable State.PROMPT_DEBUGLOGS_FOR_NOTIFICATIONS
}
val timeSinceLastConnection = System.currentTimeMillis() - SignalStore.misc.lastWebSocketConnectTime
val timeSinceLastConnectionWarning = System.currentTimeMillis() - SignalStore.misc.lastConnectivityWarningTime
if (ConnectivityWarning.isEnabled && timeSinceLastConnection > ConnectivityWarning.threshold && timeSinceLastConnectionWarning > 14.days.inWholeMilliseconds && NetworkUtil.isConnected(context)) {
return@fromCallable if (ConnectivityWarning.isDebugPromptEnabled) {
State.PROMPT_DEBUGLOGS_FOR_CONNECTIVITY_WARNING
} else {
State.PROMPT_CONNECTIVITY_WARNING
}
}
return@fromCallable state
if (LogDatabase.getInstance(context).crashes.anyMatch(patterns = CrashConfig.patterns, promptThreshold = System.currentTimeMillis() - 14.days.inWholeMilliseconds)) {
val timeSinceLastPrompt = System.currentTimeMillis() - SignalStore.uiHints.lastCrashPrompt
if (timeSinceLastPrompt > 1.days.inWholeMilliseconds) {
return@fromCallable State.PROMPT_DEBUGLOGS_FOR_CRASH
}
}
return@fromCallable State.NONE
}.subscribeOn(Schedulers.io())
}
@@ -81,6 +98,8 @@ class VitalsViewModel(private val context: Application) : AndroidViewModel(conte
PROMPT_SPECIFIC_BATTERY_SAVER_DIALOG,
PROMPT_GENERAL_BATTERY_SAVER_DIALOG,
PROMPT_DEBUGLOGS_FOR_NOTIFICATIONS,
PROMPT_DEBUGLOGS_FOR_CRASH
PROMPT_DEBUGLOGS_FOR_CRASH,
PROMPT_CONNECTIVITY_WARNING,
PROMPT_DEBUGLOGS_FOR_CONNECTIVITY_WARNING
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.util
import com.fasterxml.jackson.annotation.JsonProperty
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.keyvalue.SignalStore
import java.io.IOException
import kotlin.time.Duration.Companion.hours
/**
* An object representing the configuration of the connectivity warning UI, which lets a user know when they haven't been able to connect to the service.
*/
object ConnectivityWarning {
private val TAG = Log.tag(ConnectivityWarning::class)
private val config: Config? by lazy {
try {
JsonUtils.fromJson(RemoteConfig.connectivityWarningConfig, Config::class.java)
} catch (e: IOException) {
Log.w(TAG, "Failed to parse json!", e)
null
}
}
/** Whether or not connectivity warnings are enabled. */
val isEnabled
get() = threshold > 0
/** If the user has not connected to the service in this amount of time (in ms), then you should show the connectivity warning. A time of <= 0 means never show it. */
val threshold = config?.thresholdHours?.hours?.inWholeMilliseconds ?: 0
/** Whether or not you should prompt the user for a log when notifying them that they are unable to connect. */
val isDebugPromptEnabled: Boolean
get() {
val nonNullConfig = config ?: return false
if (nonNullConfig.percentDebugPrompt == null) {
return false
}
if (nonNullConfig.percentDebugPrompt <= 0f || nonNullConfig.percentDebugPrompt > 100f) {
return false
}
val fraction = nonNullConfig.percentDebugPrompt / 100
val partsPerMillion = (1_000_000 * fraction).toInt()
val bucket = BucketingUtil.bucket(RemoteConfig.CRASH_PROMPT_CONFIG, SignalStore.account.aci!!.rawUuid, 1_000_000)
return partsPerMillion > bucket
}
private data class Config(
@JsonProperty val thresholdHours: Int?,
@JsonProperty val percentDebugPrompt: Float?
)
}

View File

@@ -1104,5 +1104,12 @@ object RemoteConfig {
}
)
/** JSON object representing some details about how we might want to warn the user around connectivity issues. */
val connectivityWarningConfig: String by remoteString(
key = "android.connectivityWarningConfig",
defaultValue = "",
hotSwappable = true
)
// endregion
}

View File

@@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="72"
android:viewportHeight="72">
<path
android:pathData="M36,36m-33,0a33,33 0,1 1,66 0a33,33 0,1 1,-66 0"
android:fillColor="#C88600"/>
<path
android:pathData="M36,41.5m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#F9E4B6"/>
<path
android:pathData="M36,28C37.123,28 38.007,28.96 37.914,30.08L37.357,36.751C37.299,37.457 36.708,38 36,38C35.292,38 34.701,37.457 34.642,36.751L34.086,30.08C33.993,28.96 34.877,28 36,28Z"
android:fillColor="#F9E4B6"/>
<path
android:pathData="M18.917,36C18.917,26.565 26.565,18.917 36,18.917C45.435,18.917 53.083,26.565 53.083,36C53.083,45.435 45.435,53.083 36,53.083C33.213,53.083 30.578,52.415 28.25,51.228L21.564,53.635C19.574,54.352 17.648,52.426 18.365,50.436L20.772,43.75C19.585,41.422 18.917,38.787 18.917,36ZM36,22.25C28.406,22.25 22.25,28.406 22.25,36C22.25,38.351 22.838,40.559 23.875,42.491C24.203,43.102 24.28,43.846 24.03,44.54L22.101,49.899L27.46,47.97C28.154,47.72 28.898,47.797 29.509,48.125C31.441,49.161 33.649,49.75 36,49.75C43.594,49.75 49.75,43.594 49.75,36C49.75,28.406 43.594,22.25 36,22.25Z"
android:fillColor="#F9E4B6"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="72"
android:viewportHeight="72">
<path
android:pathData="M36,36m-33,0a33,33 0,1 1,66 0a33,33 0,1 1,-66 0"
android:fillColor="#F9E4B6"/>
<path
android:pathData="M36,41.5m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#C88600"/>
<path
android:pathData="M36,28C37.123,28 38.007,28.96 37.914,30.08L37.357,36.751C37.299,37.457 36.708,38 36,38C35.292,38 34.701,37.457 34.642,36.751L34.086,30.08C33.993,28.96 34.877,28 36,28Z"
android:fillColor="#C88600"/>
<path
android:pathData="M18.917,36C18.917,26.565 26.565,18.917 36,18.917C45.435,18.917 53.083,26.565 53.083,36C53.083,45.435 45.435,53.083 36,53.083C33.213,53.083 30.578,52.415 28.25,51.228L21.564,53.635C19.574,54.352 17.648,52.426 18.365,50.436L20.772,43.75C19.585,41.422 18.917,38.787 18.917,36ZM36,22.25C28.406,22.25 22.25,28.406 22.25,36C22.25,38.351 22.838,40.559 23.875,42.491C24.203,43.102 24.28,43.846 24.03,44.54L22.101,49.899L27.46,47.97C28.154,47.72 28.898,47.797 29.509,48.125C31.441,49.161 33.649,49.75 36,49.75C43.594,49.75 49.75,43.594 49.75,36C49.75,28.406 43.594,22.25 36,22.25Z"
android:fillColor="#C88600"
android:fillType="evenOdd"/>
</vector>

View File

@@ -1111,6 +1111,9 @@
<string name="PromptLogsSlowNotificationsDialog__message">Debug logs helps us diagnose and fix the issue, and do not contain identifying information.</string>
<!-- Title for dialog asking user to submit logs for debugging slow notification issues -->
<string name="PromptLogsSlowNotificationsDialog__title_crash">Signal encountered a problem. Submit debug log?</string>
<!-- Title for dialog asking user to submit logs for a situation where they're not able to connect to the signal service -->
<string name="PromptLogsSlowNotificationsDialog__title_connectivity_warning">You may not be receiving messages. Submit debug log?</string>
<!-- Title for dialog asking user to submit logs for debugging slow notification issues -->
<string name="PromptBatterySaverBottomSheet__title">Notifications may be delayed due to battery optimizations</string>
@@ -1126,6 +1129,13 @@
<!-- Button to continue and go to Signal support website -->
<string name="DeviceSpecificNotificationBottomSheet__continue">Continue</string>
<!-- Title of a bottom sheet that is shown when the user is having connectivity issues -->
<string name="ConnectivityWarningBottomSheet_title">You may not be receiving messages</string>
<!-- Body of a bottom sheet that is shown when the user is having connectivity issues -->
<string name="ConnectivityWarningBottomSheet_body">Restarting your device may help solve the message delivery issue. If this problem continues, contact Signal support.</string>
<!-- Text for a button in a bottom sheet that is shown when the user is having connectivity issues. Clicking it will dismiss the bottom sheet. -->
<string name="ConnectivityWarningBottomSheet_dismiss_button">Got it</string>
<!-- Button to continue to try and disable battery saver -->
<string name="PromptBatterySaverBottomSheet__continue">Continue</string>
<!-- Button to dismiss battery saver dialog prompt-->
@@ -3334,10 +3344,6 @@
</string-array>
<!-- Subject of email when submitting debug logs to help debug slow notifications -->
<string name="DebugLogsPromptDialogFragment__signal_android_support_request">Signal Android Debug Log Submission</string>
<!-- Category to organize the support email sent -->
<string name="DebugLogsPromptDialogFragment__slow_notifications_category">Slow notifications</string>
<!-- Category to organize the support email sent -->
<string name="DebugLogsPromptDialogFragment__crash_category">Crash</string>
<!-- Action to submit logs and take user to send an e-mail -->
<string name="DebugLogsPromptDialogFragment__submit">Submit</string>
<!-- Action to decline to submit logs -->