diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java index dbf666eb3a..96f4c7cc2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java @@ -15,12 +15,14 @@ import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment; 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.SlowNotificationHeuristics; import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository; import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel; import org.thoughtcrime.securesms.util.AppStartup; @@ -136,6 +138,10 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot } updateTabVisibility(); + + if (SlowNotificationHeuristics.shouldPromptUserForLogs()) { + DebugLogsPromptDialogFragment.show(this, getSupportFragmentManager()); + } } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DebugLogsPromptDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/DebugLogsPromptDialogFragment.kt new file mode 100644 index 0000000000..81d5a05b03 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/DebugLogsPromptDialogFragment.kt @@ -0,0 +1,103 @@ +/* + * 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 android.widget.Toast +import androidx.core.os.bundleOf +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.ViewModelProvider +import org.signal.core.util.ResourceUtil +import org.signal.core.util.concurrent.LifecycleDisposable +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.databinding.PromptLogsBottomSheetBinding +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.util.BottomSheetUtil +import org.thoughtcrime.securesms.util.CommunicationActions +import org.thoughtcrime.securesms.util.NetworkUtil +import org.thoughtcrime.securesms.util.SupportEmailUtil + +class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() { + + companion object { + + @JvmStatic + fun show(context: Context, fragmentManager: FragmentManager) { + if (NetworkUtil.isConnected(context) && fragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) == null) { + DebugLogsPromptDialogFragment().apply { + arguments = bundleOf() + }.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + SignalStore.uiHints().lastNotificationLogsPrompt = System.currentTimeMillis() + } + } + } + + override val peekHeightPercentage: Float = 0.66f + override val themeResId: Int = R.style.Widget_Signal_FixedRoundedCorners_Messages + + private val binding by ViewBinderDelegate(PromptLogsBottomSheetBinding::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_logs_bottom_sheet, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + disposables.bindTo(viewLifecycleOwner) + + viewModel = ViewModelProvider(this).get(PromptLogsViewModel::class.java) + binding.submit.setOnClickListener { + val progressDialog = SignalProgressDialog.show(requireContext()) + disposables += viewModel.submitLogs().subscribe({ result -> + submitLogs(result) + progressDialog.dismiss() + dismiss() + }, { _ -> + Toast.makeText(requireContext(), getString(R.string.HelpFragment__could_not_upload_logs), Toast.LENGTH_LONG).show() + progressDialog.dismiss() + dismiss() + }) + } + binding.decline.setOnClickListener { + SignalStore.uiHints().markDeclinedShareNotificationLogs() + dismiss() + } + } + + private fun submitLogs(debugLog: String) { + CommunicationActions.openEmail( + requireContext(), + SupportEmailUtil.getSupportEmailAddress(requireContext()), + getString(R.string.DebugLogsPromptDialogFragment__signal_android_support_request), + getEmailBody(debugLog) + ) + } + + private fun getEmailBody(debugLog: String?): String { + val suffix = StringBuilder() + if (debugLog != null) { + suffix.append("\n") + suffix.append(getString(R.string.HelpFragment__debug_log)) + suffix.append(" ") + suffix.append(debugLog) + } + val category = ResourceUtil.getEnglishResources(requireContext()).getString(R.string.DebugLogsPromptDialogFragment__slow_notifications_category) + return SupportEmailUtil.generateSupportEmailBody( + requireContext(), + R.string.DebugLogsPromptDialogFragment__signal_android_support_request, + " - $category", + "\n\n", + suffix.toString() + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/PromptLogsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/PromptLogsViewModel.kt new file mode 100644 index 0000000000..1d4fce8a82 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/PromptLogsViewModel.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components + +import androidx.lifecycle.ViewModel +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import io.reactivex.rxjava3.subjects.SingleSubject +import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository + +class PromptLogsViewModel : ViewModel() { + + private val submitDebugLogRepository = SubmitDebugLogRepository() + + fun submitLogs(): Single { + val singleSubject = SingleSubject.create() + submitDebugLogRepository.buildAndSubmitLog { result -> + if (result.isPresent) { + singleSubject.onSuccess(result.get()) + } else { + singleSubject.onError(Throwable()) + } + } + + return singleSubject.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java index bfaee3ef08..24ffb06192 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java @@ -18,6 +18,8 @@ public class UiHints extends SignalStoreValues { private static final String HAS_SEEN_TEXT_FORMATTING_ALERT = "uihints.text_formatting.has_seen_alert"; private static final String HAS_NOT_SEEN_EDIT_MESSAGE_BETA_ALERT = "uihints.edit_message.has_not_seen_beta_alert"; 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"; UiHints(@NonNull KeyValueStore store) { super(store); @@ -118,4 +120,20 @@ public class UiHints extends SignalStoreValues { public void markHasSeenSafetyNumberUpdateNux() { putBoolean(HAS_SEEN_SAFETY_NUMBER_NUX, true); } + + public long getLastNotificationLogsPrompt() { + return getLong(LAST_NOTIFICATION_LOGS_PROMPT_TIME, 0); + } + + public void setLastNotificationLogsPrompt(long timeMs) { + putLong(LAST_NOTIFICATION_LOGS_PROMPT_TIME, timeMs); + } + + public void markDeclinedShareNotificationLogs() { + putBoolean(DECLINED_NOTIFICATION_LOGS_PROMPT, true); + } + + public boolean hasDeclinedToShareNotificationLogs() { + return getBoolean(DECLINED_NOTIFICATION_LOGS_PROMPT, false); + } } 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 9e6960c1e5..464507bf17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SlowNotificationHeuristics.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SlowNotificationHeuristics.kt @@ -5,10 +5,18 @@ package org.thoughtcrime.securesms.notifications +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.FeatureFlags +import org.thoughtcrime.securesms.util.JsonUtils +import org.thoughtcrime.securesms.util.LocaleFeatureFlags import org.thoughtcrime.securesms.util.SignalLocalMetrics +import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours /** * Heuristic for estimating if a user has been experiencing issues with delayed notifications. @@ -22,6 +30,44 @@ object SlowNotificationHeuristics { private val TAG = Log.tag(SlowNotificationHeuristics::class.java) + fun getConfiguration(): Configuration { + val json = FeatureFlags.delayedNotificationsPromptConfig() + return if (TextUtils.isEmpty(json)) { + getDefaultConfiguration() + } else { + try { + JsonUtils.fromJson(json, Configuration::class.java) + } catch (exception: Exception) { + getDefaultConfiguration() + } + } + } + + private fun getDefaultConfiguration(): Configuration { + return Configuration( + minimumEventAgeMs = 3.days.inWholeMilliseconds, + minimumServiceEventCount = 10, + serviceStartFailurePercentage = 0.5f, + messageLatencyPercentage = 75, + messageLatencyThreshold = 6.hours.inWholeMilliseconds, + minimumMessageLatencyEvents = 50, + weeklyFailedQueueDrains = 5 + ) + } + + @JvmStatic + fun shouldPromptUserForLogs(): Boolean { + if (!LocaleFeatureFlags.isDelayedNotificationPromptEnabled() || SignalStore.uiHints().hasDeclinedToShareNotificationLogs()) { + return false + } + if (System.currentTimeMillis() - SignalStore.uiHints().lastNotificationLogsPrompt < TimeUnit.DAYS.toMillis(7)) { + return false + } + val configuration = getConfiguration() + + return isHavingDelayedNotifications(configuration) + } + fun isHavingDelayedNotifications(configuration: Configuration): Boolean { val db = LocalMetricsDatabase.getInstance(ApplicationDependencies.getApplication()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 5eee6c351c..19850bc845 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.groups.SelectionLimits; import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver; +import org.thoughtcrime.securesms.notifications.Configuration; import org.whispersystems.signalservice.api.RemoteConfigResult; import java.io.IOException; @@ -109,6 +110,8 @@ public final class FeatureFlags { private static final String CDS_DISABLE_COMPAT_MODE = "cds.disableCompatibilityMode"; private static final String FCM_MAY_HAVE_MESSAGES_KILL_SWITCH = "android.fcmNotificationFallbackKillSwitch"; 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"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -169,7 +172,9 @@ public final class FeatureFlags { SVR2_KILLSWITCH, CDS_DISABLE_COMPAT_MODE, SAFETY_NUMBER_ACI, - FCM_MAY_HAVE_MESSAGES_KILL_SWITCH + FCM_MAY_HAVE_MESSAGES_KILL_SWITCH, + PROMPT_FOR_NOTIFICATION_LOGS, + PROMPT_FOR_NOTIFICATION_CONFIG ); @VisibleForTesting @@ -236,7 +241,9 @@ public final class FeatureFlags { SVR2_KILLSWITCH, CDS_DISABLE_COMPAT_MODE, SAFETY_NUMBER_ACI, - FCM_MAY_HAVE_MESSAGES_KILL_SWITCH + FCM_MAY_HAVE_MESSAGES_KILL_SWITCH, + PROMPT_FOR_NOTIFICATION_LOGS, + PROMPT_FOR_NOTIFICATION_CONFIG ); /** @@ -618,6 +625,13 @@ public final class FeatureFlags { } } + public static String promptForDelayedNotificationLogs() { + return getString(PROMPT_FOR_NOTIFICATION_LOGS, "*"); + } + + public static String delayedNotificationsPromptConfig() { + return getString(PROMPT_FOR_NOTIFICATION_CONFIG, ""); + } /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LocaleFeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/LocaleFeatureFlags.java index 38dc886a4d..8f790694aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LocaleFeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LocaleFeatureFlags.java @@ -65,6 +65,9 @@ public final class LocaleFeatureFlags { return isEnabled(FeatureFlags.PAYPAL_DISABLED_REGIONS, FeatureFlags.paypalDisabledRegions()); } + public static boolean isDelayedNotificationPromptEnabled() { + return isEnabled(FeatureFlags.PROMPT_FOR_NOTIFICATION_LOGS, FeatureFlags.promptForDelayedNotificationLogs()); + } /** * 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/NetworkUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/NetworkUtil.java index b1ac7d9327..43c7d3d350 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/NetworkUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/NetworkUtil.java @@ -33,6 +33,11 @@ public final class NetworkUtil { return info != null && info.isConnected() && info.isRoaming() && info.getType() == ConnectivityManager.TYPE_MOBILE; } + public static boolean isConnected(@NonNull Context context) { + final NetworkInfo info = getNetworkInfo(context); + return info != null && info.isConnected(); + } + public static @NonNull CallManager.DataMode getCallingDataMode(@NonNull Context context) { return getCallingDataMode(context, PeerConnection.AdapterType.UNKNOWN); } diff --git a/app/src/main/res/drawable/ic_debug_log.xml b/app/src/main/res/drawable/ic_debug_log.xml new file mode 100644 index 0000000000..1c38efaf9a --- /dev/null +++ b/app/src/main/res/drawable/ic_debug_log.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/prompt_logs_bottom_sheet.xml b/app/src/main/res/layout/prompt_logs_bottom_sheet.xml new file mode 100644 index 0000000000..b5e967e019 --- /dev/null +++ b/app/src/main/res/layout/prompt_logs_bottom_sheet.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 917f8dab0e..e17a8a49aa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -905,6 +905,11 @@ %1$d members + + We noticed notifications are delayed. Submit debug log? + + Debug logs helps us diagnose and fix the issue, and do not contain identifying information. + Pending group invites Requests @@ -2866,6 +2871,14 @@ Donations & Badges SMS Export + + Signal Android Debug Log Submission + + Slow notifications + + Submit + + No thanks This Message