mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 09:49:30 +01:00
Add internal backup stats tab.
This commit is contained in:
@@ -35,6 +35,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -52,6 +53,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
@@ -133,6 +135,11 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||
override fun FragmentContent() {
|
||||
val context = LocalContext.current
|
||||
val state by viewModel.state
|
||||
val statsState by viewModel.statsState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadStats()
|
||||
}
|
||||
|
||||
Tabs(
|
||||
onBack = { findNavController().popBackStack() },
|
||||
@@ -225,6 +232,14 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||
.show()
|
||||
}
|
||||
)
|
||||
},
|
||||
statsContent = {
|
||||
InternalBackupStatsTab(
|
||||
statsState,
|
||||
object : StatsCallbacks {
|
||||
override fun loadRemoteState() = viewModel.loadRemoteStats()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -234,9 +249,10 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||
@Composable
|
||||
fun Tabs(
|
||||
onBack: () -> Unit,
|
||||
mainContent: @Composable () -> Unit
|
||||
mainContent: @Composable () -> Unit,
|
||||
statsContent: @Composable () -> Unit
|
||||
) {
|
||||
val tabs = listOf("Main")
|
||||
val tabs = listOf("Main", "Stats")
|
||||
var tabIndex by remember { mutableIntStateOf(0) }
|
||||
|
||||
val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
|
||||
@@ -274,6 +290,7 @@ fun Tabs(
|
||||
Surface(modifier = Modifier.padding(it)) {
|
||||
when (tabIndex) {
|
||||
0 -> mainContent()
|
||||
1 -> statsContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
@@ -18,6 +19,9 @@ import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.bytes
|
||||
@@ -32,8 +36,8 @@ import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveValidator
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupMetadata
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.DebugBackupMetadata
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.local.ArchiveFileSystem
|
||||
import org.thoughtcrime.securesms.backup.v2.local.ArchiveResult
|
||||
@@ -42,6 +46,8 @@ import org.thoughtcrime.securesms.backup.v2.local.LocalArchiver.FailureCause
|
||||
import org.thoughtcrime.securesms.backup.v2.local.SnapshotFileSystem
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader.Companion.MAC_SIZE
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable.DebugAttachmentStats
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupRestoreJob
|
||||
@@ -84,6 +90,8 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
)
|
||||
val state: State<ScreenState> = _state
|
||||
|
||||
val statsState: MutableStateFlow<StatsState> = MutableStateFlow(StatsState())
|
||||
|
||||
enum class DialogState {
|
||||
None,
|
||||
ImportCredentials
|
||||
@@ -256,7 +264,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
disposables += Single
|
||||
.fromCallable {
|
||||
BackupRepository.restoreBackupTier(SignalStore.account.requireAci())
|
||||
BackupRepository.getRemoteBackupState()
|
||||
BackupRepository.debugGetRemoteBackupState()
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe { result ->
|
||||
@@ -348,6 +356,29 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadStats() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
launch {
|
||||
var stats = SignalDatabase.attachments.debugGetAttachmentStats()
|
||||
|
||||
statsState.update { it.copy(attachmentStats = stats) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadRemoteStats() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
launch {
|
||||
statsState.update { it.copy(loadingRemoteStats = true) }
|
||||
val (remoteState: DebugBackupMetadata?, errorMsg: String?) = when (val result = BackupRepository.debugGetRemoteBackupState()) {
|
||||
is NetworkResult.Success -> result.result to null
|
||||
else -> null to result.toString()
|
||||
}
|
||||
statsState.update { it.copy(remoteState = remoteState, remoteFailureMsg = errorMsg, loadingRemoteStats = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteRemoteBackupData(): Boolean = withContext(Dispatchers.IO) {
|
||||
when (val result = BackupRepository.debugDeleteAllArchivedMedia()) {
|
||||
is NetworkResult.Success -> Log.i(TAG, "Remote data deleted")
|
||||
@@ -386,7 +417,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
sealed class RemoteBackupState {
|
||||
data object Unknown : RemoteBackupState()
|
||||
data object NotFound : RemoteBackupState()
|
||||
data class Available(val response: BackupMetadata) : RemoteBackupState()
|
||||
data class Available(val response: DebugBackupMetadata) : RemoteBackupState()
|
||||
}
|
||||
|
||||
data class MediaState(
|
||||
@@ -451,4 +482,13 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
val messageBackupKey: MessageBackupKey,
|
||||
val aci: ACI
|
||||
)
|
||||
|
||||
data class StatsState(
|
||||
val attachmentStats: DebugAttachmentStats? = null,
|
||||
val loadingRemoteStats: Boolean = false,
|
||||
val remoteState: DebugBackupMetadata? = null,
|
||||
val remoteFailureMsg: String? = null
|
||||
) {
|
||||
val valid = attachmentStats != null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.backup
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Dividers
|
||||
import org.signal.core.ui.compose.Texts
|
||||
import org.signal.core.util.bytes
|
||||
|
||||
@Composable
|
||||
fun InternalBackupStatsTab(stats: InternalBackupPlaygroundViewModel.StatsState, callbacks: StatsCallbacks) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = Modifier.verticalScroll(scrollState)) {
|
||||
Texts.SectionHeader(text = "Local Attachment State")
|
||||
|
||||
if (stats.attachmentStats != null) {
|
||||
Text(text = "Attachment Count: ${stats.attachmentStats.attachmentCount}")
|
||||
|
||||
Text(text = "Transit Download State:")
|
||||
stats.attachmentStats.transferStateCounts.forEach { (state, count) ->
|
||||
if (count > 0) {
|
||||
Text(text = "$state: $count")
|
||||
}
|
||||
}
|
||||
|
||||
Text(text = "Valid for archive Transit Download State:")
|
||||
stats.attachmentStats.validForArchiveTransferStateCounts.forEach { (state, count) ->
|
||||
if (count > 0) {
|
||||
Text(text = "$state: $count")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
|
||||
Text(text = "Archive State:")
|
||||
stats.attachmentStats.archiveStateCounts.forEach { (state, count) ->
|
||||
if (count > 0) {
|
||||
Text(text = "$state: $count")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Text(text = "Unique/archived data files: ${stats.attachmentStats.attachmentFileCount}/${stats.attachmentStats.finishedAttachmentFileCount}")
|
||||
Text(text = "Unique/archived verified digest count: ${stats.attachmentStats.attachmentDigestCount}/${stats.attachmentStats.finishedAttachmentDigestCount}")
|
||||
Text(text = "Unique/expected thumbnail files: ${stats.attachmentStats.thumbnailFileCount}/${stats.attachmentStats.estimatedThumbnailCount}")
|
||||
Text(text = "Local Total: ${stats.attachmentStats.attachmentFileCount + stats.attachmentStats.thumbnailFileCount}")
|
||||
Text(text = "Expected remote total: ${stats.attachmentStats.estimatedThumbnailCount + stats.attachmentStats.finishedAttachmentDigestCount}")
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Text(text = "Pending upload: ${stats.attachmentStats.pendingUploadBytes} (~${stats.attachmentStats.pendingUploadBytes.bytes.toUnitString()})")
|
||||
Text(text = "Est uploaded attachments: ${stats.attachmentStats.uploadedAttachmentBytes} (~${stats.attachmentStats.uploadedAttachmentBytes.bytes.toUnitString()})")
|
||||
Text(text = "Est uploaded thumbnails: ${stats.attachmentStats.thumbnailBytes} (~${stats.attachmentStats.thumbnailBytes.bytes.toUnitString()})")
|
||||
val total = stats.attachmentStats.thumbnailBytes + stats.attachmentStats.uploadedAttachmentBytes
|
||||
Text(text = "Est total: $total (~${total.bytes.toUnitString()})")
|
||||
} else {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Texts.SectionHeader(text = "Remote State")
|
||||
|
||||
if (!stats.loadingRemoteStats && stats.remoteState == null && stats.remoteFailureMsg == null) {
|
||||
Button(onClick = callbacks::loadRemoteState) {
|
||||
Text(text = "Load remote stats (expensive and long)")
|
||||
}
|
||||
} else {
|
||||
if (stats.loadingRemoteStats) {
|
||||
CircularProgressIndicator()
|
||||
} else if (stats.remoteState != null) {
|
||||
Text(text = "Media item count: ${stats.remoteState.mediaCount}")
|
||||
Text(text = "Media items sum size: ${stats.remoteState.mediaSize} (~${stats.remoteState.mediaSize.bytes.toUnitString()})")
|
||||
Text(text = "Server estimated used size: ${stats.remoteState.usedSpace} (~${stats.remoteState.usedSpace.bytes.toUnitString()})")
|
||||
} else if (stats.remoteFailureMsg != null) {
|
||||
Text(text = stats.remoteFailureMsg)
|
||||
}
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Texts.SectionHeader(text = "Expected vs Actual")
|
||||
|
||||
if (stats.attachmentStats != null && stats.remoteState != null) {
|
||||
val finished = stats.attachmentStats.finishedAttachmentFileCount
|
||||
val thumbnails = stats.attachmentStats.thumbnailFileCount
|
||||
Text(text = "Expected Count/Actual Remote Count: ${finished + thumbnails} / ${stats.remoteState.mediaCount}")
|
||||
} else {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface StatsCallbacks {
|
||||
fun loadRemoteState()
|
||||
|
||||
companion object {
|
||||
val EMPTY = object : StatsCallbacks {
|
||||
override fun loadRemoteState() = Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user