From f1ba947a59d4843d99fc313c324d8a0a544a41a0 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 19 Jul 2024 16:17:04 -0400 Subject: [PATCH] Add a "connectivity warning" bottom sheet. --- .../thoughtcrime/securesms/MainActivity.java | 10 +- .../ConnectivityWarningBottomSheet.kt | 101 ++++++++++++++++++ .../DebugLogsPromptDialogFragment.kt | 21 ++-- .../DeviceSpecificNotificationBottomSheet.kt | 4 +- .../NotificationsSettingsViewModel.kt | 2 +- .../securesms/crash/CrashConfig.kt | 7 +- .../securesms/keyvalue/MiscellaneousValues.kt | 12 +++ .../messages/IncomingMessageObserver.kt | 4 + .../DeviceSpecificNotificationConfig.kt | 7 +- .../SlowNotificationHeuristics.kt | 6 +- .../notifications/VitalsViewModel.kt | 69 +++++++----- .../securesms/util/ConnectivityWarning.kt | 61 +++++++++++ .../securesms/util/RemoteConfig.kt | 7 ++ .../ic_connectivity_warning.xml | 19 ++++ .../res/drawable/ic_connectivity_warning.xml | 19 ++++ app/src/main/res/values/strings.xml | 14 ++- 16 files changed, 312 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/ConnectivityWarningBottomSheet.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/ConnectivityWarning.kt create mode 100644 app/src/main/res/drawable-night/ic_connectivity_warning.xml create mode 100644 app/src/main/res/drawable/ic_connectivity_warning.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java index 686663a09f..d6fb0c6e83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java @@ -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; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConnectivityWarningBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ConnectivityWarningBottomSheet.kt new file mode 100644 index 0000000000..a70226b58c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConnectivityWarningBottomSheet.kt @@ -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() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DebugLogsPromptDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/DebugLogsPromptDialogFragment.kt index 62ce7945d0..8f5d391966 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/DebugLogsPromptDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/DebugLogsPromptDialogFragment.kt @@ -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") } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DeviceSpecificNotificationBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/DeviceSpecificNotificationBottomSheet.kt index 0ffcea076a..90605396a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/DeviceSpecificNotificationBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/DeviceSpecificNotificationBottomSheet.kt @@ -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() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsViewModel.kt index 7691a89825..4447dcdeee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsViewModel.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/crash/CrashConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/crash/CrashConfig.kt index a7a4a05dd9..6208800533 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crash/CrashConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/crash/CrashConfig.kt @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt index a5f4463233..0f8f6a66e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt @@ -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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt index 480e52699e..9581411d92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt @@ -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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeviceSpecificNotificationConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeviceSpecificNotificationConfig.kt index af62b28dd0..c882399c0b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeviceSpecificNotificationConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeviceSpecificNotificationConfig.kt @@ -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 } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SlowNotificationHeuristics.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/SlowNotificationHeuristics.kt index d6f1b02c98..68c17aef1d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SlowNotificationHeuristics.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SlowNotificationHeuristics.kt @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/VitalsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/VitalsViewModel.kt index c0b0915469..56f6e65c94 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/VitalsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/VitalsViewModel.kt @@ -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 { 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 } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ConnectivityWarning.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ConnectivityWarning.kt new file mode 100644 index 0000000000..95bdbda33d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConnectivityWarning.kt @@ -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? + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt index 108f29efcb..c47b61b312 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt @@ -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 } diff --git a/app/src/main/res/drawable-night/ic_connectivity_warning.xml b/app/src/main/res/drawable-night/ic_connectivity_warning.xml new file mode 100644 index 0000000000..62fc3272e6 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_connectivity_warning.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_connectivity_warning.xml b/app/src/main/res/drawable/ic_connectivity_warning.xml new file mode 100644 index 0000000000..22d41dc06c --- /dev/null +++ b/app/src/main/res/drawable/ic_connectivity_warning.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9d7860a0e6..f8f5ffee97 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1111,6 +1111,9 @@ Debug logs helps us diagnose and fix the issue, and do not contain identifying information. Signal encountered a problem. Submit debug log? + + You may not be receiving messages. Submit debug log? + Notifications may be delayed due to battery optimizations @@ -1126,6 +1129,13 @@ Continue + + You may not be receiving messages + + Restarting your device may help solve the message delivery issue. If this problem continues, contact Signal support. + + Got it + Continue @@ -3334,10 +3344,6 @@ Signal Android Debug Log Submission - - Slow notifications - - Crash Submit