diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 7d3f3e01ec..da7bafa894 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -29,12 +29,14 @@ import org.signal.core.util.bytes import org.signal.core.util.concurrent.LimitedWorker import org.signal.core.util.concurrent.SignalDispatchers import org.signal.core.util.concurrent.SignalExecutors +import org.signal.core.util.decodeOrNull import org.signal.core.util.forceForeignKeyConstraintsEnabled import org.signal.core.util.fullWalCheckpoint import org.signal.core.util.getAllIndexDefinitions import org.signal.core.util.getAllTableDefinitions import org.signal.core.util.getAllTriggerDefinitions import org.signal.core.util.getForeignKeyViolations +import org.signal.core.util.isNotEmpty import org.signal.core.util.logging.Log import org.signal.core.util.money.FiatMoney import org.signal.core.util.requireIntOrNull @@ -62,6 +64,7 @@ import org.thoughtcrime.securesms.backup.v2.processor.ChatItemArchiveProcessor import org.thoughtcrime.securesms.backup.v2.processor.NotificationProfileProcessor import org.thoughtcrime.securesms.backup.v2.processor.RecipientArchiveProcessor import org.thoughtcrime.securesms.backup.v2.processor.StickerArchiveProcessor +import org.thoughtcrime.securesms.backup.v2.proto.BackupDebugInfo import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo import org.thoughtcrime.securesms.backup.v2.stream.BackupExportWriter import org.thoughtcrime.securesms.backup.v2.stream.BackupImportReader @@ -104,6 +107,7 @@ import org.thoughtcrime.securesms.keyvalue.BackupValues.ArchiveServiceCredential import org.thoughtcrime.securesms.keyvalue.KeyValueStore import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.isDecisionPending +import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository import org.thoughtcrime.securesms.net.SignalNetwork import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.NotificationIds @@ -147,7 +151,10 @@ import java.io.OutputStream import java.time.ZonedDateTime import java.util.Currency import java.util.Locale +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicLong +import kotlin.jvm.optionals.getOrNull import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -798,7 +805,8 @@ object BackupRepository { version = VERSION, backupTimeMs = exportState.backupTime, mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey.value.toByteString(), - firstAppVersion = SignalStore.backup.firstAppVersion + firstAppVersion = SignalStore.backup.firstAppVersion, + debugInfo = buildDebugInfo() ) ) frameCount++ @@ -1211,6 +1219,7 @@ object BackupRepository { stopwatch.split("group-jobs") SignalStore.backup.firstAppVersion = header.firstAppVersion + SignalStore.internal.importedBackupDebugInfo = header.debugInfo.let { BackupDebugInfo.ADAPTER.decodeOrNull(it.toByteArray()) } Log.d(TAG, "[import] Finished! ${eventTimer.stop().summary}") stopwatch.stop(TAG) @@ -1846,6 +1855,38 @@ object BackupRepository { return RemoteRestoreResult.Success } + private fun buildDebugInfo(): ByteString { + if (!RemoteConfig.internalUser) { + return ByteString.EMPTY + } + + var debuglogUrl: String? = null + + if (SignalStore.internal.includeDebuglogInBackup) { + Log.i(TAG, "User has debuglog inclusion enabled. Generating a debuglog.") + val latch = CountDownLatch(1) + SubmitDebugLogRepository().buildAndSubmitLog { url -> + debuglogUrl = url.getOrNull() + latch.countDown() + } + + try { + val success = latch.await(10, TimeUnit.SECONDS) + if (!success) { + Log.w(TAG, "Timed out waiting for debuglog!") + } + } catch (e: Exception) { + Log.w(TAG, "Hit an error while generating the debuglog!") + } + } + + return BackupDebugInfo( + debuglogUrl = debuglogUrl ?: "", + attachmentDetails = SignalDatabase.attachments.debugAttachmentStatsForBackupProto(), + usingPaidTier = SignalStore.backup.backupTier == MessageBackupTier.PAID + ).encodeByteString() + } + interface ExportProgressListener { fun onAccount() fun onRecipient() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt index f6601cd6f2..d79199e934 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt @@ -171,7 +171,7 @@ private fun BackupsSettingsContent( is BackupState.ActiveFree, is BackupState.ActivePaid, is BackupState.Canceled -> { ActiveBackupsRow( - backupState = backupsSettingsState.backupState, + backupState = backupsSettingsState.backupState as BackupState.WithTypeAndRenewalTime, onBackupsRowClick = onBackupsRowClick, lastBackupAt = backupsSettingsState.lastBackupAt ) @@ -524,7 +524,8 @@ private fun BackupsSettingsContentPreview() { ), renewalTime = 0.seconds, price = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD")) - ) + ), + lastBackupAt = 0.seconds ) ) } @@ -536,7 +537,8 @@ private fun BackupsSettingsContentNotAvailablePreview() { Previews.Preview { BackupsSettingsContent( backupsSettingsState = BackupsSettingsState( - backupState = BackupState.NotAvailable + backupState = BackupState.NotAvailable, + lastBackupAt = 0.seconds ) ) } @@ -550,7 +552,8 @@ private fun BackupsSettingsContentBackupTierInternalOverridePreview() { backupsSettingsState = BackupsSettingsState( backupState = BackupState.None, showBackupTierInternalOverride = true, - backupTierInternalOverride = null + backupTierInternalOverride = null, + lastBackupAt = 0.seconds ) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt index e93fc5e314..db91f8dd26 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt @@ -279,6 +279,10 @@ class RemoteBackupsSettingsFragment : ComposeFragment() { requireActivity().finish() requireActivity().startActivity(AppSettingsActivity.manageStorage(requireActivity())) } + + override fun onIncludeDebuglogClick(newState: Boolean) { + viewModel.setIncludeDebuglog(newState) + } } private fun displayBackupKey() { @@ -368,6 +372,7 @@ private interface ContentCallbacks { fun onDisplayProgressDialog() = Unit fun onDisplayDownloadingBackupDialog() = Unit fun onManageStorageClick() = Unit + fun onIncludeDebuglogClick(newState: Boolean) = Unit object Empty : ContentCallbacks } @@ -512,6 +517,7 @@ private fun RemoteBackupsSettingsContent( canBackUpUsingCellular = state.canBackUpUsingCellular, canRestoreUsingCellular = state.canRestoreUsingCellular, canBackUpNow = !state.isOutOfStorageSpace, + includeDebuglog = state.includeDebuglog, contentCallbacks = contentCallbacks ) } else { @@ -815,6 +821,7 @@ private fun LazyListScope.appendBackupDetailsItems( canBackUpUsingCellular: Boolean, canRestoreUsingCellular: Boolean, canBackUpNow: Boolean, + includeDebuglog: Boolean?, contentCallbacks: ContentCallbacks ) { item { @@ -843,6 +850,12 @@ private fun LazyListScope.appendBackupDetailsItems( } } + if (includeDebuglog != null) { + item { + IncludeDebuglogRow(includeDebuglog) { contentCallbacks.onIncludeDebuglogClick(it) } + } + } + if (backupProgress == null || backupProgress.state == ArchiveUploadProgressState.State.None || backupProgress.state == ArchiveUploadProgressState.State.UserCanceled) { item { LastBackupRow( @@ -1421,6 +1434,19 @@ private fun getBackupUploadPhaseProgressString(state: ArchiveUploadProgressState return stringResource(R.string.RemoteBackupsSettingsFragment__uploading_s_of_s_d, formattedUploadedBytes, formattedTotalBytes, percent) } +@Composable +private fun IncludeDebuglogRow( + enabled: Boolean, + onToggle: (Boolean) -> Unit +) { + Rows.ToggleRow( + checked = enabled, + text = "[INTERNAL ONLY] Include debuglog?", + label = "If enabled, we will capture a debuglog and include it in the backup file.", + onCheckChanged = onToggle + ) +} + @Composable private fun LastBackupRow( lastBackupTimestamp: Long, @@ -1748,6 +1774,36 @@ private fun RemoteBackupsSettingsContentPreview() { } } +@SignalPreview +@Composable +private fun RemoteBackupsSettingsInternalUserContentPreview() { + Previews.Preview { + RemoteBackupsSettingsContent( + state = RemoteBackupsSettingsState( + backupsEnabled = true, + lastBackupTimestamp = -1, + canBackUpUsingCellular = false, + canRestoreUsingCellular = false, + backupsFrequency = BackupFrequency.MANUAL, + dialog = RemoteBackupsSettingsState.Dialog.NONE, + snackbar = RemoteBackupsSettingsState.Snackbar.NONE, + backupMediaSize = 2300000, + backupState = BackupState.ActiveFree( + messageBackupsType = MessageBackupsType.Free(mediaRetentionDays = 30) + ), + hasRedemptionError = false, + isOutOfStorageSpace = false, + includeDebuglog = true + ), + statusBarColorNestedScrollConnection = null, + backupDeleteState = DeletionState.NONE, + backupRestoreState = BackupRestoreState.FromBackupStatusData(BackupStatusData.CouldNotCompleteBackup), + contentCallbacks = ContentCallbacks.Empty, + backupProgress = null + ) + } +} + @SignalPreview @Composable private fun RedemptionErrorAlertPreview() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt index 427d23c1d3..0384b9bdc0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt @@ -9,6 +9,9 @@ import org.thoughtcrime.securesms.backup.v2.BackupFrequency import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.components.settings.app.backups.BackupState +/** + * @param includeDebuglog The state for whether or not we should include a debuglog in the backup. If `null`, hide the setting. + */ data class RemoteBackupsSettingsState( val tier: MessageBackupTier? = null, val backupsEnabled: Boolean, @@ -23,7 +26,8 @@ data class RemoteBackupsSettingsState( val backupsFrequency: BackupFrequency = BackupFrequency.DAILY, val lastBackupTimestamp: Long = 0, val dialog: Dialog = Dialog.NONE, - val snackbar: Snackbar = Snackbar.NONE + val snackbar: Snackbar = Snackbar.NONE, + val includeDebuglog: Boolean? = null ) { enum class Dialog { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt index 9a107cd983..ec73c62e77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.jobs.BackupMessagesJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState import org.thoughtcrime.securesms.service.MessageBackupListener +import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.TextSecurePreferences import kotlin.time.Duration.Companion.seconds @@ -66,7 +67,8 @@ class RemoteBackupsSettingsViewModel : ViewModel() { lastBackupTimestamp = SignalStore.backup.lastBackupTime, backupsFrequency = SignalStore.backup.backupFrequency, canBackUpUsingCellular = SignalStore.backup.backupWithCellular, - canRestoreUsingCellular = SignalStore.backup.restoreWithCellular + canRestoreUsingCellular = SignalStore.backup.restoreWithCellular, + includeDebuglog = SignalStore.internal.includeDebuglogInBackup.takeIf { RemoteConfig.internalUser } ) ) @@ -214,6 +216,11 @@ class RemoteBackupsSettingsViewModel : ViewModel() { ArchiveUploadProgress.cancel() } + fun setIncludeDebuglog(includeDebuglog: Boolean) { + SignalStore.internal.includeDebuglogInBackup = includeDebuglog + _state.update { it.copy(includeDebuglog = includeDebuglog) } + } + private suspend fun refreshState(lastPurchase: InAppPaymentTable.InAppPayment?) { try { Log.i(TAG, "Performing a state refresh.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt index b2df07d225..98d3bfa56b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt @@ -70,16 +70,12 @@ import org.thoughtcrime.securesms.attachments.Cdn import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.attachments.WallpaperAttachment import org.thoughtcrime.securesms.audio.AudioHash +import org.thoughtcrime.securesms.backup.v2.proto.BackupDebugInfo import org.thoughtcrime.securesms.blurhash.BlurHash import org.thoughtcrime.securesms.crypto.AttachmentSecret import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream -import org.thoughtcrime.securesms.database.AttachmentTable.ArchiveTransferState.COPY_PENDING -import org.thoughtcrime.securesms.database.AttachmentTable.ArchiveTransferState.FINISHED -import org.thoughtcrime.securesms.database.AttachmentTable.ArchiveTransferState.NONE -import org.thoughtcrime.securesms.database.AttachmentTable.ArchiveTransferState.PERMANENT_FAILURE -import org.thoughtcrime.securesms.database.AttachmentTable.ArchiveTransferState.UPLOAD_IN_PROGRESS import org.thoughtcrime.securesms.database.AttachmentTable.Companion.DATA_FILE import org.thoughtcrime.securesms.database.AttachmentTable.Companion.DATA_HASH_END import org.thoughtcrime.securesms.database.AttachmentTable.Companion.PREUPLOAD_MESSAGE_ID @@ -926,7 +922,7 @@ class AttachmentTable( fun deleteAttachments(toDelete: List): List { val unhandled = mutableListOf() for (syncAttachmentId in toDelete) { - val messageId = SignalDatabase.messages.getMessageIdOrNull(syncAttachmentId.syncMessageId) + val messageId = messages.getMessageIdOrNull(syncAttachmentId.syncMessageId) if (messageId != null) { val attachments = readableDatabase .select(ID, ATTACHMENT_UUID, REMOTE_DIGEST, DATA_HASH_END) @@ -949,7 +945,7 @@ class AttachmentTable( val attachmentToDelete = (byUuid ?: byDigest ?: byPlaintext)?.id if (attachmentToDelete != null) { if (attachments.size == 1) { - SignalDatabase.messages.deleteMessage(messageId) + messages.deleteMessage(messageId) } else { deleteAttachment(attachmentToDelete) } @@ -2463,7 +2459,7 @@ class AttachmentTable( transferProgress = cursor.requireInt(TRANSFER_STATE), size = cursor.requireLong(DATA_SIZE), fileName = cursor.requireString(FILE_NAME), - cdn = cursor.requireObject(CDN_NUMBER, Cdn.Serializer), + cdn = cursor.requireObject(CDN_NUMBER, Cdn), location = cursor.requireString(REMOTE_LOCATION), key = cursor.requireString(REMOTE_KEY), digest = cursor.requireBlob(REMOTE_DIGEST), @@ -2621,6 +2617,27 @@ class AttachmentTable( ) } + fun debugAttachmentStatsForBackupProto(): BackupDebugInfo.AttachmentDetails { + val archiveStateCounts = ArchiveTransferState + .entries.associateWith { + readableDatabase + .count() + .from(TABLE_NAME) + .where("$ARCHIVE_TRANSFER_STATE = ${it.value} AND $DATA_HASH_END NOT NULL AND $REMOTE_KEY NOT NULL") + .run() + .readToSingleLong(-1L) + } + + return BackupDebugInfo.AttachmentDetails( + notStartedCount = archiveStateCounts[ArchiveTransferState.NONE]?.toInt() ?: 0, + uploadInProgressCount = archiveStateCounts[ArchiveTransferState.UPLOAD_IN_PROGRESS]?.toInt() ?: 0, + copyPendingCount = archiveStateCounts[ArchiveTransferState.COPY_PENDING]?.toInt() ?: 0, + finishedCount = archiveStateCounts[ArchiveTransferState.FINISHED]?.toInt() ?: 0, + permanentFailureCount = archiveStateCounts[ArchiveTransferState.PERMANENT_FAILURE]?.toInt() ?: 0, + temporaryFailureCount = archiveStateCounts[ArchiveTransferState.TEMPORARY_FAILURE]?.toInt() ?: 0 + ) + } + class DataFileWriteResult( val file: File, val length: Long, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt index 58aa8e142b..e2f66fda07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt @@ -12,6 +12,7 @@ import androidx.core.app.NotificationManagerCompat import org.greenrobot.eventbus.EventBus import org.signal.core.util.Base64.decodeBase64OrThrow import org.signal.core.util.PendingIntentFlags +import org.signal.core.util.isNotNullOrBlank import org.signal.core.util.logging.Log import org.signal.libsignal.protocol.InvalidMacException import org.signal.libsignal.protocol.InvalidMessageException @@ -302,22 +303,32 @@ class RestoreAttachmentJob private constructor( Log.w(TAG, e.message) markFailed(attachmentId) } catch (e: NonSuccessfulResponseCodeException) { - if (SignalStore.backup.backsUpMedia) { - if (e.code == 404 && !forceTransitTier && attachment.remoteLocation?.isNotBlank() == true) { - Log.i(TAG, "[$attachmentId] Failed to download attachment from archive! Should only happen for recent attachments in a narrow window. Retrying download from transit CDN.") - if (RemoteConfig.internalUser) { - postFailedToDownloadFromArchiveNotification() - } + when (e.code) { + 404 -> { + if (forceTransitTier) { + Log.w(TAG, "[$attachmentId] Completely failed to restore an attachment! Failed downloading from both the archive and transit CDN.") + maybePostFailedToDownloadFromArchiveAndTransitNotification() + } else if (SignalStore.backup.backsUpMedia && attachment.remoteLocation.isNotNullOrBlank()) { + Log.w(TAG, "[$attachmentId] Failed to download attachment from the archive CDN! Retrying download from transit CDN.") + maybePostFailedToDownloadFromArchiveNotification() - retrieveAttachment(messageId, attachmentId, attachment, true) - return - } else if (e.code == 401 && useArchiveCdn) { - SignalStore.backup.mediaCredentials.cdnReadCredentials = null - SignalStore.backup.cachedMediaCdnPath = null - throw RetryLaterException(e) - } else if (e.code == 404 && attachment.remoteLocation?.isNotBlank() == true) { - Log.i(TAG, "Failed to download attachment from transit tier. Scheduling retry.") - throw e + return retrieveAttachment(messageId, attachmentId, attachment, forceTransitTier = true) + } else if (SignalStore.backup.backsUpMedia) { + Log.w(TAG, "[$attachmentId] Completely failed to restore an attachment! Failed to download from archive CDN, and there's not transit CDN info.") + maybePostFailedToDownloadFromArchiveAndTransitNotification() + } else if (attachment.remoteLocation.isNotNullOrBlank()) { + Log.w(TAG, "[$attachmentId] Failed to restore an attachment for a free tier user. Likely just older than 45 days.") + } + } + 401 -> { + if (useArchiveCdn) { + Log.w(TAG, "[$attachmentId] Had a credential issue when downloading an attachment. Clearing credentials and retrying.") + SignalStore.backup.mediaCredentials.cdnReadCredentials = null + SignalStore.backup.cachedMediaCdnPath = null + throw RetryLaterException(e) + } else { + Log.w(TAG, "[$attachmentId] Unexpected 401 response for transit CDN restore.") + } } } @@ -348,10 +359,29 @@ class RestoreAttachmentJob private constructor( SignalDatabase.attachments.setRestoreTransferState(attachmentId, AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE) } - private fun postFailedToDownloadFromArchiveNotification() { + private fun maybePostFailedToDownloadFromArchiveNotification() { + if (!RemoteConfig.internalUser || !SignalStore.backup.backsUpMedia) { + return + } + val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES) .setSmallIcon(R.drawable.ic_notification) - .setContentTitle("[Internal-only] Failed to download attachment from archive!") + .setContentTitle("[Internal-only] Failed to restore attachment from Archive CDN!") + .setContentText("Tap to send a debug log") + .setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, SubmitDebugLogActivity::class.java), PendingIntentFlags.mutable())) + .build() + + NotificationManagerCompat.from(context).notify(NotificationIds.INTERNAL_ERROR, notification) + } + + private fun maybePostFailedToDownloadFromArchiveAndTransitNotification() { + if (!RemoteConfig.internalUser || !SignalStore.backup.backsUpMedia) { + return + } + + val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle("[Internal-only] Completely failed to restore attachment!") .setContentText("Tap to send a debug log") .setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, SubmitDebugLogActivity::class.java), PendingIntentFlags.mutable())) .build() diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt index f8f84395f2..dd68429933 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.keyvalue import org.signal.ringrtc.CallManager.DataMode import org.thoughtcrime.securesms.BuildConfig +import org.thoughtcrime.securesms.backup.v2.proto.BackupDebugInfo import org.thoughtcrime.securesms.util.Environment.Calling.defaultSfuUrl import org.thoughtcrime.securesms.util.RemoteConfig @@ -34,6 +35,8 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal const val LARGE_SCREEN_UI: String = "internal.large.screen.ui" const val FORCE_SPLIT_PANE_ON_COMPACT_LANDSCAPE: String = "internal.force.split.pane.on.compact.landscape.ui" const val SHOW_ARCHIVE_STATE_HINT: String = "internal.show_archive_state_hint" + const val INCLUDE_DEBUGLOG_IN_BACKUP: String = "internal.include_debuglog_in_backup" + const val IMPORTED_BACKUP_DEBUG_INFO: String = "internal.imported_backup_debug_info" } public override fun onFirstEverAppLaunch() = Unit @@ -181,6 +184,12 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal var showArchiveStateHint by booleanValue(SHOW_ARCHIVE_STATE_HINT, false).defaultForExternalUsers() + /** Whether or not we should include a debuglog in the backup debug info when generating a backup. */ + var includeDebuglogInBackup by booleanValue(INCLUDE_DEBUGLOG_IN_BACKUP, true).falseForExternalUsers() + + /** Any [BackupDebugInfo] that was imported during the last backup restore, if any. */ + var importedBackupDebugInfo: BackupDebugInfo? by protoValue(IMPORTED_BACKUP_DEBUG_INFO, BackupDebugInfo.ADAPTER).defaultForExternalUsers() + private fun SignalStoreValueDelegate.defaultForExternalUsers(): SignalStoreValueDelegate { return this.withPrecondition { RemoteConfig.internalUser } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionRemoteBackups.kt b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionRemoteBackups.kt index 6720c5e51f..4fa6b548fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionRemoteBackups.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionRemoteBackups.kt @@ -55,7 +55,23 @@ class LogSectionRemoteBackups : LogSection { output.append("IAP error type (or null): ${inAppPayment.data.error?.type}\n") output.append("IAP cancellation reason (or null): ${inAppPayment.data.cancellation?.reason}\n") } else { - output.append("No in-app payment data available.") + output.append("No in-app payment data available.\n") + } + + output.append("\n -- Imported DebugInfo\n") + if (SignalStore.internal.importedBackupDebugInfo != null) { + val info = SignalStore.internal.importedBackupDebugInfo!! + output.append("Debuglog : ${info.debuglogUrl}\n") + output.append("Using Paid Tier : ${info.usingPaidTier}\n") + output.append("Attachment Details:\n") + output.append(" NONE : ${info.attachmentDetails?.notStartedCount ?: "N/A"}\n") + output.append(" UPLOAD_IN_PROGRESS: ${info.attachmentDetails?.uploadInProgressCount ?: "N/A"}\n") + output.append(" COPY_PENDING : ${info.attachmentDetails?.copyPendingCount ?: "N/A"}\n") + output.append(" FINISHED : ${info.attachmentDetails?.finishedCount ?: "N/A"}\n") + output.append(" PERMANENT_FAILURE : ${info.attachmentDetails?.permanentFailureCount ?: "N/A"}\n") + output.append(" TEMPORARY_FAILURE : ${info.attachmentDetails?.temporaryFailureCount ?: "N/A"}\n") + } else { + output.append("None\n") } return output diff --git a/app/src/main/protowire/Backup.proto b/app/src/main/protowire/Backup.proto index 6263b4ee7b..fe3a3df3ae 100644 --- a/app/src/main/protowire/Backup.proto +++ b/app/src/main/protowire/Backup.proto @@ -11,6 +11,7 @@ message BackupInfo { bytes mediaRootBackupKey = 3; // 32-byte random value generated when the backup is uploaded for the first time. string currentAppVersion = 4; string firstAppVersion = 5; + bytes debugInfo = 6; // Client-specific data field for debug info during testing } // Frames must follow in the following ordering rules: diff --git a/app/src/main/protowire/BackupDebugInfo.proto b/app/src/main/protowire/BackupDebugInfo.proto new file mode 100644 index 0000000000..ab7a48b895 --- /dev/null +++ b/app/src/main/protowire/BackupDebugInfo.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +option java_package = "org.thoughtcrime.securesms.backup.v2.proto"; + +message BackupDebugInfo { + message AttachmentDetails { + uint32 notStartedCount = 1; + uint32 uploadInProgressCount = 2; + uint32 copyPendingCount = 3; + uint32 finishedCount = 4; + uint32 permanentFailureCount = 5; + uint32 temporaryFailureCount = 6; + } + + string debuglogUrl = 1; + AttachmentDetails attachmentDetails = 2; + bool usingPaidTier = 3; +} \ No newline at end of file diff --git a/core-util-jvm/src/main/java/org/signal/core/util/logging/Scrubber.kt b/core-util-jvm/src/main/java/org/signal/core/util/logging/Scrubber.kt index 3f7bcf64c2..0cb348c8b6 100644 --- a/core-util-jvm/src/main/java/org/signal/core/util/logging/Scrubber.kt +++ b/core-util-jvm/src/main/java/org/signal/core/util/logging/Scrubber.kt @@ -75,7 +75,7 @@ object Scrubber { private val CALL_LINK_PATTERN = Pattern.compile("([bBcCdDfFgGhHkKmMnNpPqQrRsStTxXzZ]{4})(-[bBcCdDfFgGhHkKmMnNpPqQrRsStTxXzZ]{4}){7}") private const val CALL_LINK_CENSOR_SUFFIX = "-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX" - private val CALL_LINK_ROOM_ID_PATTERN = Pattern.compile("[0-9a-f]{61}([0-9a-f]{3})") + private val CALL_LINK_ROOM_ID_PATTERN = Pattern.compile("([^/])([0-9a-f]{61})([0-9a-f]{3})") @JvmStatic @Volatile @@ -180,7 +180,7 @@ object Scrubber { private fun CharSequence.scrubDomains(): CharSequence { return scrub(this, DOMAIN_PATTERN) { matcher, output -> val match: String = matcher.group(0)!! - if (matcher.groupCount() == 2 && TOP_100_TLDS.contains(matcher.group(2)!!.lowercase()) && !match.endsWith("signal.org")) { + if (matcher.groupCount() == 2 && TOP_100_TLDS.contains(matcher.group(2)!!.lowercase()) && !match.endsWith("signal.org") && !match.endsWith("debuglogs.org")) { output .append(DOMAIN_CENSOR) .append(matcher.group(2)) @@ -209,10 +209,10 @@ object Scrubber { private fun CharSequence.scrubCallLinkRoomIds(): CharSequence { return scrub(this, CALL_LINK_ROOM_ID_PATTERN) { matcher, output -> - val match = matcher.group(1) output - .append("[REDACTED]") - .append(match) + .append(matcher.group(1)) + .append("*************************************************************") + .append(matcher.group(3)) } } diff --git a/core-util-jvm/src/test/java/org/signal/core/util/logging/ScrubberTest.kt b/core-util-jvm/src/test/java/org/signal/core/util/logging/ScrubberTest.kt index e98e7a5340..71aeffcedd 100644 --- a/core-util-jvm/src/test/java/org/signal/core/util/logging/ScrubberTest.kt +++ b/core-util-jvm/src/test/java/org/signal/core/util/logging/ScrubberTest.kt @@ -206,6 +206,14 @@ class ScrubberTest(private val input: String, private val expected: String) { "Not a Call Link Root Key (Missing Quartet) BCAF-FGHK-MNPQ-RSTX-ZRQH-BCDF-STXZ", "Not a Call Link Root Key (Missing Quartet) BCAF-FGHK-MNPQ-RSTX-ZRQH-BCDF-STXZ" ), + arrayOf( + "A Call Link Room ID 905db82618b907f9ceaf8f12cb65f061ffc187f7df747cb3f38d5281f7c686be", + "A Call Link Room ID *************************************************************6be" + ), + arrayOf( + "Not a Call Link Room ID 905db82618b907f9ceaf8f12cb65f061ffc187f7df747cb3f38d5281f7c686b", + "Not a Call Link Room ID 905db82618b907f9ceaf8f12cb65f061ffc187f7df747cb3f38d5281f7c686b" + ), arrayOf( "2345:0425:2CA1:0000:0000:0567:5673:23b5", "...ipv6..." @@ -241,6 +249,10 @@ class ScrubberTest(private val input: String, private val expected: String) { arrayOf( "Recipient::123", "Recipient::123" + ), + arrayOf( + "https://debuglogs.org/android/7.47.2/2b5ccf4e3e58e44f12b3c92cfd5b526a2432f1dd0f81c8f89dededb176f1122d", + "https://debuglogs.org/android/7.47.2/2b5ccf4e3e58e44f12b3c92cfd5b526a2432f1dd0f81c8f89dededb176f1122d" ) ) }