mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Add a "connectivity warning" bottom sheet.
This commit is contained in:
committed by
Nicholas Tinsley
parent
44b2c62a0e
commit
f1ba947a59
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
19
app/src/main/res/drawable-night/ic_connectivity_warning.xml
Normal file
19
app/src/main/res/drawable-night/ic_connectivity_warning.xml
Normal 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>
|
||||
19
app/src/main/res/drawable/ic_connectivity_warning.xml
Normal file
19
app/src/main/res/drawable/ic_connectivity_warning.xml
Normal 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>
|
||||
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user