From 9024c1916957676d670120f3b291203eeb2ea89c Mon Sep 17 00:00:00 2001 From: Michelle Tang Date: Tue, 9 Jul 2024 12:04:09 -0400 Subject: [PATCH] Update device-specific notification support configs. --- .../thoughtcrime/securesms/MainActivity.java | 6 +- .../DeviceSpecificNotificationBottomSheet.kt | 118 ++++++++++++++++++ .../PromptBatterySaverDialogFragment.kt | 4 +- .../NotificationsSettingsViewModel.kt | 4 +- .../securesms/keyvalue/UiHintValues.java | 15 +++ ...kt => DeviceSpecificNotificationConfig.kt} | 34 +++-- .../SlowNotificationHeuristics.kt | 10 +- .../notifications/VitalsViewModel.kt | 33 +++-- .../securesms/util/LocaleRemoteConfig.java | 8 +- .../securesms/util/RemoteConfig.kt | 6 +- app/src/main/res/values/strings.xml | 9 ++ .../DelayedNotificationConfigTest.kt | 100 --------------- .../DeviceSpecificNotificationConfigTest.kt | 100 +++++++++++++++ .../util/RemoteConfig_StaticValuesTest.kt | 3 +- 14 files changed, 319 insertions(+), 131 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/DeviceSpecificNotificationBottomSheet.kt rename app/src/main/java/org/thoughtcrime/securesms/notifications/{DelayedNotificationConfig.kt => DeviceSpecificNotificationConfig.kt} (60%) delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/notifications/DelayedNotificationConfigTest.kt create mode 100644 app/src/test/java/org/thoughtcrime/securesms/notifications/DeviceSpecificNotificationConfigTest.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java index 98671047d5..686663a09f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java @@ -20,6 +20,7 @@ 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.settings.app.AppSettingsActivity; import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController; import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner; @@ -112,7 +113,10 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot switch (state) { case NONE: break; - case PROMPT_BATTERY_SAVER_DIALOG: + case PROMPT_SPECIFIC_BATTERY_SAVER_DIALOG: + DeviceSpecificNotificationBottomSheet.show(getSupportFragmentManager()); + break; + case PROMPT_GENERAL_BATTERY_SAVER_DIALOG: PromptBatterySaverDialogFragment.show(getSupportFragmentManager()); break; case PROMPT_DEBUGLOGS_FOR_NOTIFICATIONS: diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DeviceSpecificNotificationBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/DeviceSpecificNotificationBottomSheet.kt new file mode 100644 index 0000000000..0ffcea076a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/DeviceSpecificNotificationBottomSheet.kt @@ -0,0 +1,118 @@ +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.core.os.bundleOf +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.notifications.DeviceSpecificNotificationConfig +import org.thoughtcrime.securesms.util.BottomSheetUtil +import org.thoughtcrime.securesms.util.CommunicationActions + +class DeviceSpecificNotificationBottomSheet : ComposeBottomSheetDialogFragment() { + + override val peekHeightPercentage: Float = 0.66f + + companion object { + private const val ARG_LINK = "arg.link" + private const val ARG_LINK_VERSION = "arg.link.version" + + @JvmStatic + fun show(fragmentManager: FragmentManager) { + if (fragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) == null) { + DeviceSpecificNotificationBottomSheet().apply { + arguments = bundleOf( + ARG_LINK to DeviceSpecificNotificationConfig.currentConfig.link, + ARG_LINK_VERSION to DeviceSpecificNotificationConfig.currentConfig.version + ) + }.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + SignalStore.uiHints.lastSupportVersionSeen = DeviceSpecificNotificationConfig.currentConfig.version + } + } + } + + @Composable + override fun SheetContent() { + DeviceSpecificSheet(this::onContinue, this::dismissAllowingStateLoss) + } + + private fun onContinue() { + val link = arguments?.getString(ARG_LINK) ?: getString(R.string.PromptBatterySaverBottomSheet__learn_more_url) + CommunicationActions.openBrowserLink(requireContext(), link) + } +} + +@Composable +fun DeviceSpecificSheet(onContinue: () -> Unit = {}, onDismiss: () -> Unit = {}) { + return Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth().wrapContentSize(Alignment.Center) + ) { + BottomSheets.Handle() + Icon( + painterResource(id = R.drawable.ic_troubleshoot_notification), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.padding(top = 32.dp, bottom = 8.dp) + ) + Text( + text = stringResource(id = R.string.DeviceSpecificNotificationBottomSheet__notifications_may_be_delayed), + 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.DeviceSpecificNotificationBottomSheet__disable_battery_optimizations), + 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).weight(1f) + ) { + Text(stringResource(id = R.string.DeviceSpecificNotificationBottomSheet__no_thanks)) + } + Buttons.MediumTonal( + onClick = onContinue, + modifier = Modifier.padding(start = 12.dp).weight(1f) + ) { + Icon(painterResource(id = R.drawable.ic_open_20), contentDescription = null, modifier = Modifier.padding(end = 4.dp)) + Text(stringResource(id = R.string.DeviceSpecificNotificationBottomSheet__continue)) + } + } + } +} + +@SignalPreview +@Composable +fun DeviceSpecificSheetPreview() { + Previews.BottomSheetPreview { + DeviceSpecificSheet() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/PromptBatterySaverDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/PromptBatterySaverDialogFragment.kt index df0ce51542..5536a7d167 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/PromptBatterySaverDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/PromptBatterySaverDialogFragment.kt @@ -18,7 +18,7 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.databinding.PromptBatterySaverBottomSheetBinding import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.notifications.DelayedNotificationConfig +import org.thoughtcrime.securesms.notifications.DeviceSpecificNotificationConfig import org.thoughtcrime.securesms.util.BottomSheetUtil import org.thoughtcrime.securesms.util.LocalMetrics import org.thoughtcrime.securesms.util.PowerManagerCompat @@ -35,7 +35,7 @@ class PromptBatterySaverDialogFragment : FixedRoundedCornerBottomSheetDialogFrag if (fragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) == null) { PromptBatterySaverDialogFragment().apply { arguments = bundleOf( - ARG_LEARN_MORE_LINK to DelayedNotificationConfig.currentConfig.link + ARG_LEARN_MORE_LINK to DeviceSpecificNotificationConfig.currentConfig.link ) }.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) SignalStore.uiHints.lastBatterySaverPrompt = System.currentTimeMillis() 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 d384f1a40c..7691a89825 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 @@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.notifications.DeviceSpecificNotificationConfig import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.SlowNotificationHeuristics import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference @@ -120,7 +121,8 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer messagePrivacy = SignalStore.settings.messageNotificationsPrivacy.toString(), priority = TextSecurePreferences.getNotificationPriority(AppDependencies.application), troubleshootNotifications = if (calculateSlowNotifications) { - SlowNotificationHeuristics.isPotentiallyCausedByBatteryOptimizations() && (SlowNotificationHeuristics.isHavingDelayedNotifications() || SlowNotificationHeuristics.showPreemptively()) + (SlowNotificationHeuristics.isBatteryOptimizationsOn() && SlowNotificationHeuristics.isHavingDelayedNotifications()) || + SlowNotificationHeuristics.showCondition() == DeviceSpecificNotificationConfig.ShowCondition.ALWAYS } else if (currentState != null) { currentState.messageNotificationsState.troubleshootNotifications } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHintValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHintValues.java index 5fd3bd89c8..bb72dc3e92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHintValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHintValues.java @@ -26,6 +26,7 @@ public class UiHintValues extends SignalStoreValues { private static final String HAS_SEEN_DOUBLE_TAP_EDIT_EDUCATION_SHEET = "uihints.has_seen_double_tap_edit_education_sheet"; private static final String DISMISSED_CONTACTS_PERMISSION_BANNER = "uihints.dismissed_contacts_permission_banner"; private static final String HAS_SEEN_DELETE_SYNC_EDUCATION_SHEET = "uihints.has_seen_delete_sync_education_sheet"; + private static final String LAST_SUPPORT_VERSION_SEEN = "uihints.last_support_version_seen"; UiHintValues(@NonNull KeyValueStore store) { super(store); @@ -185,4 +186,18 @@ public class UiHintValues extends SignalStoreValues { public boolean getHasSeenDeleteSyncEducationSheet() { return getBoolean(HAS_SEEN_DELETE_SYNC_EDUCATION_SHEET, false); } + + /** + * @return the last version of the support article for delayed notifications that users have seen. Versions are increased in a remote config. + */ + public int getLastSupportVersionSeen() { + return getInteger(LAST_SUPPORT_VERSION_SEEN, 0); + } + + /** + * Sets the version number of the support article that users see if they have device-specific notifications issues + */ + public void setLastSupportVersionSeen(int version) { + putLong(LAST_SUPPORT_VERSION_SEEN, version); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DelayedNotificationConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeviceSpecificNotificationConfig.kt similarity index 60% rename from app/src/main/java/org/thoughtcrime/securesms/notifications/DelayedNotificationConfig.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/DeviceSpecificNotificationConfig.kt index 93101f466f..af62b28dd0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DelayedNotificationConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeviceSpecificNotificationConfig.kt @@ -11,32 +11,50 @@ import java.io.IOException /** * Remote configs for a device to show a support screen in an effort to prevent delayed notifications */ -object DelayedNotificationConfig { +object DeviceSpecificNotificationConfig { - private val TAG = Log.tag(DelayedNotificationConfig::class.java) + private val TAG = Log.tag(DeviceSpecificNotificationConfig::class.java) private const val GENERAL_SUPPORT_URL = "https://support.signal.org/hc/articles/360007318711#android_notifications_troubleshooting" + @JvmStatic val currentConfig: Config by lazy { computeConfig() } /** * Maps a device model to specific modifications set in order to support better notification * @param model either exact device model name or model name that ends with a wildcard - * @param showPreemptively shows support sheet immediately if true or after a vitals failure if not, still dependent on localePercent + * @param showConditionCode outlines under which conditions to show the prompt, still dependent on localePercent * @param link represents the Signal support url that corresponds to this device model * @param localePercent represents the percent of people who will get this change per country + * @param version represents the version of the link being shown and should be incremented if the link or link content changes */ data class Config( @JsonProperty val model: String = "", - @JsonProperty val showPreemptively: Boolean = false, + @JsonProperty val showConditionCode: String = "has-slow-notifications", @JsonProperty val link: String = GENERAL_SUPPORT_URL, - @JsonProperty val localePercent: String = RemoteConfig.promptBatterySaver - ) + @JsonProperty val localePercent: String = "*", + @JsonProperty val version: Int = 0 + ) { + val showCondition: ShowCondition = ShowCondition.fromCode(showConditionCode) + } + + /** + * Describes under which conditions to show device help prompt + */ + enum class ShowCondition(val code: String) { + ALWAYS("always"), + HAS_BATTERY_OPTIMIZATION_ON("has-battery-optimization-on"), + HAS_SLOW_NOTIFICATIONS("has-slow-notifications"); + + companion object { + fun fromCode(code: String) = values().firstOrNull { it.code == code } ?: HAS_SLOW_NOTIFICATIONS + } + } @VisibleForTesting fun computeConfig(): Config { val default = Config() - val serialized = RemoteConfig.promptDelayedNotificationConfig - if (serialized.isNullOrBlank()) { + val serialized = RemoteConfig.deviceSpecificNotificationConfig + if (serialized.isBlank()) { return default } 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 08ed78096e..d6f1b02c98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SlowNotificationHeuristics.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SlowNotificationHeuristics.kt @@ -132,7 +132,7 @@ object SlowNotificationHeuristics { * true can most definitely be at fault. */ @JvmStatic - fun isPotentiallyCausedByBatteryOptimizations(): Boolean { + fun isBatteryOptimizationsOn(): Boolean { val applicationContext = AppDependencies.application if (DeviceProperties.getDataSaverState(applicationContext) == DeviceProperties.DataSaverState.ENABLED) { return false @@ -143,8 +143,12 @@ object SlowNotificationHeuristics { return true } - fun showPreemptively(): Boolean { - return DelayedNotificationConfig.currentConfig.showPreemptively + fun showCondition(): DeviceSpecificNotificationConfig.ShowCondition { + return DeviceSpecificNotificationConfig.currentConfig.showCondition + } + + fun shouldShowDialog(): Boolean { + return LocaleRemoteConfig.isDeviceSpecificNotificationEnabled() && SignalStore.uiHints.lastSupportVersionSeen < DeviceSpecificNotificationConfig.currentConfig.version } private fun hasRepeatedFailedServiceStarts(metrics: List, minimumEventAgeMs: Long, minimumEventCount: Int, failurePercentage: Float): Boolean { 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 40787459df..1128361bc7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/VitalsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/VitalsViewModel.kt @@ -46,17 +46,29 @@ class VitalsViewModel(private val context: Application) : AndroidViewModel(conte private fun checkHeuristics(): Single { return Single.fromCallable { var state = State.NONE - if (SlowNotificationHeuristics.showPreemptively() || SlowNotificationHeuristics.isHavingDelayedNotifications()) { - if (SlowNotificationHeuristics.isPotentiallyCausedByBatteryOptimizations() && SlowNotificationHeuristics.shouldPromptBatterySaver()) { - state = State.PROMPT_BATTERY_SAVER_DIALOG - } else if (SlowNotificationHeuristics.shouldPromptUserForLogs()) { - state = State.PROMPT_DEBUGLOGS_FOR_NOTIFICATIONS + when (SlowNotificationHeuristics.showCondition()) { + DeviceSpecificNotificationConfig.ShowCondition.ALWAYS -> { + if (SlowNotificationHeuristics.shouldShowDialog()) { + state = State.PROMPT_SPECIFIC_BATTERY_SAVER_DIALOG + } } - } else if (LogDatabase.getInstance(context).crashes.anyMatch(patterns = CrashConfig.patterns, promptThreshold = System.currentTimeMillis() - 14.days.inWholeMilliseconds)) { - val timeSinceLastPrompt = System.currentTimeMillis() - SignalStore.uiHints.lastCrashPrompt + DeviceSpecificNotificationConfig.ShowCondition.HAS_BATTERY_OPTIMIZATION_ON -> { + if (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 - if (timeSinceLastPrompt > 1.days.inWholeMilliseconds) { - state = State.PROMPT_DEBUGLOGS_FOR_CRASH + if (timeSinceLastPrompt > 1.days.inWholeMilliseconds) { + state = State.PROMPT_DEBUGLOGS_FOR_CRASH + } + } } } @@ -66,7 +78,8 @@ class VitalsViewModel(private val context: Application) : AndroidViewModel(conte enum class State { NONE, - PROMPT_BATTERY_SAVER_DIALOG, + PROMPT_SPECIFIC_BATTERY_SAVER_DIALOG, + PROMPT_GENERAL_BATTERY_SAVER_DIALOG, PROMPT_DEBUGLOGS_FOR_NOTIFICATIONS, PROMPT_DEBUGLOGS_FOR_CRASH } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LocaleRemoteConfig.java b/app/src/main/java/org/thoughtcrime/securesms/util/LocaleRemoteConfig.java index 0c824edfb0..725e983c65 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LocaleRemoteConfig.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LocaleRemoteConfig.java @@ -8,7 +8,7 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.mms.PushMediaConstraints; -import org.thoughtcrime.securesms.notifications.DelayedNotificationConfig; +import org.thoughtcrime.securesms.notifications.DeviceSpecificNotificationConfig; import org.thoughtcrime.securesms.recipients.Recipient; import java.util.Arrays; @@ -75,7 +75,11 @@ public final class LocaleRemoteConfig { } public static boolean isBatterySaverPromptEnabled() { - return RemoteConfig.internalUser() || isEnabledPartsPerMillion(RemoteConfig.PROMPT_BATTERY_SAVER, DelayedNotificationConfig.INSTANCE.getCurrentConfig().getLocalePercent()); + return RemoteConfig.internalUser() || isEnabledPartsPerMillion(RemoteConfig.PROMPT_BATTERY_SAVER, RemoteConfig.promptBatterySaver()); + } + + public static boolean isDeviceSpecificNotificationEnabled() { + return isEnabledPartsPerMillion(RemoteConfig.DEVICE_SPECIFIC_NOTIFICATION_CONFIG, DeviceSpecificNotificationConfig.getCurrentConfig().getLocalePercent()); } /** 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 4e7789d6b1..ea15e512ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt @@ -874,10 +874,10 @@ object RemoteConfig { hotSwappable = true ) - private const val PROMPT_DELAYED_NOTIFICATION_CONFIG: String = "android.delayedNotificationConfig" + const val DEVICE_SPECIFIC_NOTIFICATION_CONFIG: String = "android.deviceSpecificNotificationConfig" - val promptDelayedNotificationConfig: String by remoteString( - key = PROMPT_DELAYED_NOTIFICATION_CONFIG, + val deviceSpecificNotificationConfig: String by remoteString( + key = DEVICE_SPECIFIC_NOTIFICATION_CONFIG, defaultValue = "", hotSwappable = true ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8d41262952..34b7a2cfd9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1097,6 +1097,15 @@ You can disable battery optimizations for Signal to ensure that message notifications will not be delayed. + + Notifications may be delayed due to battery optimizations + + Disable battery optimizations for Signal to ensure that message notifications will not be delayed. Tap “Continue” to see device-specific instructions. + + No thanks + + Continue + Continue diff --git a/app/src/test/java/org/thoughtcrime/securesms/notifications/DelayedNotificationConfigTest.kt b/app/src/test/java/org/thoughtcrime/securesms/notifications/DelayedNotificationConfigTest.kt deleted file mode 100644 index 22ab2b2a9b..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/notifications/DelayedNotificationConfigTest.kt +++ /dev/null @@ -1,100 +0,0 @@ -package org.thoughtcrime.securesms.notifications - -import android.app.Application -import android.os.Build -import io.mockk.every -import io.mockk.mockkObject -import io.mockk.unmockkObject -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.util.ReflectionHelpers -import org.thoughtcrime.securesms.assertIs -import org.thoughtcrime.securesms.util.RemoteConfig - -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE, application = Application::class) -class DelayedNotificationConfigTest { - - @Before - fun setup() { - mockkObject(RemoteConfig) - } - - @After - fun tearDown() { - unmockkObject(RemoteConfig) - } - - @Test - fun `empty config`() { - every { RemoteConfig.promptDelayedNotificationConfig } returns "" - DelayedNotificationConfig.computeConfig() assertIs DelayedNotificationConfig.Config() - } - - @Test - fun `invalid config`() { - every { RemoteConfig.promptDelayedNotificationConfig } returns "bad" - DelayedNotificationConfig.computeConfig() assertIs DelayedNotificationConfig.Config() - } - - @Test - fun `simple device match`() { - ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "test") - every { RemoteConfig.promptDelayedNotificationConfig } returns """[ { "model": "test", "link": "test.com", "showPreemptively": true, "localePercent": "*:500000" } ]""" - DelayedNotificationConfig.computeConfig() assertIs DelayedNotificationConfig.Config(model = "test", link = "test.com", showPreemptively = true, localePercent = "*:500000") - } - - @Test - fun `complex device match`() { - ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "test-1") - every { RemoteConfig.promptDelayedNotificationConfig } returns - """ - [ - { "model": "test", "showPreemptively": false, "localePercent": "*:10000" }, - { "model": "test-1", "showPreemptively": true, "localePercent": "*:20000" }, - { "model": "test-11", "showPreemptively": false, "localePercent": "*:30000" }, - { "model": "test-11*", "showPreemptively": false, "localePercent": "*:40000" } - ] - """.trimMargin() - DelayedNotificationConfig.computeConfig() assertIs DelayedNotificationConfig.Config(model = "test-1", showPreemptively = true, localePercent = "*:20000") - } - - @Test - fun `simple wildcard device match`() { - ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "test1") - every { RemoteConfig.promptDelayedNotificationConfig } returns """[ { "model": "test*", "link": "test.com", "showPreemptively": true, "localePercent": "*:500000" } ]""" - DelayedNotificationConfig.currentConfig assertIs DelayedNotificationConfig.Config(model = "test*", link = "test.com", showPreemptively = true, localePercent = "*:500000") - } - - @Test - fun `complex wildcard device match`() { - ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "test-1") - every { RemoteConfig.promptDelayedNotificationConfig } returns - """ - [ - { "model": "*", "showPreemptively": false, "localePercent": "*:10000" }, - { "model": "test1", "showPreemptively": false, "localePercent": "*:20000" }, - { "model": "test-", "showPreemptively": false, "localePercent": "*:30000" } - ] - """.trimMargin() - DelayedNotificationConfig.computeConfig() assertIs DelayedNotificationConfig.Config(model = "*", showPreemptively = false, localePercent = "*:10000") - } - - @Test - fun `no device match`() { - ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "bad") - every { RemoteConfig.promptDelayedNotificationConfig } returns """[ { "model": "test", "link": "test.com", "showPreemptively": true, "localePercent": "*:500000" } ]""" - DelayedNotificationConfig.computeConfig() assertIs DelayedNotificationConfig.Config() - } - - @Test - fun `default fields is zero percent`() { - ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "test") - every { RemoteConfig.promptDelayedNotificationConfig } returns """[ { "model": "test" } ]""" - DelayedNotificationConfig.computeConfig() assertIs DelayedNotificationConfig.Config(model = "test", showPreemptively = false, localePercent = "*") - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/notifications/DeviceSpecificNotificationConfigTest.kt b/app/src/test/java/org/thoughtcrime/securesms/notifications/DeviceSpecificNotificationConfigTest.kt new file mode 100644 index 0000000000..a2f1bf1f2d --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/notifications/DeviceSpecificNotificationConfigTest.kt @@ -0,0 +1,100 @@ +package org.thoughtcrime.securesms.notifications + +import android.app.Application +import android.os.Build +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.util.ReflectionHelpers +import org.thoughtcrime.securesms.assertIs +import org.thoughtcrime.securesms.util.RemoteConfig + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) +class DeviceSpecificNotificationConfigTest { + + @Before + fun setup() { + mockkObject(RemoteConfig) + } + + @After + fun tearDown() { + unmockkObject(RemoteConfig) + } + + @Test + fun `empty config`() { + every { RemoteConfig.deviceSpecificNotificationConfig } returns "" + DeviceSpecificNotificationConfig.computeConfig() assertIs DeviceSpecificNotificationConfig.Config() + } + + @Test + fun `invalid config`() { + every { RemoteConfig.deviceSpecificNotificationConfig } returns "bad" + DeviceSpecificNotificationConfig.computeConfig() assertIs DeviceSpecificNotificationConfig.Config() + } + + @Test + fun `simple device match`() { + ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "test") + every { RemoteConfig.deviceSpecificNotificationConfig } returns """[ { "model": "test", "link": "test.com", "showConditionCode": "always", "localePercent": "*:500000", "version": 3 } ]""" + DeviceSpecificNotificationConfig.computeConfig() assertIs DeviceSpecificNotificationConfig.Config(model = "test", link = "test.com", showConditionCode = "always", localePercent = "*:500000", version = 3) + } + + @Test + fun `complex device match`() { + ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "test-1") + every { RemoteConfig.deviceSpecificNotificationConfig } returns + """ + [ + { "model": "test", "showConditionCode": "always", "localePercent": "*:10000", "version": 1 }, + { "model": "test-1", "showConditionCode": "has-battery-optimization-on", "localePercent": "*:20000", "version": 2 }, + { "model": "test-11", "showConditionCode": "has-slow-notifications", "localePercent": "*:30000", "version": 3 }, + { "model": "test-11*", "showConditionCode": "never", "localePercent": "*:40000", "version": 4 } + ] + """.trimMargin() + DeviceSpecificNotificationConfig.computeConfig() assertIs DeviceSpecificNotificationConfig.Config(model = "test-1", showConditionCode = "has-battery-optimization-on", localePercent = "*:20000", version = 2) + } + + @Test + fun `simple wildcard device match`() { + ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "test1") + every { RemoteConfig.deviceSpecificNotificationConfig } returns """[ { "model": "test*", "link": "test.com", "showConditionCode": "never", "localePercent": "*:500000", "version": 1 } ]""" + DeviceSpecificNotificationConfig.currentConfig assertIs DeviceSpecificNotificationConfig.Config(model = "test*", link = "test.com", showConditionCode = "never", localePercent = "*:500000", version = 1) + } + + @Test + fun `complex wildcard device match`() { + ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "test-1") + every { RemoteConfig.deviceSpecificNotificationConfig } returns + """ + [ + { "model": "*", "showConditionCode": "always", "localePercent": "*:10000", "version": 1 }, + { "model": "test1", "showConditionCode": "has-slow-notifications", "localePercent": "*:20000", "version": 2 }, + { "model": "test-", "showConditionCode": "never", "localePercent": "*:30000", "version": 3 } + ] + """.trimMargin() + DeviceSpecificNotificationConfig.computeConfig() assertIs DeviceSpecificNotificationConfig.Config(model = "*", showConditionCode = "always", localePercent = "*:10000", version = 1) + } + + @Test + fun `no device match`() { + ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "bad") + every { RemoteConfig.deviceSpecificNotificationConfig } returns """[ { "model": "test", "link": "test.com", "showConditionCode": "always", "localePercent": "*:500000", "version": 1 } ]""" + DeviceSpecificNotificationConfig.computeConfig() assertIs DeviceSpecificNotificationConfig.Config() + } + + @Test + fun `default fields is zero percent`() { + ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "test") + every { RemoteConfig.deviceSpecificNotificationConfig } returns """[ { "model": "test" } ]""" + DeviceSpecificNotificationConfig.computeConfig() assertIs DeviceSpecificNotificationConfig.Config(model = "test", localePercent = "*", version = 0) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/RemoteConfig_StaticValuesTest.kt b/app/src/test/java/org/thoughtcrime/securesms/util/RemoteConfig_StaticValuesTest.kt index f502400818..7b5bcfc844 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/util/RemoteConfig_StaticValuesTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/util/RemoteConfig_StaticValuesTest.kt @@ -41,7 +41,8 @@ class RemoteConfig_StaticValuesTest { "debugPendingDiskValues", "CRASH_PROMPT_CONFIG", "PROMPT_BATTERY_SAVER", - "PROMPT_FOR_NOTIFICATION_LOGS" + "PROMPT_FOR_NOTIFICATION_LOGS", + "DEVICE_SPECIFIC_NOTIFICATION_CONFIG" ) val publicVals: List> = RemoteConfig::class.memberProperties