mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-22 10:46:50 +00:00
Display progress for RestoreAttachmentJobs as a Banner.
This commit is contained in:
committed by
mtang-signal
parent
4af6e0480a
commit
7807d92825
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
||||
class MediaRestoreProgressBanner(private val data: MediaRestoreEvent) : Banner() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(MediaRestoreProgressBanner::class)
|
||||
|
||||
/**
|
||||
* Create a Lifecycle-aware [Flow] of [MediaRestoreProgressBanner] that observes the database for changes in attachments and emits banners when attachments are updated.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun createLifecycleAwareFlow(lifecycleOwner: LifecycleOwner): Flow<MediaRestoreProgressBanner> {
|
||||
if (SignalStore.backup.isRestoreInProgress) {
|
||||
val observer = LifecycleObserver()
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
return observer.flow
|
||||
} else {
|
||||
return emptyFlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var enabled: Boolean = data.totalBytes > 0L && data.totalBytes != data.completedBytes
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner() {
|
||||
BackupStatus(data = BackupStatusData.RestoringMedia(data.completedBytes, data.totalBytes))
|
||||
}
|
||||
|
||||
data class MediaRestoreEvent(val completedBytes: Long, val totalBytes: Long)
|
||||
|
||||
private class LifecycleObserver : DefaultLifecycleObserver {
|
||||
private var attachmentObserver: DatabaseObserver.Observer? = null
|
||||
private val _mutableSharedFlow = MutableSharedFlow<MediaRestoreEvent>(replay = 1)
|
||||
|
||||
val flow = _mutableSharedFlow.map { MediaRestoreProgressBanner(it) }
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
val queryObserver = DatabaseObserver.Observer {
|
||||
owner.lifecycleScope.launch {
|
||||
_mutableSharedFlow.emit(loadData())
|
||||
}
|
||||
}
|
||||
|
||||
attachmentObserver = queryObserver
|
||||
queryObserver.onChanged()
|
||||
AppDependencies.databaseObserver.registerAttachmentObserver(queryObserver)
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
attachmentObserver?.let {
|
||||
AppDependencies.databaseObserver.unregisterObserver(it)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadData() = withContext(Dispatchers.IO) {
|
||||
// TODO [backups]: define and query data for interrupted/paused restores
|
||||
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize
|
||||
val remainingAttachmentSize = SignalDatabase.attachments.getTotalRestorableAttachmentSize()
|
||||
val completedBytes = totalRestoreSize - remainingAttachmentSize
|
||||
MediaRestoreEvent(completedBytes, totalRestoreSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,7 @@ import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBot
|
||||
import org.thoughtcrime.securesms.banner.Banner;
|
||||
import org.thoughtcrime.securesms.banner.BannerManager;
|
||||
import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.MediaRestoreProgressBanner;
|
||||
import org.thoughtcrime.securesms.components.DeleteSyncEducationDialog;
|
||||
import org.thoughtcrime.securesms.components.Material3SearchToolbar;
|
||||
import org.thoughtcrime.securesms.components.RatingManager;
|
||||
@@ -882,8 +883,9 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
|
||||
private void initializeBanners() {
|
||||
if (RemoteConfig.newBannerUi()) {
|
||||
final List<Flow<? extends Banner>> bannerRepositories = List.of(OutdatedBuildBanner.createFlow(requireContext()));
|
||||
final BannerManager bannerManager = new BannerManager(bannerRepositories);
|
||||
final List<Flow<? extends Banner>> bannerRepositories = List.of(OutdatedBuildBanner.createFlow(requireContext()),
|
||||
MediaRestoreProgressBanner.createLifecycleAwareFlow(getViewLifecycleOwner()));
|
||||
final BannerManager bannerManager = new BannerManager(bannerRepositories);
|
||||
bannerManager.setContent(bannerView.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.signal.core.util.groupBy
|
||||
import org.signal.core.util.isNull
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.readToSingleLong
|
||||
import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireBlob
|
||||
import org.signal.core.util.requireBoolean
|
||||
@@ -454,6 +455,15 @@ class AttachmentTable(
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
fun getTotalRestorableAttachmentSize(): Long {
|
||||
return readableDatabase
|
||||
.select("SUM($DATA_SIZE)")
|
||||
.from(TABLE_NAME)
|
||||
.where("$TRANSFER_STATE = ?", TRANSFER_NEEDS_RESTORE.toString())
|
||||
.run()
|
||||
.readToSingleLong()
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the next eligible attachment that needs to be uploaded to the archive service.
|
||||
* If it exists, it'll also atomically be marked as [ArchiveTransferState.BACKFILL_UPLOAD_IN_PROGRESS].
|
||||
|
||||
@@ -48,6 +48,7 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo
|
||||
throw NotPushRegisteredException()
|
||||
}
|
||||
|
||||
SignalStore.backup.totalRestorableAttachmentSize = SignalDatabase.attachments.getTotalRestorableAttachmentSize()
|
||||
val jobManager = AppDependencies.jobManager
|
||||
val batchSize = 100
|
||||
val restoreTime = System.currentTimeMillis()
|
||||
|
||||
@@ -103,7 +103,7 @@ class RestoreAttachmentJob private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private val attachmentId: Long
|
||||
private val attachmentId: Long = attachmentId.id
|
||||
|
||||
constructor(messageId: Long, attachmentId: AttachmentId, manual: Boolean, forceArchiveDownload: Boolean = false, restoreMode: RestoreMode = RestoreMode.ORIGINAL) : this(
|
||||
Parameters.Builder()
|
||||
@@ -119,10 +119,6 @@ class RestoreAttachmentJob private constructor(
|
||||
restoreMode
|
||||
)
|
||||
|
||||
init {
|
||||
this.attachmentId = attachmentId.id
|
||||
}
|
||||
|
||||
override fun serialize(): ByteArray? {
|
||||
return JsonJobData.Builder()
|
||||
.putLong(KEY_MESSAGE_ID, messageId)
|
||||
@@ -156,6 +152,10 @@ class RestoreAttachmentJob private constructor(
|
||||
if (!SignalDatabase.messages.isStory(messageId)) {
|
||||
AppDependencies.messageNotifier.updateNotification(context, forConversation(0))
|
||||
}
|
||||
|
||||
if (SignalDatabase.attachments.getTotalRestorableAttachmentSize() == 0L) {
|
||||
SignalStore.backup.totalRestorableAttachmentSize = 0L
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, RetryLaterException::class)
|
||||
|
||||
@@ -27,6 +27,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
private const val KEY_NEXT_BACKUP_TIME = "backup.nextBackupTime"
|
||||
private const val KEY_LAST_BACKUP_TIME = "backup.lastBackupTime"
|
||||
private const val KEY_LAST_BACKUP_MEDIA_SYNC_TIME = "backup.lastBackupMediaSyncTime"
|
||||
private const val KEY_TOTAL_RESTORABLE_ATTACHMENT_SIZE = "backup.totalRestorableAttachmentSize"
|
||||
private const val KEY_BACKUP_FREQUENCY = "backup.backupFrequency"
|
||||
|
||||
private const val KEY_CDN_BACKUP_DIRECTORY = "backup.cdn.directory"
|
||||
@@ -61,6 +62,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
var nextBackupTime: Long by longValue(KEY_NEXT_BACKUP_TIME, -1)
|
||||
var lastBackupTime: Long by longValue(KEY_LAST_BACKUP_TIME, -1)
|
||||
var lastMediaSyncTime: Long by longValue(KEY_LAST_BACKUP_MEDIA_SYNC_TIME, -1)
|
||||
var totalRestorableAttachmentSize: Long by longValue(KEY_TOTAL_RESTORABLE_ATTACHMENT_SIZE, 0)
|
||||
var backupFrequency: BackupFrequency by enumValue(KEY_BACKUP_FREQUENCY, BackupFrequency.MANUAL, BackupFrequency.Serializer)
|
||||
var backupTier: MessageBackupTier? by enumValue(KEY_BACKUP_TIER, null, MessageBackupTier.Serializer)
|
||||
|
||||
@@ -86,6 +88,9 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
|
||||
var backupsInitialized: Boolean by booleanValue(KEY_BACKUPS_INITIALIZED, false)
|
||||
|
||||
val isRestoreInProgress: Boolean
|
||||
get() = totalRestorableAttachmentSize > 0
|
||||
|
||||
/**
|
||||
* Retrieves the stored credentials, mapped by the day they're valid. The day is represented as
|
||||
* the unix time (in seconds) of the start of the day. Wrapped in a [ArchiveServiceCredentials]
|
||||
|
||||
Reference in New Issue
Block a user