Update device-specific notification support configs.
This commit is contained in:
parent
60a0565ba8
commit
9024c19169
14 changed files with 319 additions and 131 deletions
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<LocalMetricsDatabase.EventMetrics>, minimumEventAgeMs: Long, minimumEventCount: Int, failurePercentage: Float): Boolean {
|
||||
|
|
|
@ -46,17 +46,29 @@ class VitalsViewModel(private val context: Application) : AndroidViewModel(conte
|
|||
private fun checkHeuristics(): Single<State> {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -1097,6 +1097,15 @@
|
|||
<!-- Message explaining that battery saver may delay notifications -->
|
||||
<string name="PromptBatterySaverBottomSheet__message">You can disable battery optimizations for Signal to ensure that message notifications will not be delayed.</string>
|
||||
|
||||
<!-- Title in bottom sheet that states that notifications for this device could be delayed -->
|
||||
<string name="DeviceSpecificNotificationBottomSheet__notifications_may_be_delayed">Notifications may be delayed due to battery optimizations</string>
|
||||
<!-- Message in bottom sheet prompting users to fix potential issues by disabling battery optimizations -->
|
||||
<string name="DeviceSpecificNotificationBottomSheet__disable_battery_optimizations">Disable battery optimizations for Signal to ensure that message notifications will not be delayed. Tap “Continue” to see device-specific instructions.</string>
|
||||
<!-- Button to dismiss notification help prompt -->
|
||||
<string name="DeviceSpecificNotificationBottomSheet__no_thanks">No thanks</string>
|
||||
<!-- Button to continue and go to Signal support website -->
|
||||
<string name="DeviceSpecificNotificationBottomSheet__continue">Continue</string>
|
||||
|
||||
<!-- Button to continue to try and disable battery saver -->
|
||||
<string name="PromptBatterySaverBottomSheet__continue">Continue</string>
|
||||
<!-- Button to dismiss battery saver dialog prompt-->
|
||||
|
|
|
@ -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 = "*")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<KProperty1<*, *>> = RemoteConfig::class.memberProperties
|
||||
|
|
Loading…
Add table
Reference in a new issue