mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-03-01 22:22:15 +00:00
Add prompt to help troubleshoot slow notifications.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(" ")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user