Display progress for RestoreAttachmentJobs as a Banner.
This commit is contained in:
parent
4af6e0480a
commit
7807d92825
10 changed files with 196 additions and 7 deletions
|
@ -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]
|
||||
|
|
20
app/src/main/res/drawable/restore_reminder_icon.xml
Normal file
20
app/src/main/res/drawable/restore_reminder_icon.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2024 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/signal_colorPrimaryContainer" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:tint="@color/signal_colorPrimary">
|
||||
<vector android:drawable="@drawable/symbol_backup_display_bold_24" />
|
||||
</item>
|
||||
</layer-list>
|
12
app/src/main/res/drawable/symbol_backup_display_bold_24.xml
Normal file
12
app/src/main/res/drawable/symbol_backup_display_bold_24.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M4.25 20C4.25 11.3 11.3 4.25 20 4.25S35.75 11.3 35.75 20 28.7 35.75 20 35.75c-1.11 0-2.2-0.12-3.24-0.33-0.68-0.15-1.34 0.29-1.48 0.96-0.14 0.68 0.29 1.34 0.96 1.48 1.22 0.26 2.47 0.39 3.76 0.39 10.08 0 18.25-8.17 18.25-18.25S30.08 1.75 20 1.75 1.75 9.92 1.75 20c0 3.1 0.78 6.03 2.15 8.6 0.85 1.59 2 2.95 3.33 4.15H5.25C4.56 32.75 4 33.31 4 34s0.56 1.25 1.25 1.25h5c0.33 0 0.65-0.13 0.88-0.37 0.24-0.23 0.37-0.55 0.37-0.88v-5c0-0.69-0.56-1.25-1.25-1.25S9 28.31 9 29v2.53c-1.13-1.2-2.1-2.6-2.9-4.11C4.92 25.2 4.25 22.68 4.25 20Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M18.37 8.1C18.4 7.48 18.9 7 19.5 7c0.61 0 1.11 0.48 1.13 1.1l0.33 10.96 7.46 0.31c0.6 0.03 1.08 0.52 1.08 1.13 0 0.6-0.48 1.1-1.08 1.13L19.6 22h-0.1c-0.83 0-1.5-0.67-1.5-1.5v-0.07l0.37-12.34Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20 4.25C11.3 4.25 4.25 11.3 4.25 20c0 2.68 0.67 5.2 1.85 7.42 0.8 1.5 1.77 2.91 2.9 4.11V29c0-0.69 0.56-1.25 1.25-1.25S11.5 28.31 11.5 29v5c0 0.33-0.13 0.65-0.37 0.88-0.23 0.24-0.55 0.37-0.88 0.37h-5C4.56 35.25 4 34.69 4 34s0.56-1.25 1.25-1.25h1.98c-1.32-1.2-2.48-2.56-3.33-4.15-1.37-2.57-2.15-5.5-2.15-8.6C1.75 9.92 9.92 1.75 20 1.75S38.25 9.92 38.25 20 30.08 38.25 20 38.25c-1.29 0-2.54-0.13-3.76-0.39-0.67-0.14-1.1-0.8-0.96-1.48 0.14-0.67 0.8-1.1 1.48-0.96 1.04 0.21 2.13 0.33 3.24 0.33 8.7 0 15.75-7.05 15.75-15.75S28.7 4.25 20 4.25Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20 10c-0.98 0-1.76 0.81-1.72 1.8l0.46 10.74c0.03 0.68 0.58 1.21 1.26 1.21s1.23-0.53 1.26-1.2l0.46-10.76C21.76 10.81 20.98 10 20 10Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20 25.5c-1.1 0-2 0.9-2 2s0.9 2 2 2 2-0.9 2-2-0.9-2-2-2Z"/>
|
||||
</vector>
|
|
@ -816,6 +816,41 @@
|
|||
<!-- Action button to dismiss sheet and back up now -->
|
||||
<string name="CreateBackupBottomSheet__back_up_now">Back up now</string>
|
||||
|
||||
<!-- Headline text for a bottom sheet dialog shown when the restoration of the media backup fails. -->
|
||||
<string name="RestoreMediaFailedBottomSheet__Cant_restore_media">Can\'t restore media</string>
|
||||
<!-- Help text for a bottom sheet dialog shown when the restoration of the media backup fails, displayed while the app is calculating the amount of disk space required. -->
|
||||
<string name="RestoreMediaFailedBottomSheet__Your_device_does_not_have_enough_free_space_placeholder">Your device does not have enough free space. Free up storage space to restore your media.\n\nIf you choose “Skip restore” the media in your backup will be deleted the next time your device completes a new backup.</string>
|
||||
<!-- Help text for a bottom sheet dialog shown when the restoration of the media backup fails, displayed once the app knows how much disk space is required. The placeholder is a filesize, such as "1.23 GB." -->
|
||||
<string name="RestoreMediaFailedBottomSheet__Your_device_does_not_have_enough_free_space">Your device does not have enough free space. Free up %1$s of space to restore your media.\n\nIf you choose “Skip restore” the media in your backup will be deleted the next time your device completes a new backup.</string>
|
||||
<!-- Confirmation button to dismiss bottom sheet dialog without aborting the restoration process. -->
|
||||
<string name="RestoreMediaFailedBottomSheet__Okay">OK</string>
|
||||
<!-- Negative button on a bottom sheet dialog shown when the restoration of the media backup fails. This aborts the restoration process. -->
|
||||
<string name="RestoreMediaFailedBottomSheet__Skip_restore">Skip restore</string>
|
||||
<!-- Accessibility content description for the "Backup Error" icon -->
|
||||
<string name="RestoreMediaFailedBottomSheet__Backup_error_icon_content_description">Backup error icon</string>
|
||||
|
||||
<!-- Banner text that is displayed at the top of the app when trying to restore more media than the device's memory has space for. -->
|
||||
<string name="RestoreMediaErrorBanner__Free_up_storage_space">Free up storage space to restore your media.</string>
|
||||
<!-- Banner text that is displayed at the top of the app when trying to restore more media than the device's memory has space for. The placeholder is a filesize, such as "1.23 GB." -->
|
||||
<string name="RestoreMediaErrorBanner__Free_up_of_space">Free up %1$s of space to restore your media.</string>
|
||||
|
||||
<!-- Remote media restoration in-process status title. -->
|
||||
<string name="RestoreMediaReminder__Restoring_media">Restoring media</string>
|
||||
<!-- Remote media restoration in-process description. The first to placeholders are filesizes, such as "865 MB" and "2.3 GB". The final placeholder is a whole number representing the percentage progress. -->
|
||||
<string name="RestoreMediaReminder__Progress_filesize">%1$s of %2$s (%3$d%%)</string>
|
||||
<!-- Remote media restoration paused status title. -->
|
||||
<string name="RestoreMediaReminder__Restoring_media_paused">Restoring media paused</string>
|
||||
<!-- Remote media restoration paused status description for when the restore is paused because unmetered connectivity, such as WiFi, is unavailable. -->
|
||||
<string name="RestoreMediaReminder__Waiting_for_Wifi">Waiting for WiFi…</string>
|
||||
<!-- Remote media restoration paused status description for when the restore is paused because internet is unavailable. -->
|
||||
<string name="RestoreMediaReminder__Waiting_for_internet_connection">Waiting for Internet connection…</string>
|
||||
<!-- Remote media restoration paused status description for when the device's battery is low -->
|
||||
<string name="RestoreMediaReminder__low_battery">Low battery. Charge your device.</string>
|
||||
<!-- Remote media restoration call to action when the device is out of space. The placeholder string is a file size, such as "1.23 GB". -->
|
||||
<string name="RestoreMediaReminder__Free_up_space">Free up %1$s to restore your media.</string>
|
||||
<!-- Button label to abort media restoration -->
|
||||
<string name="RestoreMediaReminder__Skip_restore">Skip restore</string>
|
||||
|
||||
<!-- BackupsPreferenceFragment -->
|
||||
<string name="BackupsPreferenceFragment__chat_backups">Chat backups</string>
|
||||
<string name="BackupsPreferenceFragment__backups_are_encrypted_with_a_passphrase">Backups are encrypted with a passphrase and stored on your device.</string>
|
||||
|
|
Loading…
Add table
Reference in a new issue