mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-03-03 15:58:40 +00:00
Prompt user for debug logs with slow notifications.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<String> {
|
||||
val singleSubject = SingleSubject.create<String?>()
|
||||
submitDebugLogRepository.buildAndSubmitLog { result ->
|
||||
if (result.isPresent) {
|
||||
singleSubject.onSuccess(result.get())
|
||||
} else {
|
||||
singleSubject.onError(Throwable())
|
||||
}
|
||||
}
|
||||
|
||||
return singleSubject.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user