Add prompt to help troubleshoot slow notifications.

This commit is contained in:
Clark
2023-08-15 12:31:54 -04:00
committed by Cody Henthorne
parent 98ec2cceb4
commit 4cbcee85d6
16 changed files with 329 additions and 10 deletions

View File

@@ -90,6 +90,8 @@ import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.PowerManagerCompat;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
import org.thoughtcrime.securesms.util.TextSecurePreferences;

View File

@@ -4,6 +4,7 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewTreeObserver;
@@ -16,12 +17,14 @@ import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment;
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSheetFragment;
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.SlowNotificationHeuristics;
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository;
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel;
@@ -30,6 +33,7 @@ import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.PowerManagerCompat;
import org.thoughtcrime.securesms.util.SplashScreenUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
@@ -139,8 +143,14 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
updateTabVisibility();
if (SlowNotificationHeuristics.shouldPromptUserForLogs()) {
DebugLogsPromptDialogFragment.show(this, getSupportFragmentManager());
if (SlowNotificationHeuristics.isHavingDelayedNotifications()) {
if (SlowNotificationHeuristics.isPotentiallyCausedByBatteryOptimizations() && Build.VERSION.SDK_INT >= 23) {
if (SlowNotificationHeuristics.shouldPromptBatterySaver()) {
PromptBatterySaverDialogFragment.show(this, getSupportFragmentManager());
}
} else if (SlowNotificationHeuristics.shouldPromptUserForLogs()) {
DebugLogsPromptDialogFragment.show(this, getSupportFragmentManager());
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.ViewModelProvider
import org.signal.core.util.concurrent.LifecycleDisposable
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.databinding.PromptBatterySaverBottomSheetBinding
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.PowerManagerCompat
@RequiresApi(23)
class PromptBatterySaverDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
companion object {
@JvmStatic
fun show(context: Context, fragmentManager: FragmentManager) {
if (fragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) == null) {
PromptBatterySaverDialogFragment().apply {
arguments = bundleOf()
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
SignalStore.uiHints().lastBatterySaverPrompt = System.currentTimeMillis()
}
}
}
override val peekHeightPercentage: Float = 0.66f
override val themeResId: Int = R.style.Widget_Signal_FixedRoundedCorners_Messages
private val binding by ViewBinderDelegate(PromptBatterySaverBottomSheetBinding::bind)
private lateinit var viewModel: PromptLogsViewModel
private val disposables: LifecycleDisposable = LifecycleDisposable()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.prompt_battery_saver_bottom_sheet, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
disposables.bindTo(viewLifecycleOwner)
viewModel = ViewModelProvider(this)[PromptLogsViewModel::class.java]
binding.continueButton.setOnClickListener {
PowerManagerCompat.requestIgnoreBatteryOptimizations(requireContext())
}
binding.dismissButton.setOnClickListener {
SignalStore.uiHints().markDismissedBatterySaverPrompt()
dismiss()
}
}
}

View File

@@ -22,6 +22,7 @@ import androidx.preference.PreferenceManager
import org.signal.core.util.getParcelableExtraCompat
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
@@ -184,6 +185,16 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
}
)
if (Build.VERSION.SDK_INT >= 23 && state.messageNotificationsState.troubleshootNotifications) {
clickPref(
title = DSLSettingsText.from(R.string.preferences_notifications__troubleshoot),
isEnabled = true,
onClick = {
PromptBatterySaverDialogFragment.show(requireContext(), childFragmentManager)
}
)
}
if (Build.VERSION.SDK_INT < 30) {
if (NotificationChannels.supported()) {
clickPref(

View File

@@ -17,7 +17,8 @@ data class MessageNotificationsState(
val inChatSoundsEnabled: Boolean,
val repeatAlerts: Int,
val messagePrivacy: String,
val priority: Int
val priority: Int,
val troubleshootNotifications: Boolean
)
data class CallNotificationsState(

View File

@@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.SlowNotificationHeuristics
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.livedata.Store
@@ -104,7 +105,8 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer
inChatSoundsEnabled = SignalStore.settings().isMessageNotificationsInChatSoundsEnabled,
repeatAlerts = SignalStore.settings().messageNotificationsRepeatAlerts,
messagePrivacy = SignalStore.settings().messageNotificationsPrivacy.toString(),
priority = TextSecurePreferences.getNotificationPriority(ApplicationDependencies.getApplication())
priority = TextSecurePreferences.getNotificationPriority(ApplicationDependencies.getApplication()),
troubleshootNotifications = SlowNotificationHeuristics.isPotentiallyCausedByBatteryOptimizations() && SlowNotificationHeuristics.isHavingDelayedNotifications()
),
callNotificationsState = CallNotificationsState(
notificationsEnabled = SignalStore.settings().isCallNotificationsEnabled,

View File

@@ -20,6 +20,8 @@ public class UiHints extends SignalStoreValues {
private static final String HAS_SEEN_SAFETY_NUMBER_NUX = "uihints.has_seen_safety_number_nux";
private static final String DECLINED_NOTIFICATION_LOGS_PROMPT = "uihints.declined_notification_logs";
private static final String LAST_NOTIFICATION_LOGS_PROMPT_TIME = "uihints.last_notification_logs_prompt";
private static final String DISMISSED_BATTERY_SAVER_PROMPT = "uihints.declined_battery_saver_prompt";
private static final String LAST_BATTERY_SAVER_PROMPT = "uihints.last_battery_saver_prompt";
UiHints(@NonNull KeyValueStore store) {
super(store);
@@ -136,4 +138,20 @@ public class UiHints extends SignalStoreValues {
public boolean hasDeclinedToShareNotificationLogs() {
return getBoolean(DECLINED_NOTIFICATION_LOGS_PROMPT, false);
}
public void markDismissedBatterySaverPrompt() {
putBoolean(DISMISSED_BATTERY_SAVER_PROMPT, true);
}
public boolean hasDismissedBatterySaverPrompt() {
return getBoolean(DISMISSED_BATTERY_SAVER_PROMPT, false);
}
public long getLastBatterySaverPrompt() {
return getLong(LAST_BATTERY_SAVER_PROMPT, 0);
}
public void setLastBatterySaverPrompt(long time) {
putLong(LAST_BATTERY_SAVER_PROMPT, time);
}
}

View File

@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.emoji.EmojiFiles;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
import org.thoughtcrime.securesms.notifications.SlowNotificationHeuristics;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.util.AppSignatureUtil;
@@ -88,7 +89,10 @@ public class LogSectionSystemInfo implements LogSection {
builder.append("Server Time Offset: ").append(SignalStore.misc().getLastKnownServerTimeOffset()).append(" ms (last updated: ").append(SignalStore.misc().getLastKnownServerTimeOffsetUpdateTime()).append(")").append("\n");
builder.append("Telecom : ").append(AndroidTelecomUtil.getTelecomSupported()).append("\n");
builder.append("User-Agent : ").append(StandardUserAgentInterceptor.USER_AGENT).append("\n");
builder.append("SlowNotifications : ").append(SlowNotificationHeuristics.isHavingDelayedNotifications()).append("\n");
builder.append("PotentiallyBattery: ").append(SlowNotificationHeuristics.isPotentiallyCausedByBatteryOptimizations()).append("\n");
builder.append("App : ");
try {
builder.append(pm.getApplicationLabel(pm.getApplicationInfo(context.getPackageName(), 0)))
.append(" ")

View File

@@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDona
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;

View File

@@ -5,14 +5,17 @@
package org.thoughtcrime.securesms.notifications
import android.os.Build
import android.text.TextUtils
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.DeviceProperties
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.JsonUtils
import org.thoughtcrime.securesms.util.LocaleFeatureFlags
import org.thoughtcrime.securesms.util.PowerManagerCompat
import org.thoughtcrime.securesms.util.SignalLocalMetrics
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.days
@@ -63,12 +66,34 @@ object SlowNotificationHeuristics {
if (System.currentTimeMillis() - SignalStore.uiHints().lastNotificationLogsPrompt < TimeUnit.DAYS.toMillis(7)) {
return false
}
val configuration = getConfiguration()
return isHavingDelayedNotifications(configuration)
return true
}
fun isHavingDelayedNotifications(configuration: Configuration): Boolean {
@JvmStatic
fun shouldPromptBatterySaver(): Boolean {
if (Build.VERSION.SDK_INT < 23) {
return false
}
if (!LocaleFeatureFlags.isBatterySaverPromptEnabled() || SignalStore.uiHints().hasDismissedBatterySaverPrompt()) {
return false
}
if (System.currentTimeMillis() - SignalStore.uiHints().lastBatterySaverPrompt < TimeUnit.DAYS.toMillis(7)) {
return false
}
return true
}
@JvmStatic
fun isHavingDelayedNotifications(): Boolean {
if (!SignalStore.settings().isMessageNotificationsEnabled ||
!NotificationChannels.getInstance().areNotificationsEnabled()
) {
// If user does not have notifications enabled, we shouldn't bother them about delayed notifications
return false
}
val configuration = getConfiguration()
val db = LocalMetricsDatabase.getInstance(ApplicationDependencies.getApplication())
val metrics = db.getMetrics()
@@ -84,6 +109,32 @@ object SlowNotificationHeuristics {
return false
}
/**
* Returns whether or not the delayed notifications may be due to battery saver optimizations.
*
* Some OEMs over-optimize this battery restrictions and remove network even after receiving a
* high priority push.
*
* We consider a scenario where removing battery optimizations can fix delayed notifications:
* - Data saver must be off (or white listed), otherwise it can be causing delayed notifications
* - App must not already be exempted from battery optimizations
*
* We do not need to check if {ActivityManager#isBackgroundRestricted} is true, because if the app
* is set to "Optimized" this will be false (and can be culprit to delayed notifications) or if
* true can most definitely be at fault.
*/
@JvmStatic
fun isPotentiallyCausedByBatteryOptimizations(): Boolean {
val applicationContext = ApplicationDependencies.getApplication()
if (DeviceProperties.getDataSaverState(applicationContext) == DeviceProperties.DataSaverState.ENABLED) {
return false
}
if (PowerManagerCompat.isIgnoringBatteryOptimizations(applicationContext)) {
return false
}
return true
}
private fun hasRepeatedFailedServiceStarts(metrics: List<LocalMetricsDatabase.EventMetrics>, minimumEventAgeMs: Long, minimumEventCount: Int, failurePercentage: Float): Boolean {
if (!haveEnoughData(SignalLocalMetrics.FcmServiceStartSuccess.NAME, minimumEventAgeMs) && !haveEnoughData(SignalLocalMetrics.FcmServiceStartFailure.NAME, minimumEventAgeMs)) {
Log.d(TAG, "insufficient data for service starts")

View File

@@ -111,6 +111,7 @@ public final class FeatureFlags {
private static final String SAFETY_NUMBER_ACI = "global.safetyNumberAci";
public static final String PROMPT_FOR_NOTIFICATION_LOGS = "android.logs.promptNotifications";
private static final String PROMPT_FOR_NOTIFICATION_CONFIG = "android.logs.promptNotificationsConfig";
public static final String PROMPT_BATTERY_SAVER = "android.promptBatterySaver";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -173,7 +174,8 @@ public final class FeatureFlags {
SAFETY_NUMBER_ACI,
FCM_MAY_HAVE_MESSAGES_KILL_SWITCH,
PROMPT_FOR_NOTIFICATION_LOGS,
PROMPT_FOR_NOTIFICATION_CONFIG
PROMPT_FOR_NOTIFICATION_CONFIG,
PROMPT_BATTERY_SAVER
);
@VisibleForTesting
@@ -242,7 +244,8 @@ public final class FeatureFlags {
SAFETY_NUMBER_ACI,
FCM_MAY_HAVE_MESSAGES_KILL_SWITCH,
PROMPT_FOR_NOTIFICATION_LOGS,
PROMPT_FOR_NOTIFICATION_CONFIG
PROMPT_FOR_NOTIFICATION_CONFIG,
PROMPT_BATTERY_SAVER
);
/**
@@ -631,6 +634,11 @@ public final class FeatureFlags {
public static String delayedNotificationsPromptConfig() {
return getString(PROMPT_FOR_NOTIFICATION_CONFIG, "");
}
public static String promptBatterySaver() {
return getString(PROMPT_BATTERY_SAVER, "*");
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View File

@@ -68,6 +68,11 @@ public final class LocaleFeatureFlags {
public static boolean isDelayedNotificationPromptEnabled() {
return isEnabled(FeatureFlags.PROMPT_FOR_NOTIFICATION_LOGS, FeatureFlags.promptForDelayedNotificationLogs());
}
public static boolean isBatterySaverPromptEnabled() {
return isEnabled(FeatureFlags.PROMPT_BATTERY_SAVER, FeatureFlags.PROMPT_BATTERY_SAVER);
}
/**
* Parses a comma-separated list of country codes colon-separated from how many buckets out of 1 million
* should be enabled to see this megaphone in that country code. At the end of the list, an optional

View File

@@ -19,6 +19,13 @@ public class PowerManagerCompat {
return false;
}
public static boolean isIgnoringBatteryOptimizations(@NonNull Context context) {
if (Build.VERSION.SDK_INT < 23) {
return true;
}
return ServiceUtil.getPowerManager(context).isIgnoringBatteryOptimizations(context.getPackageName());
}
@RequiresApi(api = 23)
public static void requestIgnoreBatteryOptimizations(@NonNull Context context) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,