From 6fdb9addefd299d2a6f1f6dde8c216bec9229251 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 6 Nov 2025 16:31:03 +1100 Subject: [PATCH 1/7] Initial draft of the debug logger --- .../attachments/AvatarReuploadWorker.kt | 47 +++--- .../attachments/AvatarUploadManager.kt | 12 +- .../securesms/debugmenu/DebugActivity.kt | 4 +- .../securesms/debugmenu/DebugLogScreen.kt | 143 ++++++++++++++++++ .../securesms/debugmenu/DebugLogger.kt | 90 +++++++++++ .../securesms/debugmenu/DebugMenu.kt | 71 ++++++--- .../securesms/debugmenu/DebugMenuNavHost.kt | 111 ++++++++++++++ .../securesms/debugmenu/DebugMenuScreen.kt | 11 +- .../securesms/debugmenu/DebugMenuViewModel.kt | 40 ++++- .../prosettings/ProSettingsNavHost.kt | 2 +- .../thoughtcrime/securesms/ui/Components.kt | 2 +- .../PlayStoreSubscriptionManager.kt | 27 ++-- 12 files changed, 488 insertions(+), 72 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuNavHost.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarReuploadWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarReuploadWorker.kt index ac6369708a..cb44d1c2ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarReuploadWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarReuploadWorker.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.attachments import android.content.Context -import android.widget.Toast import androidx.compose.ui.unit.IntSize import androidx.hilt.work.HiltWorker import androidx.work.BackoffPolicy @@ -17,8 +16,6 @@ import dagger.Lazy import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import network.loki.messenger.BuildConfig import okhttp3.HttpUrl.Companion.toHttpUrl import okio.BufferedSource @@ -31,8 +28,9 @@ import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.RemoteFile.Companion.toRemoteFile import org.session.libsignal.exceptions.NonRetryableException import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.debugmenu.DebugLogGroup +import org.thoughtcrime.securesms.debugmenu.DebugLogger import org.thoughtcrime.securesms.util.BitmapUtil -import org.thoughtcrime.securesms.util.CurrentActivityObserver import org.thoughtcrime.securesms.util.DateUtils.Companion.secondsToInstant import org.thoughtcrime.securesms.util.ImageUtils import java.time.Duration @@ -55,22 +53,15 @@ class AvatarReuploadWorker @AssistedInject constructor( private val avatarUploadManager: Lazy, private val localEncryptedFileInputStreamFactory: LocalEncryptedFileInputStream.Factory, private val fileServerApi: FileServerApi, - private val prefs: TextSecurePreferences, - private val currentActivityObserver: CurrentActivityObserver, + private val debugLogger: DebugLogger ) : CoroutineWorker(context, params) { /** * Log the given message and show a toast if in debug mode */ - private suspend inline fun logAndToast(message: String, e: Throwable? = null) { - Log.d(TAG, message, e) - - val context = currentActivityObserver.currentActivity.value ?: return - if (prefs.debugAvatarReupload || BuildConfig.DEBUG) { - withContext(Dispatchers.Main) { - Toast.makeText(context, "AvatarReupload[debug only]: $message", Toast.LENGTH_SHORT).show() - } - } + private fun log(message: String, e: Throwable? = null) { + debugLogger.logD(message = "Avatar Reupload: $message", + group = DebugLogGroup.AVATAR, throwable = e) } override suspend fun doWork(): Result { @@ -79,13 +70,13 @@ class AvatarReuploadWorker @AssistedInject constructor( } if (profile == null) { - logAndToast("No profile picture set; nothing to do.") + log("No profile picture set; nothing to do.") return Result.success() } val localFile = AvatarDownloadManager.computeFileName(context, profile) if (!localFile.exists()) { - logAndToast("Avatar file is missing locally; nothing to do.") + log("Avatar file is missing locally; nothing to do.") return Result.success() } @@ -94,7 +85,7 @@ class AvatarReuploadWorker @AssistedInject constructor( // Check if the file exists and whether we need to do reprocessing, if we do, we reprocess and re-upload localEncryptedFileInputStreamFactory.create(localFile).use { stream -> if (stream.meta.hasPermanentDownloadError) { - logAndToast("Permanent download error for current avatar; nothing to do.") + log("Permanent download error for current avatar; nothing to do.") return Result.success() } @@ -103,7 +94,7 @@ class AvatarReuploadWorker @AssistedInject constructor( val source = stream.source().buffer() if ((lastUpdated != null && needsReProcessing(source)) || lastUpdated == null) { - logAndToast("About to start reuploading avatar.") + log("About to start reuploading avatar.") val attachment = attachmentProcessor.processAvatar( data = source.use { it.readByteArray() }, ) ?: return Result.failure() @@ -118,14 +109,14 @@ class AvatarReuploadWorker @AssistedInject constructor( } catch (e: CancellationException) { throw e } catch (e: NonRetryableException) { - logAndToast("Non-retryable error while reuploading avatar.", e) + log("Non-retryable error while reuploading avatar.", e) return Result.failure() } catch (e: Exception) { - logAndToast("Error while reuploading avatar.", e) + log("Error while reuploading avatar.", e) return Result.retry() } - logAndToast("Successfully reuploaded avatar.") + log("Successfully reuploaded avatar.") return Result.success() } } @@ -133,7 +124,7 @@ class AvatarReuploadWorker @AssistedInject constructor( // Otherwise, we only need to renew the same avatar on the server val parsed = fileServerApi.parseAttachmentUrl(profile.url.toHttpUrl()) - logAndToast("Renewing user avatar on ${parsed.fileServer}") + log("Renewing user avatar on ${parsed.fileServer}") try { fileServerApi.renew( fileId = parsed.fileId, @@ -149,7 +140,7 @@ class AvatarReuploadWorker @AssistedInject constructor( val now = Instant.now() if (fileExpiry?.isBefore(now) == true || (lastUpdated?.isBefore(now.minus(Duration.ofDays(12)))) == true) { - logAndToast("FileServer renew failed, trying to upload", e) + log("FileServer renew failed, trying to upload", e) val pictureData = localEncryptedFileInputStreamFactory.create(localFile).use { stream -> check(!stream.meta.hasPermanentDownloadError) { @@ -166,18 +157,18 @@ class AvatarReuploadWorker @AssistedInject constructor( } catch (e: CancellationException) { throw e } catch (e: Exception) { - logAndToast("Error while reuploading avatar after renew failed.", e) + log("Error while reuploading avatar after renew failed.", e) return Result.failure() } - logAndToast("Successfully reuploaded avatar after renew failed.") + log("Successfully reuploaded avatar after renew failed.") } else { - logAndToast( "Not reuploading avatar after renew failed; last updated too recent.") + log( "Not reuploading avatar after renew failed; last updated too recent.") } return Result.success() } else { - logAndToast("Error while renewing avatar. Retrying...", e) + log("Error while renewing avatar. Retrying...", e) return Result.retry() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarUploadManager.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarUploadManager.kt index b4bd8db154..7c1eb3797e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarUploadManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarUploadManager.kt @@ -21,6 +21,8 @@ import org.session.libsession.utilities.Util import org.session.libsession.utilities.recipients.RemoteFile import org.session.libsession.utilities.recipients.RemoteFile.Companion.toRemoteFile import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.debugmenu.DebugLogGroup +import org.thoughtcrime.securesms.debugmenu.DebugLogger import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.dependencies.OnAppStartupComponent import org.thoughtcrime.securesms.util.castAwayType @@ -42,6 +44,7 @@ class AvatarUploadManager @Inject constructor( private val localEncryptedFileOutputStreamFactory: LocalEncryptedFileOutputStream.Factory, private val fileServerApi: FileServerApi, private val attachmentProcessor: AttachmentProcessor, + private val debugLogger: DebugLogger ) : OnAppStartupComponent { init { // Manage scheduling/cancellation of the AvatarReuploadWorker based on login state @@ -99,7 +102,8 @@ class AvatarUploadManager @Inject constructor( customExpiresDuration = DEBUG_AVATAR_TTL.takeIf { prefs.forcedShortTTL() } ) - Log.d(TAG, "Avatar upload finished with $uploadResult") + debugLogger.logD(message = "Avatar upload finished with $uploadResult", + group = DebugLogGroup.AVATAR, tag = TAG) val remoteFile = RemoteFile.Encrypted(url = uploadResult.fileUrl, key = Bytes(result.key)) @@ -111,7 +115,8 @@ class AvatarUploadManager @Inject constructor( it.write(pictureData) } - Log.d(TAG, "Avatar file written to local storage") + debugLogger.logD(message = "Avatar file written to local storage", + group = DebugLogGroup.AVATAR, tag = TAG) // Now that we have the file both locally and remotely, we can update the user profile val oldPic = configFactory.withMutableUserConfigs { @@ -134,7 +139,8 @@ class AvatarUploadManager @Inject constructor( // If we had an old avatar, delete it from local storage val oldFile = AvatarDownloadManager.computeFileName(application, oldPic) if (oldFile.exists()) { - Log.d(TAG, "Deleting old avatar file: $oldFile") + debugLogger.logD(message = "Deleting old avatar file: $oldFile", + group = DebugLogGroup.AVATAR, tag = TAG) oldFile.delete() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugActivity.kt index d89c3b78c0..458968bb4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugActivity.kt @@ -9,8 +9,8 @@ class DebugActivity : FullComposeActivity() { @Composable override fun ComposeContent() { - DebugMenuScreen( - onClose = { finish() } + DebugMenuNavHost( + onBack = { finish() } ) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt new file mode 100644 index 0000000000..4c9bde6852 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt @@ -0,0 +1,143 @@ +package org.thoughtcrime.securesms.debugmenu + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import org.thoughtcrime.securesms.ui.Cell +import org.thoughtcrime.securesms.ui.components.BackAppBar +import org.thoughtcrime.securesms.ui.theme.LocalColors +import org.thoughtcrime.securesms.ui.theme.LocalDimensions +import org.thoughtcrime.securesms.ui.theme.LocalType +import org.thoughtcrime.securesms.ui.theme.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider +import org.thoughtcrime.securesms.ui.theme.ThemeColors +import org.thoughtcrime.securesms.ui.theme.bold +import org.thoughtcrime.securesms.ui.theme.monospace +import java.time.Duration +import java.time.Instant + + +@Composable +fun DebugLogScreen( + viewModel: DebugMenuViewModel, + onBack: () -> Unit, +){ + val logs by viewModel.debugLogs.collectAsState() + + DebugLogs( + logs = logs, + onBack = onBack, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DebugLogs( + logs: List, + onBack: () -> Unit, +){ + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + // App bar + BackAppBar(title = "Debug Logs", onBack = onBack) + }, + ) { contentPadding -> + val scrollState = rememberLazyListState() + + Cell( + modifier = Modifier.fillMaxSize() + .padding(contentPadding) + .padding(LocalDimensions.current.smallSpacing), + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(LocalDimensions.current.smallSpacing), + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing), + state = scrollState + ) { + items(items = logs){ log -> + Column { + Row { + Text( + text = log.formattedDate, + style = LocalType.current.small.bold() + ) + + Spacer(Modifier.width(LocalDimensions.current.xxsSpacing)) + + Text( + text = "[${log.group.label}]", + style = LocalType.current.small.bold().copy( + color = log.group.color + ) + ) + } + + Spacer(Modifier.height(2.dp)) + + Text( + text = log.message, + style = LocalType.current.large.monospace().bold() + ) + } + } + } + } + } +} + +@Preview +@Composable +fun PrewviewDebugLogs( + @PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors +) { + PreviewTheme(colors) { + DebugLogs( + logs = listOf( + DebugLogData( + message = "This is a log", + group = DebugLogGroup.PRO_SUBSCRIPTION, + date = Instant.now(), + formattedDate = "10: 36" + ), + DebugLogData( + message = "This is another log", + group = DebugLogGroup.PRO_SUBSCRIPTION, + date = Instant.now() - Duration.ofMinutes(4), + formattedDate = "10: 36" + ), + DebugLogData( + message = "This is also a log", + group = DebugLogGroup.AVATAR, + date = Instant.now() - Duration.ofMinutes(7), + formattedDate = "10: 36" + ), + ), + onBack = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt new file mode 100644 index 0000000000..c42363b011 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt @@ -0,0 +1,90 @@ +package org.thoughtcrime.securesms.debugmenu + +import android.app.Application +import android.widget.Toast +import androidx.compose.ui.graphics.Color +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.ui.theme.primaryGreen +import org.thoughtcrime.securesms.ui.theme.primaryOrange +import org.thoughtcrime.securesms.util.DateUtils +import java.time.Instant +import javax.inject.Inject +import javax.inject.Singleton + +/** + * A class that keeps track of certain logs and allows certain logs to pop as toasts + */ +@Singleton +class DebugLogger @Inject constructor( + private val application: Application, + private val prefs: TextSecurePreferences, + private val dateUtils: DateUtils +){ + private val prefPrefix: String = "debug_logger_" + + private val _logs: MutableStateFlow> = MutableStateFlow(emptyList()) + val logs: StateFlow> = _logs + + fun showGroupToast(group: DebugLogGroup, showToast: Boolean){ + prefs.setBooleanPreference(prefPrefix + group.label, showToast) + } + + fun getGroupToastPreference(group: DebugLogGroup): Boolean{ + return prefs.getBooleanPreference(prefPrefix + group.label, false) + } + + fun log(message: String, group: DebugLogGroup, tag: String = "", logSeverity: LogSeverity = LogSeverity.INFO, throwable: Throwable? = null){ + // add this message to our list + val date = Instant.now() + _logs.update { + (it + DebugLogData( + message = message, + group = group, + date = date, + formattedDate = dateUtils.getLocaleFormattedTime(date.toEpochMilli()) + )).sortedByDescending { log -> log.date } + } + + // log the message + when(logSeverity){ + LogSeverity.INFO -> Log.d(tag, message, throwable) + LogSeverity.WARNING -> Log.w(tag, message, throwable) + LogSeverity.ERROR -> Log.e(tag, message, throwable) + } + + // show this as a toast if the prefs have this group toggled + if(prefs.getBooleanPreference(prefPrefix + group.label, false)){ + Toast.makeText(application, message, Toast.LENGTH_LONG).show() + } + } + + fun logD(message: String, group: DebugLogGroup, tag: String = "", throwable: Throwable? = null){ + log(message = message, group = group, tag = tag, throwable = throwable, logSeverity = LogSeverity.INFO) + } + fun logW(message: String, group: DebugLogGroup, tag: String = "", throwable: Throwable? = null){ + log(message = message, group = group, tag = tag, throwable = throwable, logSeverity = LogSeverity.WARNING) + } + fun logE(message: String, group: DebugLogGroup, tag: String = "", throwable: Throwable? = null){ + log(message = message, group = group, tag = tag, throwable = throwable, logSeverity = LogSeverity.ERROR) + } +} + +data class DebugLogData( + val message: String, + val group: DebugLogGroup, + val date: Instant, + val formattedDate: String +) + +enum class LogSeverity{ + INFO, WARNING, ERROR +} + +enum class DebugLogGroup(val label: String, val color: Color){ + AVATAR("Avatar", primaryOrange), PRO_SUBSCRIPTION("Pro Subscription", primaryGreen) +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt index 4db3947687..3249da0ef1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt @@ -71,13 +71,15 @@ import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.DialogButtonData import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.LoadingDialog +import org.thoughtcrime.securesms.ui.components.SlimFillButtonRect import org.thoughtcrime.securesms.ui.components.BackAppBar import org.thoughtcrime.securesms.ui.components.Button import org.thoughtcrime.securesms.ui.components.ButtonType import org.thoughtcrime.securesms.ui.components.DropDown import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField import org.thoughtcrime.securesms.ui.components.SessionSwitch -import org.thoughtcrime.securesms.ui.components.SlimOutlineButton +import org.thoughtcrime.securesms.ui.components.SlimFillButtonRect +import org.thoughtcrime.securesms.ui.components.SlimFillButtonRect import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType @@ -216,17 +218,34 @@ fun DebugMenu( ) } - if (uiState.dbInspectorState != DebugMenuViewModel.DatabaseInspectorState.NOT_AVAILABLE) { - DebugCell("Database inspector") { - Button( - onClick = { - sendCommand(DebugMenuViewModel.Commands.ToggleDatabaseInspector) - }, - text = if (uiState.dbInspectorState == DebugMenuViewModel.DatabaseInspectorState.STOPPED) - "Start" - else "Stop", - type = ButtonType.AccentFill, - ) + // Debug Logger + DebugCell( + "Debug Logger", + verticalArrangement = Arrangement.spacedBy(0.dp)) { + Spacer(modifier = Modifier.height(LocalDimensions.current.xxsSpacing)) + + SlimFillButtonRect( + modifier = Modifier.fillMaxWidth(), + text = "Show Debug Logs", + ) { + sendCommand(DebugMenuViewModel.Commands.NavigateTo(DebugMenuDestination.DebugMenuLogs)) + } + + Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing)) + + Column { + DebugLogGroup.entries.forEach { logGroup -> + DebugSwitchRow( + text = "Show toasts for ${logGroup.label}", + checked = uiState.showToastForGroups[logGroup.label] == true, + onCheckedChange = { + sendCommand(DebugMenuViewModel.Commands.ToggleDebugLogGroup( + group = logGroup, + showToast = it) + ) + } + ) + } } } @@ -409,6 +428,20 @@ fun DebugMenu( } } + if (uiState.dbInspectorState != DebugMenuViewModel.DatabaseInspectorState.NOT_AVAILABLE) { + DebugCell("Database inspector") { + SlimFillButtonRect( + modifier = Modifier.fillMaxWidth(), + onClick = { + sendCommand(DebugMenuViewModel.Commands.ToggleDatabaseInspector) + }, + text = if (uiState.dbInspectorState == DebugMenuViewModel.DatabaseInspectorState.STOPPED) + "Start" + else "Stop", + ) + } + } + // Fake contacts DebugCell("Generate fake contacts") { var prefix by remember { mutableStateOf("User-") } @@ -433,7 +466,7 @@ fun DebugMenu( ) } - SlimOutlineButton(modifier = Modifier.fillMaxWidth(), text = "Generate") { + SlimFillButtonRect(modifier = Modifier.fillMaxWidth(), text = "Generate") { sendCommand( GenerateContacts( prefix = prefix, @@ -446,7 +479,7 @@ fun DebugMenu( // Session Token DebugCell("Session Token") { // Schedule a test token-drop notification for 10 seconds from now - SlimOutlineButton( + SlimFillButtonRect( modifier = Modifier.fillMaxWidth(), text = "Schedule Token Page Notification (10s)", onClick = { sendCommand(ScheduleTokenNotification) } @@ -456,7 +489,7 @@ fun DebugMenu( // Keys DebugCell("User Details") { - SlimOutlineButton ( + SlimFillButtonRect ( text = "Copy Account ID", modifier = Modifier.fillMaxWidth(), onClick = { @@ -464,7 +497,7 @@ fun DebugMenu( } ) - SlimOutlineButton( + SlimFillButtonRect( text = "Copy 07-prefixed Version Blinded Public Key", modifier = Modifier.fillMaxWidth(), onClick = { @@ -493,7 +526,7 @@ fun DebugMenu( } ) - SlimOutlineButton( + SlimFillButtonRect( modifier = Modifier.fillMaxWidth(), text = "Clear All Trusted Downloads", ) { @@ -542,14 +575,14 @@ fun DebugMenu( } ) - SlimOutlineButton( + SlimFillButtonRect( modifier = Modifier.fillMaxWidth(), text = "Reset Push Token", ) { sendCommand(DebugMenuViewModel.Commands.ResetPushToken) } - SlimOutlineButton( + SlimFillButtonRect( modifier = Modifier.fillMaxWidth(), text = "Clear All Trusted Downloads", ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuNavHost.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuNavHost.kt new file mode 100644 index 0000000000..0eb22ac784 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuNavHost.kt @@ -0,0 +1,111 @@ +package org.thoughtcrime.securesms.debugmenu + +import android.annotation.SuppressLint +import android.os.Parcelable +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.navigation +import androidx.navigation.compose.rememberNavController +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable +import org.thoughtcrime.securesms.ui.NavigationAction +import org.thoughtcrime.securesms.ui.ObserveAsEvents +import org.thoughtcrime.securesms.ui.UINavigator +import org.thoughtcrime.securesms.ui.horizontalSlideComposable + +// Destinations +sealed interface DebugMenuDestination: Parcelable { + @Serializable + @Parcelize + data object DebugMenuHome: DebugMenuDestination + + @Serializable + @Parcelize + data object DebugMenuLogs: DebugMenuDestination + +} + +@Serializable object DebugMenuGraph + +@SuppressLint("RestrictedApi") +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun DebugMenuNavHost( + startDestination: DebugMenuDestination = DebugMenuDestination.DebugMenuHome, + onBack: () -> Unit +){ + val navController = rememberNavController() + val navigator: UINavigator = remember { + UINavigator() + } + + val handleBack: () -> Unit = { + if (navController.previousBackStackEntry != null) { + navController.navigateUp() + } else { + onBack() // Finish activity if at root + } + } + + + ObserveAsEvents(flow = navigator.navigationActions) { action -> + when (action) { + is NavigationAction.Navigate -> navController.navigate( + action.destination + ) { + action.navOptions(this) + } + + NavigationAction.NavigateUp -> handleBack() + + is NavigationAction.NavigateToIntent -> { + navController.context.startActivity(action.intent) + } + + is NavigationAction.ReturnResult -> {} + } + } + + NavHost( + navController = navController, + startDestination = DebugMenuGraph + ) { + navigation(startDestination = startDestination) { + // Home + horizontalSlideComposable { entry -> + val viewModel = navController.debugGraphViewModel(entry, navigator) + + DebugMenuScreen( + viewModel = viewModel, + onBack = onBack + ) + } + + // Logs + horizontalSlideComposable { entry -> + val viewModel = navController.debugGraphViewModel(entry, navigator) + + DebugLogScreen( + viewModel = viewModel, + onBack = handleBack + ) + } + } + } +} + +@Composable +private fun NavController.debugGraphViewModel( + entry: androidx.navigation.NavBackStackEntry, + navigator: UINavigator +): DebugMenuViewModel { + val graphEntry = remember(entry) { getBackStackEntry(DebugMenuGraph) } + return hiltViewModel< + DebugMenuViewModel, + DebugMenuViewModel.Factory + >(graphEntry) { factory -> factory.create(navigator) } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuScreen.kt index 6c0f22805a..7162f89014 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuScreen.kt @@ -4,20 +4,19 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun DebugMenuScreen( modifier: Modifier = Modifier, - debugMenuViewModel: DebugMenuViewModel = viewModel(), - onClose: () -> Unit + viewModel: DebugMenuViewModel, + onBack: () -> Unit ) { - val uiState by debugMenuViewModel.uiState.collectAsState() + val uiState by viewModel.uiState.collectAsState() DebugMenu( modifier = modifier, uiState = uiState, - sendCommand = debugMenuViewModel::onCommand, - onClose = onClose + sendCommand = viewModel::onCommand, + onClose = onBack ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt index fb2b6786cf..0779a3d9b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -7,14 +7,17 @@ import android.os.Build import android.widget.Toast import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -39,17 +42,20 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.RecipientSettingsDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.tokenpage.TokenPageNotificationManager +import org.thoughtcrime.securesms.ui.UINavigator import org.thoughtcrime.securesms.util.ClearDataUtils import java.time.ZonedDateTime import javax.inject.Inject -@HiltViewModel -class DebugMenuViewModel @Inject constructor( +@HiltViewModel(assistedFactory = DebugMenuViewModel.Factory::class) +class DebugMenuViewModel @AssistedInject constructor( + @Assisted private val navigator: UINavigator, @param:ApplicationContext private val context: Context, private val textSecurePreferences: TextSecurePreferences, private val tokenPageNotificationManager: TokenPageNotificationManager, @@ -62,10 +68,16 @@ class DebugMenuViewModel @Inject constructor( private val conversationRepository: ConversationRepository, private val databaseInspector: DatabaseInspector, private val tokenFetcher: TokenFetcher, + private val debugLogger: DebugLogger, subscriptionManagers: Set<@JvmSuppressWildcards SubscriptionManager>, ) : ViewModel() { private val TAG = "DebugMenu" + @AssistedFactory + interface Factory { + fun create(navigator: UINavigator): DebugMenuViewModel + } + private val _uiState = MutableStateFlow( UIState( currentEnvironment = textSecurePreferences.getEnvironment().label, @@ -113,11 +125,14 @@ class DebugMenuViewModel @Inject constructor( withinQuickRefund = textSecurePreferences.getDebugIsWithinQuickRefund(), availableAltFileServers = TEST_FILE_SERVERS, alternativeFileServer = textSecurePreferences.alternativeFileServer, + showToastForGroups = getDebugGroupToastPref() ) ) val uiState: StateFlow get() = _uiState + val debugLogs = debugLogger.logs + init { if (databaseInspector.available) { viewModelScope.launch { @@ -368,9 +383,25 @@ class DebugMenuViewModel @Inject constructor( _uiState.update { it.copy(alternativeFileServer = command.fileServer) } textSecurePreferences.alternativeFileServer = command.fileServer } + + is Commands.NavigateTo -> { + viewModelScope.launch { + navigator.navigate(command.destination) + } + } + + is Commands.ToggleDebugLogGroup -> { + debugLogger.showGroupToast(command.group, command.showToast) + } } } + private fun getDebugGroupToastPref(): Map { + return DebugLogGroup.entries.associate { group -> + group.label to debugLogger.getGroupToastPreference(group) + } + } + private fun showEnvironmentWarningDialog(environment: String) { if(environment == _uiState.value.currentEnvironment) return val env = Environment.entries.firstOrNull { it.label == environment } ?: return @@ -482,6 +513,7 @@ class DebugMenuViewModel @Inject constructor( val withinQuickRefund: Boolean, val alternativeFileServer: FileServer? = null, val availableAltFileServers: List = emptyList(), + val showToastForGroups: Map = emptyMap(), ) enum class DatabaseInspectorState { @@ -539,6 +571,8 @@ class DebugMenuViewModel @Inject constructor( data object ToggleDebugAvatarReupload : Commands() data object ResetPushToken : Commands() data class SelectAltFileServer(val fileServer: FileServer?) : Commands() + data class NavigateTo(val destination: DebugMenuDestination) : Commands() + data class ToggleDebugLogGroup(val group: DebugLogGroup, val showToast: Boolean) : Commands() } companion object { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsNavHost.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsNavHost.kt index 82e904ad48..cc7b20e95e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsNavHost.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsNavHost.kt @@ -156,7 +156,7 @@ fun ProSettingsNavHost( } @Composable -fun NavController.proGraphViewModel( +private fun NavController.proGraphViewModel( entry: androidx.navigation.NavBackStackEntry, navigator: UINavigator ): ProSettingsViewModel { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 5b10c2c273..26239d97db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -419,7 +419,7 @@ fun Cell( modifier: Modifier = Modifier, dropShadow: Boolean = false, bgColor: Color = LocalColors.current.backgroundSecondary, - content: @Composable () -> Unit + content: @Composable BoxScope.() -> Unit ) { Box( modifier = modifier diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/pro/subscription/PlayStoreSubscriptionManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/pro/subscription/PlayStoreSubscriptionManager.kt index 106a65a4f9..2640368585 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/pro/subscription/PlayStoreSubscriptionManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/pro/subscription/PlayStoreSubscriptionManager.kt @@ -35,6 +35,9 @@ import kotlinx.coroutines.withContext import network.loki.messenger.R import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.debugmenu.DebugLogGroup +import org.thoughtcrime.securesms.debugmenu.DebugLogger +import org.thoughtcrime.securesms.debugmenu.LogSeverity import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager.PurchaseEvent import org.thoughtcrime.securesms.util.CurrentActivityObserver @@ -51,7 +54,8 @@ class PlayStoreSubscriptionManager @Inject constructor( private val application: Application, @param:ManagerScope private val scope: CoroutineScope, private val currentActivityObserver: CurrentActivityObserver, - private val prefs: TextSecurePreferences + private val prefs: TextSecurePreferences, + private val debugLogger: DebugLogger ) : SubscriptionManager { override val id = "google_play_store" override val name = "Google Play Store" @@ -84,7 +88,7 @@ class PlayStoreSubscriptionManager @Inject constructor( private val billingClient by lazy { BillingClient.newBuilder(application) .setListener { result, purchases -> - Log.d(TAG, "onPurchasesUpdated: $result, $purchases") + debugLogger.logD(message = "onPurchasesUpdated: $result, $purchases", group = DebugLogGroup.PRO_SUBSCRIPTION, tag = TAG) if (result.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { purchases.firstOrNull()?.let{ scope.launch { @@ -99,7 +103,7 @@ class PlayStoreSubscriptionManager @Inject constructor( } } } else { - Log.w(TAG, "Purchase failed or cancelled: $result") + debugLogger.logW(message = "Purchase failed or cancelled: $result", group = DebugLogGroup.PRO_SUBSCRIPTION, tag = TAG) scope.launch { _purchaseEvents.emit(PurchaseEvent.Cancelled) } @@ -156,7 +160,7 @@ class PlayStoreSubscriptionManager @Inject constructor( // If user has an existing subscription, configure upgrade/downgrade if (existingPurchase != null) { - Log.d(TAG, "Found existing subscription, configuring upgrade/downgrade with WITHOUT_PRORATION") + debugLogger.log(message = "Found existing subscription, configuring upgrade/downgrade with WITHOUT_PRORATION", group = DebugLogGroup.PRO_SUBSCRIPTION, tag = TAG) billingFlowParamsBuilder.setSubscriptionUpdateParams( BillingFlowParams.SubscriptionUpdateParams.newBuilder() @@ -183,7 +187,8 @@ class PlayStoreSubscriptionManager @Inject constructor( } catch (e: CancellationException) { throw e } catch (e: Exception) { - Log.e(TAG, "Error purchase plan", e) + debugLogger.logE(message = "Error purchase plan", group = DebugLogGroup.PRO_SUBSCRIPTION, + throwable = e, tag = TAG) withContext(Dispatchers.Main) { Toast.makeText(application, application.getString(R.string.errorGeneric), Toast.LENGTH_LONG).show() @@ -218,7 +223,8 @@ class PlayStoreSubscriptionManager @Inject constructor( } override fun onBillingSetupFinished(result: BillingResult) { - Log.d(TAG, "onBillingSetupFinished with $result") + debugLogger.log(message = "onBillingSetupFinished with $result", group = DebugLogGroup.PRO_SUBSCRIPTION, + tag = TAG) if (result.responseCode == BillingClient.BillingResponseCode.OK) { _playBillingAvailable.update { true } } @@ -243,7 +249,8 @@ class PlayStoreSubscriptionManager @Inject constructor( it.purchaseState == Purchase.PurchaseState.PURCHASED //todo PRO Should we also OR PENDING here? } } catch (e: Exception) { - Log.e(TAG, "Error querying existing subscription", e) + debugLogger.logE(message = "Error querying existing subscription", group = DebugLogGroup.PRO_SUBSCRIPTION, + throwable = e, tag = TAG) null } } @@ -278,7 +285,8 @@ class PlayStoreSubscriptionManager @Inject constructor( val productDetails = result.productDetailsList?.firstOrNull() ?: run { - Log.w(TAG, "No ProductDetails returned for product id session_pro") + debugLogger.logW(message = "No ProductDetails returned for product id session_pro", group = DebugLogGroup.PRO_SUBSCRIPTION, + tag = TAG) return emptyList() } @@ -290,7 +298,8 @@ class PlayStoreSubscriptionManager @Inject constructor( return availablePlans.mapNotNull { duration -> val offer = offersByBasePlan[duration.id] if (offer == null) { - Log.w(TAG, "No offer found for basePlanId=${duration.id}") + debugLogger.logW(message = "No offer found for basePlanId=${duration.id}", group = DebugLogGroup.PRO_SUBSCRIPTION, + tag = TAG) return@mapNotNull null } From 17655c855115528e4c8f0cdc0f406071382ba34c Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 6 Nov 2025 17:13:14 +1100 Subject: [PATCH 2/7] Added extra features: clear, copy all, long press copy row --- .../securesms/debugmenu/DebugLogScreen.kt | 119 ++++++++++++++---- .../securesms/debugmenu/DebugLogger.kt | 4 + .../securesms/debugmenu/DebugMenuViewModel.kt | 41 ++++++ 3 files changed, 138 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt index 4c9bde6852..ed559412b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt @@ -1,12 +1,14 @@ package org.thoughtcrime.securesms.debugmenu import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -20,14 +22,26 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import network.loki.messenger.R +import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsViewModel.Commands.CopyAccountId import org.thoughtcrime.securesms.ui.Cell +import org.thoughtcrime.securesms.ui.components.AccentFillButtonRect import org.thoughtcrime.securesms.ui.components.BackAppBar +import org.thoughtcrime.securesms.ui.components.DropDown +import org.thoughtcrime.securesms.ui.qaTag import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType @@ -49,6 +63,7 @@ fun DebugLogScreen( DebugLogs( logs = logs, + sendCommand = viewModel::onCommand, onBack = onBack, ) } @@ -57,6 +72,7 @@ fun DebugLogScreen( @Composable fun DebugLogs( logs: List, + sendCommand: (DebugMenuViewModel.Commands) -> Unit, onBack: () -> Unit, ){ Scaffold( @@ -68,44 +84,94 @@ fun DebugLogs( ) { contentPadding -> val scrollState = rememberLazyListState() - Cell( + Column( modifier = Modifier.fillMaxSize() .padding(contentPadding) - .padding(LocalDimensions.current.smallSpacing), + .padding(LocalDimensions.current.smallSpacing) ) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(LocalDimensions.current.smallSpacing), - verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing), - state = scrollState + var filter: DebugLogGroup? by remember { mutableStateOf(null) } + + DropDown( + selected = filter, + values = DebugLogGroup.entries, + onValueSelected = { filter = it }, + labeler = { it?.label ?: "Show All" }, + allowSelectingNullValue = true, + ) + + Spacer(Modifier.height(LocalDimensions.current.xsSpacing)) + + Cell( + modifier = Modifier.weight(1f), ) { - items(items = logs){ log -> - Column { - Row { - Text( - text = log.formattedDate, - style = LocalType.current.small.bold() - ) + val haptics = LocalHapticFeedback.current - Spacer(Modifier.width(LocalDimensions.current.xxsSpacing)) + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(LocalDimensions.current.smallSpacing), + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing), + state = scrollState + ) { + items(items = logs.filter { filter == null || it.group == filter }) { log -> + Column( + modifier = Modifier.fillMaxWidth() + .pointerInput(Unit) { + detectTapGestures( + onLongPress = { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + sendCommand(DebugMenuViewModel.Commands.CopyLog(log)) + } + ) + } + ) { + Row { + Text( + text = log.formattedDate, + style = LocalType.current.small.bold() + ) - Text( - text = "[${log.group.label}]", - style = LocalType.current.small.bold().copy( - color = log.group.color + Spacer(Modifier.width(LocalDimensions.current.xxsSpacing)) + + Text( + text = "[${log.group.label}]", + style = LocalType.current.small.bold().copy( + color = log.group.color + ) ) - ) - } + } - Spacer(Modifier.height(2.dp)) + Spacer(Modifier.height(2.dp)) - Text( - text = log.message, - style = LocalType.current.large.monospace().bold() - ) + Text( + text = log.message, + style = LocalType.current.large.monospace().bold() + ) + } } } } + + Spacer(Modifier.height(LocalDimensions.current.xsSpacing)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.xxsSpacing) + ) { + AccentFillButtonRect( + modifier = Modifier.weight(1f), + text = "Copy all logs", + onClick = { + sendCommand(DebugMenuViewModel.Commands.CopyAllLogs) + } + ) + AccentFillButtonRect( + modifier = Modifier.weight(1f), + text = "Clear logs", + onClick = { + sendCommand(DebugMenuViewModel.Commands.ClearAllDebugLogs) + } + ) + } } } } @@ -137,6 +203,7 @@ fun PrewviewDebugLogs( formattedDate = "10: 36" ), ), + sendCommand = {}, onBack = {} ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt index c42363b011..6a68553ab9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt @@ -72,6 +72,10 @@ class DebugLogger @Inject constructor( fun logE(message: String, group: DebugLogGroup, tag: String = "", throwable: Throwable? = null){ log(message = message, group = group, tag = tag, throwable = throwable, logSeverity = LogSeverity.ERROR) } + + fun clearAllLogs(){ + _logs.update { emptyList() } + } } data class DebugLogData( diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt index 0779a3d9b2..574ec5d38a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -393,6 +393,44 @@ class DebugMenuViewModel @AssistedInject constructor( is Commands.ToggleDebugLogGroup -> { debugLogger.showGroupToast(command.group, command.showToast) } + + is Commands.ClearAllDebugLogs -> { + debugLogger.clearAllLogs() + } + + is Commands.CopyAllLogs -> { + val logs = debugLogger.logs.value.joinToString("\n\n") { + "${it.formattedDate}: ${it.message}" + } + + val clip = ClipData.newPlainText("Debug Logs", logs) + clipboardManager.setPrimaryClip(ClipData(clip)) + + // Show a toast if the version is below Android 13 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + Toast.makeText( + context, + "Copied Debug Logs to clipboard", + Toast.LENGTH_SHORT + ).show() + } + } + + is Commands.CopyLog -> { + val log = "${command.log.formattedDate}: ${command.log.message}" + + val clip = ClipData.newPlainText("Debug Log", log) + clipboardManager.setPrimaryClip(ClipData(clip)) + + // Show a toast if the version is below Android 13 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + Toast.makeText( + context, + "Copied Debug Log to clipboard", + Toast.LENGTH_SHORT + ).show() + } + } } } @@ -573,6 +611,9 @@ class DebugMenuViewModel @AssistedInject constructor( data class SelectAltFileServer(val fileServer: FileServer?) : Commands() data class NavigateTo(val destination: DebugMenuDestination) : Commands() data class ToggleDebugLogGroup(val group: DebugLogGroup, val showToast: Boolean) : Commands() + data object ClearAllDebugLogs : Commands() + data object CopyAllLogs : Commands() + data class CopyLog(val log: DebugLogData) : Commands() } companion object { From b0fb60ffd155eb1325f5a26173aac41db1e3a5e9 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 6 Nov 2025 17:28:44 +1100 Subject: [PATCH 3/7] Fixing up toasts --- .../thoughtcrime/securesms/debugmenu/DebugLogger.kt | 11 +++++++++-- .../securesms/debugmenu/DebugMenuViewModel.kt | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt index 6a68553ab9..af25b1304c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt @@ -3,12 +3,16 @@ package org.thoughtcrime.securesms.debugmenu import android.app.Application import android.widget.Toast import androidx.compose.ui.graphics.Color +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.ui.theme.primaryGreen import org.thoughtcrime.securesms.ui.theme.primaryOrange import org.thoughtcrime.securesms.util.DateUtils @@ -23,7 +27,8 @@ import javax.inject.Singleton class DebugLogger @Inject constructor( private val application: Application, private val prefs: TextSecurePreferences, - private val dateUtils: DateUtils + private val dateUtils: DateUtils, + @ManagerScope private val scope: CoroutineScope ){ private val prefPrefix: String = "debug_logger_" @@ -59,7 +64,9 @@ class DebugLogger @Inject constructor( // show this as a toast if the prefs have this group toggled if(prefs.getBooleanPreference(prefPrefix + group.label, false)){ - Toast.makeText(application, message, Toast.LENGTH_LONG).show() + scope.launch(Dispatchers.Main) { + Toast.makeText(application.applicationContext, message, Toast.LENGTH_LONG).show() + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt index 574ec5d38a..01813f42b9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -392,6 +392,9 @@ class DebugMenuViewModel @AssistedInject constructor( is Commands.ToggleDebugLogGroup -> { debugLogger.showGroupToast(command.group, command.showToast) + _uiState.update { + it.copy(showToastForGroups = getDebugGroupToastPref()) + } } is Commands.ClearAllDebugLogs -> { From 2a815aaa5b617186c2b8c59cb169ea7da96598b7 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 7 Nov 2025 13:41:45 +1100 Subject: [PATCH 4/7] PR Feedback: Limiting log creation - only on subscribe now --- .../securesms/debugmenu/DebugLogScreen.kt | 12 +---- .../securesms/debugmenu/DebugLogger.kt | 53 ++++++++++++++----- .../securesms/debugmenu/DebugMenuViewModel.kt | 4 +- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt index ed559412b0..be4d8abf41 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.debugmenu -import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -17,7 +16,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -25,24 +23,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import network.loki.messenger.R -import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsViewModel.Commands.CopyAccountId +import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.components.AccentFillButtonRect import org.thoughtcrime.securesms.ui.components.BackAppBar import org.thoughtcrime.securesms.ui.components.DropDown -import org.thoughtcrime.securesms.ui.qaTag -import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.PreviewTheme @@ -59,7 +51,7 @@ fun DebugLogScreen( viewModel: DebugMenuViewModel, onBack: () -> Unit, ){ - val logs by viewModel.debugLogs.collectAsState() + val logs by viewModel.debugLogs.collectAsStateWithLifecycle(initialValue = emptyList()) DebugLogs( logs = logs, diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt index af25b1304c..9cd6725ed5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt @@ -5,9 +5,13 @@ import android.widget.Toast import androidx.compose.ui.graphics.Color import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.session.libsession.utilities.TextSecurePreferences @@ -20,6 +24,8 @@ import java.time.Instant import javax.inject.Inject import javax.inject.Singleton +private const val MAX_LOG_ENTRIES = 200 + /** * A class that keeps track of certain logs and allows certain logs to pop as toasts */ @@ -32,8 +38,22 @@ class DebugLogger @Inject constructor( ){ private val prefPrefix: String = "debug_logger_" - private val _logs: MutableStateFlow> = MutableStateFlow(emptyList()) - val logs: StateFlow> = _logs + private val buffer = ArrayDeque(MAX_LOG_ENTRIES) + + private val logChanges = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + // should only run when collected + val logSnapshots: Flow> = + logChanges + .onStart { emit(Unit) } + .map { currentSnapshot() } + + fun currentSnapshot(): List = + synchronized(buffer) { buffer.toList().asReversed() } fun showGroupToast(group: DebugLogGroup, showToast: Boolean){ prefs.setBooleanPreference(prefPrefix + group.label, showToast) @@ -45,14 +65,20 @@ class DebugLogger @Inject constructor( fun log(message: String, group: DebugLogGroup, tag: String = "", logSeverity: LogSeverity = LogSeverity.INFO, throwable: Throwable? = null){ // add this message to our list - val date = Instant.now() - _logs.update { - (it + DebugLogData( - message = message, - group = group, - date = date, - formattedDate = dateUtils.getLocaleFormattedTime(date.toEpochMilli()) - )).sortedByDescending { log -> log.date } + val now = Instant.now() + val entry = DebugLogData( + message = message, + group = group, + date = now, + formattedDate = dateUtils.getLocaleFormattedTime(now.toEpochMilli()) + ) + + scope.launch(Dispatchers.Default) { + synchronized(buffer) { + if (buffer.size == MAX_LOG_ENTRIES) buffer.removeFirst() + buffer.addLast(entry) + } + logChanges.tryEmit(Unit) } // log the message @@ -80,8 +106,11 @@ class DebugLogger @Inject constructor( log(message = message, group = group, tag = tag, throwable = throwable, logSeverity = LogSeverity.ERROR) } - fun clearAllLogs(){ - _logs.update { emptyList() } + fun clearAllLogs() { + scope.launch(Dispatchers.Default) { + synchronized(buffer) { buffer.clear() } + logChanges.tryEmit(Unit) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt index 01813f42b9..26ae413c5a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -131,7 +131,7 @@ class DebugMenuViewModel @AssistedInject constructor( val uiState: StateFlow get() = _uiState - val debugLogs = debugLogger.logs + val debugLogs = debugLogger.logSnapshots init { if (databaseInspector.available) { @@ -402,7 +402,7 @@ class DebugMenuViewModel @AssistedInject constructor( } is Commands.CopyAllLogs -> { - val logs = debugLogger.logs.value.joinToString("\n\n") { + val logs = debugLogger.currentSnapshot().joinToString("\n\n") { "${it.formattedDate}: ${it.message}" } From dbb7ef1bf3b10e562ff68e1ba0865778807d082d Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 7 Nov 2025 14:19:49 +1100 Subject: [PATCH 5/7] Making use of our existing Logging structure --- .../securesms/ApplicationContext.kt | 4 +- .../attachments/AvatarReuploadWorker.kt | 7 +- .../attachments/AvatarUploadManager.kt | 13 +-- .../securesms/debugmenu/DebugLogger.kt | 101 +++++++++--------- .../securesms/debugmenu/DebugMenuViewModel.kt | 3 +- .../PlayStoreSubscriptionManager.kt | 24 ++--- 6 files changed, 66 insertions(+), 86 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.kt b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.kt index 1ebbedd2bb..e5787d787e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.kt @@ -51,6 +51,7 @@ import org.session.libsignal.utilities.HTTP.isConnectedToNetwork import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.AppContext.configureKovenant import org.thoughtcrime.securesms.debugmenu.DebugActivity +import org.thoughtcrime.securesms.debugmenu.DebugLogger import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseModule.init import org.thoughtcrime.securesms.dependencies.OnAppStartupComponents @@ -89,6 +90,7 @@ class ApplicationContext : Application(), DefaultLifecycleObserver, Configuratio @Inject lateinit var startupComponents: Lazy @Inject lateinit var persistentLogger: Lazy + @Inject lateinit var debugLogger: Lazy @Inject lateinit var textSecurePreferences: Lazy @Inject lateinit var migrationManager: Lazy @@ -208,7 +210,7 @@ class ApplicationContext : Application(), DefaultLifecycleObserver, Configuratio } private fun initializeLogging() { - Log.initialize(AndroidLogger(), persistentLogger.get()) + Log.initialize(AndroidLogger(), persistentLogger.get(), debugLogger.get()) Logger.addLogger(object : Logger { private val tag = "LibSession" diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarReuploadWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarReuploadWorker.kt index cb44d1c2ca..0c1aca9c25 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarReuploadWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarReuploadWorker.kt @@ -29,7 +29,6 @@ import org.session.libsession.utilities.recipients.RemoteFile.Companion.toRemote import org.session.libsignal.exceptions.NonRetryableException import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.debugmenu.DebugLogGroup -import org.thoughtcrime.securesms.debugmenu.DebugLogger import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.DateUtils.Companion.secondsToInstant import org.thoughtcrime.securesms.util.ImageUtils @@ -52,16 +51,14 @@ class AvatarReuploadWorker @AssistedInject constructor( private val configFactory: ConfigFactoryProtocol, private val avatarUploadManager: Lazy, private val localEncryptedFileInputStreamFactory: LocalEncryptedFileInputStream.Factory, - private val fileServerApi: FileServerApi, - private val debugLogger: DebugLogger + private val fileServerApi: FileServerApi ) : CoroutineWorker(context, params) { /** * Log the given message and show a toast if in debug mode */ private fun log(message: String, e: Throwable? = null) { - debugLogger.logD(message = "Avatar Reupload: $message", - group = DebugLogGroup.AVATAR, throwable = e) + Log.d(DebugLogGroup.AVATAR.label, "Avatar Reupload: $message", e) } override suspend fun doWork(): Result { diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarUploadManager.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarUploadManager.kt index 7c1eb3797e..d9ffb76596 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarUploadManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/AvatarUploadManager.kt @@ -22,7 +22,6 @@ import org.session.libsession.utilities.recipients.RemoteFile import org.session.libsession.utilities.recipients.RemoteFile.Companion.toRemoteFile import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.debugmenu.DebugLogGroup -import org.thoughtcrime.securesms.debugmenu.DebugLogger import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.dependencies.OnAppStartupComponent import org.thoughtcrime.securesms.util.castAwayType @@ -43,8 +42,7 @@ class AvatarUploadManager @Inject constructor( @ManagerScope scope: CoroutineScope, private val localEncryptedFileOutputStreamFactory: LocalEncryptedFileOutputStream.Factory, private val fileServerApi: FileServerApi, - private val attachmentProcessor: AttachmentProcessor, - private val debugLogger: DebugLogger + private val attachmentProcessor: AttachmentProcessor ) : OnAppStartupComponent { init { // Manage scheduling/cancellation of the AvatarReuploadWorker based on login state @@ -102,8 +100,7 @@ class AvatarUploadManager @Inject constructor( customExpiresDuration = DEBUG_AVATAR_TTL.takeIf { prefs.forcedShortTTL() } ) - debugLogger.logD(message = "Avatar upload finished with $uploadResult", - group = DebugLogGroup.AVATAR, tag = TAG) + Log.d(DebugLogGroup.AVATAR.label, "Avatar upload finished with $uploadResult") val remoteFile = RemoteFile.Encrypted(url = uploadResult.fileUrl, key = Bytes(result.key)) @@ -115,8 +112,7 @@ class AvatarUploadManager @Inject constructor( it.write(pictureData) } - debugLogger.logD(message = "Avatar file written to local storage", - group = DebugLogGroup.AVATAR, tag = TAG) + Log.d(DebugLogGroup.AVATAR.label, "Avatar file written to local storage") // Now that we have the file both locally and remotely, we can update the user profile val oldPic = configFactory.withMutableUserConfigs { @@ -139,8 +135,7 @@ class AvatarUploadManager @Inject constructor( // If we had an old avatar, delete it from local storage val oldFile = AvatarDownloadManager.computeFileName(application, oldPic) if (oldFile.exists()) { - debugLogger.logD(message = "Deleting old avatar file: $oldFile", - group = DebugLogGroup.AVATAR, tag = TAG) + Log.d(DebugLogGroup.AVATAR.label, "Deleting old avatar file: $oldFile") oldFile.delete() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt index 9cd6725ed5..62c1c0a36b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt @@ -28,90 +28,89 @@ private const val MAX_LOG_ENTRIES = 200 /** * A class that keeps track of certain logs and allows certain logs to pop as toasts + * To use: Set the tag as one of the known [DebugLogGroup] */ @Singleton class DebugLogger @Inject constructor( - private val application: Application, + private val app: Application, private val prefs: TextSecurePreferences, private val dateUtils: DateUtils, @ManagerScope private val scope: CoroutineScope -){ +) : Log.Logger() { private val prefPrefix: String = "debug_logger_" private val buffer = ArrayDeque(MAX_LOG_ENTRIES) private val logChanges = MutableSharedFlow( - replay = 0, - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST + replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST ) - // should only run when collected + private val allowedTags: Set = + DebugLogGroup.entries.map { it.label.lowercase() }.toSet() + + private fun groupForTag(tag: String): DebugLogGroup? = + DebugLogGroup.entries.firstOrNull { it.label.equals(tag, ignoreCase = true) } + val logSnapshots: Flow> = - logChanges - .onStart { emit(Unit) } - .map { currentSnapshot() } + logChanges.onStart { emit(Unit) }.map { currentSnapshot() } fun currentSnapshot(): List = synchronized(buffer) { buffer.toList().asReversed() } - fun showGroupToast(group: DebugLogGroup, showToast: Boolean){ - prefs.setBooleanPreference(prefPrefix + group.label, showToast) + fun clearAll() { + synchronized(buffer) { buffer.clear() } + logChanges.tryEmit(Unit) } - fun getGroupToastPreference(group: DebugLogGroup): Boolean{ - return prefs.getBooleanPreference(prefPrefix + group.label, false) + fun getGroupToastPreference(group: DebugLogGroup): Boolean = + prefs.getBooleanPreference(prefPrefix + group.label, false) + + fun showGroupToast(group: DebugLogGroup, showToast: Boolean) { + prefs.setBooleanPreference(prefPrefix + group.label, showToast) } - fun log(message: String, group: DebugLogGroup, tag: String = "", logSeverity: LogSeverity = LogSeverity.INFO, throwable: Throwable? = null){ - // add this message to our list + // ---- Log.Logger overrides (no “level” logic) ---- + override fun v(tag: String, message: String?, t: Throwable?) = add(tag, message, t) + override fun d(tag: String, message: String?, t: Throwable?) = add(tag, message, t) + override fun i(tag: String, message: String?, t: Throwable?) = add(tag, message, t) + override fun w(tag: String, message: String?, t: Throwable?) = add(tag, message, t) + override fun e(tag: String, message: String?, t: Throwable?) = add(tag, message, t) + override fun wtf(tag: String, message: String?, t: Throwable?) = add(tag, message, t) + override fun blockUntilAllWritesFinished() { /* no-op */ } + + private fun add(tag: String, message: String?, t: Throwable?) { + // Capture ONLY if tag is in our allow-list + if (!allowedTags.contains(tag.lowercase())) return + + val group = groupForTag(tag) ?: return + val now = Instant.now() + val text = when { + !message.isNullOrBlank() -> message + t != null -> t.localizedMessage ?: t::class.java.simpleName + else -> "" // nothing meaningful + } + val entry = DebugLogData( - message = message, + message = text, group = group, date = now, formattedDate = dateUtils.getLocaleFormattedTime(now.toEpochMilli()) ) - scope.launch(Dispatchers.Default) { - synchronized(buffer) { - if (buffer.size == MAX_LOG_ENTRIES) buffer.removeFirst() - buffer.addLast(entry) - } - logChanges.tryEmit(Unit) - } - - // log the message - when(logSeverity){ - LogSeverity.INFO -> Log.d(tag, message, throwable) - LogSeverity.WARNING -> Log.w(tag, message, throwable) - LogSeverity.ERROR -> Log.e(tag, message, throwable) + synchronized(buffer) { + if (buffer.size == MAX_LOG_ENTRIES) buffer.removeFirst() + buffer.addLast(entry) } + logChanges.tryEmit(Unit) - // show this as a toast if the prefs have this group toggled - if(prefs.getBooleanPreference(prefPrefix + group.label, false)){ + // Toast decision is independent from capture. + if (getGroupToastPreference(group)) { scope.launch(Dispatchers.Main) { - Toast.makeText(application.applicationContext, message, Toast.LENGTH_LONG).show() + Toast.makeText(app, text, Toast.LENGTH_SHORT).show() } } } - - fun logD(message: String, group: DebugLogGroup, tag: String = "", throwable: Throwable? = null){ - log(message = message, group = group, tag = tag, throwable = throwable, logSeverity = LogSeverity.INFO) - } - fun logW(message: String, group: DebugLogGroup, tag: String = "", throwable: Throwable? = null){ - log(message = message, group = group, tag = tag, throwable = throwable, logSeverity = LogSeverity.WARNING) - } - fun logE(message: String, group: DebugLogGroup, tag: String = "", throwable: Throwable? = null){ - log(message = message, group = group, tag = tag, throwable = throwable, logSeverity = LogSeverity.ERROR) - } - - fun clearAllLogs() { - scope.launch(Dispatchers.Default) { - synchronized(buffer) { buffer.clear() } - logChanges.tryEmit(Unit) - } - } } data class DebugLogData( @@ -121,10 +120,6 @@ data class DebugLogData( val formattedDate: String ) -enum class LogSeverity{ - INFO, WARNING, ERROR -} - enum class DebugLogGroup(val label: String, val color: Color){ AVATAR("Avatar", primaryOrange), PRO_SUBSCRIPTION("Pro Subscription", primaryGreen) } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt index 26ae413c5a..82ce9b5ac2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -42,7 +42,6 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.RecipientSettingsDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.dependencies.ConfigFactory -import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager import org.thoughtcrime.securesms.repository.ConversationRepository @@ -398,7 +397,7 @@ class DebugMenuViewModel @AssistedInject constructor( } is Commands.ClearAllDebugLogs -> { - debugLogger.clearAllLogs() + debugLogger.clearAll() } is Commands.CopyAllLogs -> { diff --git a/app/src/play/kotlin/org/thoughtcrime/securesms/pro/subscription/PlayStoreSubscriptionManager.kt b/app/src/play/kotlin/org/thoughtcrime/securesms/pro/subscription/PlayStoreSubscriptionManager.kt index 2640368585..dce3d0ee46 100644 --- a/app/src/play/kotlin/org/thoughtcrime/securesms/pro/subscription/PlayStoreSubscriptionManager.kt +++ b/app/src/play/kotlin/org/thoughtcrime/securesms/pro/subscription/PlayStoreSubscriptionManager.kt @@ -36,8 +36,6 @@ import network.loki.messenger.R import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.debugmenu.DebugLogGroup -import org.thoughtcrime.securesms.debugmenu.DebugLogger -import org.thoughtcrime.securesms.debugmenu.LogSeverity import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager.PurchaseEvent import org.thoughtcrime.securesms.util.CurrentActivityObserver @@ -55,7 +53,6 @@ class PlayStoreSubscriptionManager @Inject constructor( @param:ManagerScope private val scope: CoroutineScope, private val currentActivityObserver: CurrentActivityObserver, private val prefs: TextSecurePreferences, - private val debugLogger: DebugLogger ) : SubscriptionManager { override val id = "google_play_store" override val name = "Google Play Store" @@ -88,7 +85,7 @@ class PlayStoreSubscriptionManager @Inject constructor( private val billingClient by lazy { BillingClient.newBuilder(application) .setListener { result, purchases -> - debugLogger.logD(message = "onPurchasesUpdated: $result, $purchases", group = DebugLogGroup.PRO_SUBSCRIPTION, tag = TAG) + Log.d(DebugLogGroup.PRO_SUBSCRIPTION.label, "onPurchasesUpdated: $result, $purchases") if (result.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { purchases.firstOrNull()?.let{ scope.launch { @@ -103,7 +100,7 @@ class PlayStoreSubscriptionManager @Inject constructor( } } } else { - debugLogger.logW(message = "Purchase failed or cancelled: $result", group = DebugLogGroup.PRO_SUBSCRIPTION, tag = TAG) + Log.w(DebugLogGroup.PRO_SUBSCRIPTION.label, "Purchase failed or cancelled: $result") scope.launch { _purchaseEvents.emit(PurchaseEvent.Cancelled) } @@ -160,7 +157,7 @@ class PlayStoreSubscriptionManager @Inject constructor( // If user has an existing subscription, configure upgrade/downgrade if (existingPurchase != null) { - debugLogger.log(message = "Found existing subscription, configuring upgrade/downgrade with WITHOUT_PRORATION", group = DebugLogGroup.PRO_SUBSCRIPTION, tag = TAG) + Log.d(DebugLogGroup.PRO_SUBSCRIPTION.label, "Found existing subscription, configuring upgrade/downgrade with WITHOUT_PRORATION") billingFlowParamsBuilder.setSubscriptionUpdateParams( BillingFlowParams.SubscriptionUpdateParams.newBuilder() @@ -187,8 +184,7 @@ class PlayStoreSubscriptionManager @Inject constructor( } catch (e: CancellationException) { throw e } catch (e: Exception) { - debugLogger.logE(message = "Error purchase plan", group = DebugLogGroup.PRO_SUBSCRIPTION, - throwable = e, tag = TAG) + Log.e(DebugLogGroup.PRO_SUBSCRIPTION.label, "Error purchase plan", e) withContext(Dispatchers.Main) { Toast.makeText(application, application.getString(R.string.errorGeneric), Toast.LENGTH_LONG).show() @@ -223,8 +219,7 @@ class PlayStoreSubscriptionManager @Inject constructor( } override fun onBillingSetupFinished(result: BillingResult) { - debugLogger.log(message = "onBillingSetupFinished with $result", group = DebugLogGroup.PRO_SUBSCRIPTION, - tag = TAG) + Log.d(DebugLogGroup.PRO_SUBSCRIPTION.label, "onBillingSetupFinished with $result") if (result.responseCode == BillingClient.BillingResponseCode.OK) { _playBillingAvailable.update { true } } @@ -249,8 +244,7 @@ class PlayStoreSubscriptionManager @Inject constructor( it.purchaseState == Purchase.PurchaseState.PURCHASED //todo PRO Should we also OR PENDING here? } } catch (e: Exception) { - debugLogger.logE(message = "Error querying existing subscription", group = DebugLogGroup.PRO_SUBSCRIPTION, - throwable = e, tag = TAG) + Log.e(DebugLogGroup.PRO_SUBSCRIPTION.label, "Error querying existing subscription", e) null } } @@ -285,8 +279,7 @@ class PlayStoreSubscriptionManager @Inject constructor( val productDetails = result.productDetailsList?.firstOrNull() ?: run { - debugLogger.logW(message = "No ProductDetails returned for product id session_pro", group = DebugLogGroup.PRO_SUBSCRIPTION, - tag = TAG) + Log.w(DebugLogGroup.PRO_SUBSCRIPTION.label, "No ProductDetails returned for product id session_pro") return emptyList() } @@ -298,8 +291,7 @@ class PlayStoreSubscriptionManager @Inject constructor( return availablePlans.mapNotNull { duration -> val offer = offersByBasePlan[duration.id] if (offer == null) { - debugLogger.logW(message = "No offer found for basePlanId=${duration.id}", group = DebugLogGroup.PRO_SUBSCRIPTION, - tag = TAG) + Log.w(DebugLogGroup.PRO_SUBSCRIPTION.label, "No offer found for basePlanId=${duration.id}") return@mapNotNull null } From 442e4aaab44eff91e29221ca92d3f9fa43ec901e Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 7 Nov 2025 15:28:30 +1100 Subject: [PATCH 6/7] PR feedback --- .../securesms/debugmenu/DebugLogScreen.kt | 3 ++- .../securesms/debugmenu/DebugLogger.kt | 20 +++++++++++++------ .../securesms/debugmenu/DebugMenuViewModel.kt | 4 ++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt index be4d8abf41..03948f2af3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt @@ -51,7 +51,8 @@ fun DebugLogScreen( viewModel: DebugMenuViewModel, onBack: () -> Unit, ){ - val logs by viewModel.debugLogs.collectAsStateWithLifecycle(initialValue = emptyList()) + val flowLogs = remember { viewModel.debugLogs } + val logs by flowLogs.collectAsStateWithLifecycle(initialValue = emptyList()) DebugLogs( logs = logs, diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt index 62c1c0a36b..81ce49d7b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt @@ -51,8 +51,15 @@ class DebugLogger @Inject constructor( private fun groupForTag(tag: String): DebugLogGroup? = DebugLogGroup.entries.firstOrNull { it.label.equals(tag, ignoreCase = true) } - val logSnapshots: Flow> = - logChanges.onStart { emit(Unit) }.map { currentSnapshot() } + val logSnapshots: Flow> + get() = logChanges.onStart { emit(Unit) }.map { currentSnapshot() } + + // In-memory cache for toast prefs + private val toastEnabled = java.util.EnumMap(DebugLogGroup::class.java).apply { + DebugLogGroup.entries.forEach { group -> + this[group] = prefs.getBooleanPreference(prefPrefix + group.label, false) + } + } fun currentSnapshot(): List = synchronized(buffer) { buffer.toList().asReversed() } @@ -62,13 +69,14 @@ class DebugLogger @Inject constructor( logChanges.tryEmit(Unit) } - fun getGroupToastPreference(group: DebugLogGroup): Boolean = - prefs.getBooleanPreference(prefPrefix + group.label, false) - fun showGroupToast(group: DebugLogGroup, showToast: Boolean) { + toastEnabled[group] = showToast prefs.setBooleanPreference(prefPrefix + group.label, showToast) } + fun getGroupToastPreference(group: DebugLogGroup): Boolean = + toastEnabled[group] == true + // ---- Log.Logger overrides (no “level” logic) ---- override fun v(tag: String, message: String?, t: Throwable?) = add(tag, message, t) override fun d(tag: String, message: String?, t: Throwable?) = add(tag, message, t) @@ -105,7 +113,7 @@ class DebugLogger @Inject constructor( logChanges.tryEmit(Unit) // Toast decision is independent from capture. - if (getGroupToastPreference(group)) { + if (toastEnabled[group] == true) { scope.launch(Dispatchers.Main) { Toast.makeText(app, text, Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt index 82ce9b5ac2..be23f0810c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -14,6 +14,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest @@ -49,7 +50,6 @@ import org.thoughtcrime.securesms.tokenpage.TokenPageNotificationManager import org.thoughtcrime.securesms.ui.UINavigator import org.thoughtcrime.securesms.util.ClearDataUtils import java.time.ZonedDateTime -import javax.inject.Inject @HiltViewModel(assistedFactory = DebugMenuViewModel.Factory::class) @@ -130,7 +130,7 @@ class DebugMenuViewModel @AssistedInject constructor( val uiState: StateFlow get() = _uiState - val debugLogs = debugLogger.logSnapshots + val debugLogs: Flow> get() = debugLogger.logSnapshots init { if (databaseInspector.available) { From d883edefca84222934859e800c3fea8d966ba867 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 7 Nov 2025 16:00:51 +1100 Subject: [PATCH 7/7] Local time formatting in compose - fine for debug screen --- .../securesms/debugmenu/DebugLogScreen.kt | 14 ++++++++++---- .../securesms/debugmenu/DebugLogger.kt | 2 -- .../securesms/debugmenu/DebugMenuViewModel.kt | 6 ++++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt index 03948f2af3..bc197eb605 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogScreen.kt @@ -42,8 +42,12 @@ import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors import org.thoughtcrime.securesms.ui.theme.bold import org.thoughtcrime.securesms.ui.theme.monospace +import org.thoughtcrime.securesms.util.DateUtils import java.time.Duration import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Locale @Composable @@ -118,8 +122,13 @@ fun DebugLogs( } ) { Row { + val locale = remember(Unit) { Locale.getDefault() } + val formatter = remember(Unit){ DateTimeFormatter.ofPattern("HH:mm", locale)} + Text( - text = log.formattedDate, + text = Instant.ofEpochMilli(log.date.toEpochMilli()) + .atZone(ZoneId.systemDefault()) + .format(formatter), style = LocalType.current.small.bold() ) @@ -181,19 +190,16 @@ fun PrewviewDebugLogs( message = "This is a log", group = DebugLogGroup.PRO_SUBSCRIPTION, date = Instant.now(), - formattedDate = "10: 36" ), DebugLogData( message = "This is another log", group = DebugLogGroup.PRO_SUBSCRIPTION, date = Instant.now() - Duration.ofMinutes(4), - formattedDate = "10: 36" ), DebugLogData( message = "This is also a log", group = DebugLogGroup.AVATAR, date = Instant.now() - Duration.ofMinutes(7), - formattedDate = "10: 36" ), ), sendCommand = {}, diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt index 81ce49d7b3..e1a0421c1a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugLogger.kt @@ -103,7 +103,6 @@ class DebugLogger @Inject constructor( message = text, group = group, date = now, - formattedDate = dateUtils.getLocaleFormattedTime(now.toEpochMilli()) ) synchronized(buffer) { @@ -125,7 +124,6 @@ data class DebugLogData( val message: String, val group: DebugLogGroup, val date: Instant, - val formattedDate: String ) enum class DebugLogGroup(val label: String, val color: Color){ diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt index be23f0810c..ad9d31c0ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.tokenpage.TokenPageNotificationManager import org.thoughtcrime.securesms.ui.UINavigator import org.thoughtcrime.securesms.util.ClearDataUtils +import org.thoughtcrime.securesms.util.DateUtils import java.time.ZonedDateTime @@ -68,6 +69,7 @@ class DebugMenuViewModel @AssistedInject constructor( private val databaseInspector: DatabaseInspector, private val tokenFetcher: TokenFetcher, private val debugLogger: DebugLogger, + private val dateUtils: DateUtils, subscriptionManagers: Set<@JvmSuppressWildcards SubscriptionManager>, ) : ViewModel() { private val TAG = "DebugMenu" @@ -402,7 +404,7 @@ class DebugMenuViewModel @AssistedInject constructor( is Commands.CopyAllLogs -> { val logs = debugLogger.currentSnapshot().joinToString("\n\n") { - "${it.formattedDate}: ${it.message}" + "${dateUtils.getLocaleFormattedTime(it.date.toEpochMilli())}: ${it.message}" } val clip = ClipData.newPlainText("Debug Logs", logs) @@ -419,7 +421,7 @@ class DebugMenuViewModel @AssistedInject constructor( } is Commands.CopyLog -> { - val log = "${command.log.formattedDate}: ${command.log.message}" + val log = "${dateUtils.getLocaleFormattedTime(command.log.date.toEpochMilli())}: ${command.log.message}" val clip = ClipData.newPlainText("Debug Log", log) clipboardManager.setPrimaryClip(ClipData(clip))