Keep device alive during backup restore flows and show progress for link and sync import.

This commit is contained in:
Cody Henthorne
2026-07-16 13:38:35 -04:00
parent b8508af636
commit 97a8795100
79 changed files with 384 additions and 83 deletions
+1 -1
View File
@@ -1536,7 +1536,7 @@
</receiver>
<receiver
android:name="org.thoughtcrime.securesms.jobs.ForegroundServiceUtil$Receiver"
android:name="org.signal.core.util.ForegroundServiceUtil$Receiver"
android:exported="false" />
<receiver
@@ -53,7 +53,7 @@ public enum BackupFileIOError {
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, AppSettingsActivity.backups(context), PendingIntentFlags.mutable());
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
.setSmallIcon(R.drawable.ic_signal_backup)
.setSmallIcon(org.signal.core.ui.R.drawable.ic_signal_backup)
.setContentTitle(context.getString(titleId))
.setContentText(context.getString(messageId))
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(messageId)))
@@ -185,6 +185,7 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import org.signal.registration.R as RegistrationR
object BackupRepository {
@@ -2358,7 +2359,7 @@ object BackupRepository {
try {
DataRestoreConstraint.isRestoringData = true
return withContext(Dispatchers.IO) {
return@withContext BackupProgressService.start(context, context.getString(R.string.BackupProgressService_title)).use {
return@withContext BackupProgressService.start(context, context.getString(RegistrationR.string.MessageSyncScreen__syncing_messages)).use {
restoreLinkAndSyncBackup(response, ephemeralBackupKey, controller = it, cancellationSignal = { !isActive })
}
}
@@ -2373,7 +2374,7 @@ object BackupRepository {
val progressListener = object : ProgressListener {
override fun onAttachmentProgress(progress: AttachmentTransferProgress) {
controller.update(
title = AppDependencies.application.getString(R.string.BackupProgressService_title_downloading),
title = AppDependencies.application.getString(RegistrationR.string.MessageSyncScreen__syncing_messages),
progress = progress.value,
indeterminate = false
)
@@ -2405,7 +2406,7 @@ object BackupRepository {
}
controller.update(
title = AppDependencies.application.getString(R.string.BackupProgressService_title),
title = AppDependencies.application.getString(RegistrationR.string.MessageSyncScreen__syncing_messages),
progress = 0f,
indeterminate = true
)
@@ -85,7 +85,7 @@ public final class NewDeviceTransferSetupFragment extends DeviceTransferSetupFra
protected void startTransfer() {
PendingIntent pendingIntent = PendingIntent.getActivity(requireContext(), 0, MainActivity.clearTop(requireContext()), PendingIntentFlags.mutable());
TransferNotificationData notificationData = new TransferNotificationData(NotificationIds.DEVICE_TRANSFER, NotificationChannels.getInstance().BACKUPS, R.drawable.ic_signal_backup);
TransferNotificationData notificationData = new TransferNotificationData(NotificationIds.DEVICE_TRANSFER, NotificationChannels.getInstance().BACKUPS, org.signal.core.ui.R.drawable.ic_signal_backup);
DeviceToDeviceTransferService.startServer(requireContext(), new NewDeviceServerTask(), notificationData, pendingIntent);
}
}
@@ -58,7 +58,7 @@ public final class OldDeviceTransferSetupFragment extends DeviceTransferSetupFra
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(requireContext(), 0, intent, PendingIntentFlags.mutable());
DeviceToDeviceTransferService.TransferNotificationData notificationData = new DeviceToDeviceTransferService.TransferNotificationData(NotificationIds.DEVICE_TRANSFER, NotificationChannels.getInstance().BACKUPS, R.drawable.ic_signal_backup);
DeviceToDeviceTransferService.TransferNotificationData notificationData = new DeviceToDeviceTransferService.TransferNotificationData(NotificationIds.DEVICE_TRANSFER, NotificationChannels.getInstance().BACKUPS, org.signal.core.ui.R.drawable.ic_signal_backup);
DeviceToDeviceTransferService.startClient(requireContext(), new OldDeviceClientTask(), notificationData, pendingIntent);
}
@@ -7,11 +7,11 @@ import android.content.Intent
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import org.signal.core.util.ForegroundServiceUtil
import org.signal.core.util.PendingIntentFlags
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.util.WakeLockUtil
@@ -7,6 +7,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.signal.core.util.Stopwatch
import org.signal.core.util.UnableToStartException
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.BackupFileIOError
@@ -24,6 +25,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.service.GenericForegroundService
import org.thoughtcrime.securesms.service.NotificationController
import java.io.IOException
import org.signal.core.ui.R as CoreUiR
/**
* Local backup job for installs using new backupv2 folder format.
@@ -57,7 +59,7 @@ class LocalArchiveJob internal constructor(parameters: Parameters) : Job(paramet
context,
context.getString(R.string.LocalBackupJob_creating_signal_backup),
NotificationChannels.getInstance().BACKUPS,
R.drawable.ic_signal_backup
CoreUiR.drawable.ic_signal_backup
)
} catch (e: UnableToStartException) {
Log.w(TAG, "Unable to start foreground backup service, continuing without service")
@@ -13,6 +13,7 @@ import org.signal.core.ui.permissions.Permissions;
import org.signal.core.ui.util.StorageUtil;
import org.signal.core.util.NoExternalStorageException;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.UnableToStartException;
import org.signal.core.util.crypto.AttachmentSecretProvider;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
@@ -117,7 +118,7 @@ public final class LocalBackupJob extends BaseJob {
try (NotificationController notification = GenericForegroundService.startForegroundTask(context,
context.getString(R.string.LocalBackupJob_creating_signal_backup),
NotificationChannels.getInstance().BACKUPS,
R.drawable.ic_signal_backup))
org.signal.core.ui.R.drawable.ic_signal_backup))
{
updater.setNotification(notification);
EventBus.getDefault().register(updater);
@@ -12,6 +12,7 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.UnableToStartException;
import org.signal.core.util.androidx.DocumentFileUtil;
import org.signal.core.util.androidx.DocumentFileUtil.OperationResult;
import org.signal.core.util.crypto.AttachmentSecretProvider;
@@ -89,7 +90,7 @@ public final class LocalBackupJobApi29 extends BaseJob {
notification = GenericForegroundService.startForegroundTask(context,
context.getString(R.string.LocalBackupJob_creating_signal_backup),
NotificationChannels.getInstance().BACKUPS,
R.drawable.ic_signal_backup);
org.signal.core.ui.R.drawable.ic_signal_backup);
} catch (UnableToStartException e) {
Log.w(TAG, "Unable to start foreground backup service, continuing without service");
}
@@ -11,6 +11,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.signal.core.util.PendingIntentFlags.immutable
import org.signal.core.util.Stopwatch
import org.signal.core.util.UnableToStartException
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.LocalExportProgress
@@ -28,6 +29,7 @@ import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import org.signal.core.ui.R as CoreUiR
class LocalPlaintextArchiveJob internal constructor(
private val destinationUri: String,
@@ -73,7 +75,7 @@ class LocalPlaintextArchiveJob internal constructor(
context,
context.getString(R.string.LocalBackupJob_creating_signal_backup),
NotificationChannels.getInstance().BACKUPS,
R.drawable.ic_signal_backup,
CoreUiR.drawable.ic_signal_backup,
contentIntent
)
} catch (e: UnableToStartException) {
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.messages.WebSocketDrainer;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.signal.core.util.AppForegroundObserver;
import org.signal.core.util.UnableToStartException;
import org.signal.network.exceptions.PushNetworkException;
import java.io.IOException;
@@ -13,6 +13,7 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.models.ServiceId
import org.signal.core.util.AppForegroundObserver
import org.signal.core.util.SafeForegroundService
import org.signal.core.util.SleepTimer
import org.signal.core.util.UptimeSleepTimer
import org.signal.core.util.concurrent.SignalExecutors
@@ -37,7 +38,6 @@ import org.thoughtcrime.securesms.messages.protocol.BufferedProtocolStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess.Companion.toApplicableSystemHttpProxy
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.SafeForegroundService
import org.thoughtcrime.securesms.util.AlarmSleepTimer
import org.thoughtcrime.securesms.util.Environment
import org.thoughtcrime.securesms.util.SignalLocalMetrics
@@ -777,7 +777,7 @@ class AppRegistrationNetworkController(
val notificationData = org.signal.devicetransfer.DeviceToDeviceTransferService.TransferNotificationData(
org.thoughtcrime.securesms.notifications.NotificationIds.DEVICE_TRANSFER,
org.thoughtcrime.securesms.notifications.NotificationChannels.getInstance().BACKUPS,
org.thoughtcrime.securesms.R.drawable.ic_signal_backup
org.signal.core.ui.R.drawable.ic_signal_backup
)
org.signal.devicetransfer.DeviceToDeviceTransferService.startServer(
context,
@@ -50,6 +50,7 @@ import org.signal.registration.proto.RegistrationData
import org.signal.registration.screens.localbackuprestore.LocalBackupInfo
import org.signal.registration.screens.messagesync.LinkAndSyncProgress
import org.signal.registration.screens.remotebackuprestore.RemoteBackupRestoreProgress
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.BackupEvent
import org.thoughtcrime.securesms.backup.BackupPassphrase
import org.thoughtcrime.securesms.backup.FullBackupImporter
@@ -88,6 +89,7 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.service.BackupProgressService
import org.thoughtcrime.securesms.service.DirectoryRefreshListener
import org.thoughtcrime.securesms.service.LocalBackupListener
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener
@@ -293,6 +295,7 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
EventBus.getDefault().register(subscriber)
launch(Dispatchers.IO) {
val progressService = BackupProgressService.start(context, context.getString(R.string.BackupProgressService_title))
try {
if (!FullBackupImporter.validatePassphrase(context, backupUri, passphrase)) {
Log.w(TAG, "V1 restore failed: incorrect passphrase")
@@ -344,6 +347,7 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
Log.w(TAG, "V1 restore failed", e)
trySend(LocalBackupRestoreProgress.Error(e))
} finally {
progressService.close()
channel.close()
}
}
@@ -382,6 +386,7 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
EventBus.getDefault().register(subscriber)
launch(Dispatchers.IO) {
val progressService = BackupProgressService.start(context, context.getString(R.string.BackupProgressService_title))
try {
val backupDir = DocumentFile.fromTreeUri(context, backupUri)
if (backupDir == null || !backupDir.canRead()) {
@@ -441,6 +446,7 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
Log.w(TAG, "V2 restore failed", e)
trySend(LocalBackupRestoreProgress.Error(e))
} finally {
progressService.close()
channel.close()
}
}
@@ -598,8 +604,8 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
fun onRestoreEvent(event: RestoreV2Event) {
val progress = when (event.type) {
RestoreV2Event.Type.PROGRESS_DOWNLOAD -> LinkAndSyncProgress.Downloading(event.count, event.estimatedTotalCount)
RestoreV2Event.Type.PROGRESS_RESTORE -> LinkAndSyncProgress.Restoring
RestoreV2Event.Type.PROGRESS_FINALIZING -> LinkAndSyncProgress.Restoring
RestoreV2Event.Type.PROGRESS_RESTORE -> LinkAndSyncProgress.Restoring(event.count, event.estimatedTotalCount)
RestoreV2Event.Type.PROGRESS_FINALIZING -> LinkAndSyncProgress.Finalizing
}
trySend(progress)
}
@@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.signal.core.util.PendingIntentFlags
import org.signal.core.util.SafeForegroundService
import org.signal.core.util.logging.Log
import org.signal.core.util.throttleLatest
import org.thoughtcrime.securesms.MainActivity
@@ -8,6 +8,7 @@ import android.os.Build
import androidx.core.app.NotificationCompat
import org.signal.core.util.ByteSize
import org.signal.core.util.PendingIntentFlags
import org.signal.core.util.SafeForegroundService
import org.signal.core.util.bytes
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.MainActivity
@@ -12,18 +12,21 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import org.signal.core.util.PendingIntentFlags
import org.signal.core.util.SafeForegroundService
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.NotificationIds
import java.util.concurrent.locks.ReentrantLock
import javax.annotation.CheckReturnValue
import kotlin.concurrent.withLock
import kotlin.time.Duration.Companion.hours
import org.signal.core.ui.R as CoreUiR
/**
* Foreground service to provide "long" run support to backup jobs.
@@ -44,6 +47,9 @@ class BackupProgressService : SafeForegroundService() {
private var progress: Float = 0f
private var indeterminate: Boolean = true
private const val WAKE_LOCK_TAG = "signal:backupProgress"
private val WAKE_LOCK_TIMEOUT = 2.hours.inWholeMilliseconds
@CheckReturnValue
fun start(context: Context, startingTitle: String): Controller {
controllerLock.withLock {
@@ -72,7 +78,7 @@ class BackupProgressService : SafeForegroundService() {
private fun getForegroundNotification(context: Context): Notification {
return NotificationCompat.Builder(context, NotificationChannels.getInstance().OTHER)
.setSmallIcon(R.drawable.ic_notification)
.setSmallIcon(CoreUiR.drawable.ic_signal_backup)
.setContentTitle(title)
.setProgress(100, (progress * 100).toInt(), indeterminate)
.setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.clearTop(context), PendingIntentFlags.mutable()))
@@ -81,6 +87,8 @@ class BackupProgressService : SafeForegroundService() {
}
}
private var wakeLock: PowerManager.WakeLock? = null
override val tag: String = TAG
override val notificationId: Int = NotificationIds.BACKUP_PROGRESS
@@ -88,11 +96,47 @@ class BackupProgressService : SafeForegroundService() {
return getForegroundNotification(this)
}
override fun onServiceStartCommandReceived(intent: Intent) {
acquireWakeLock()
}
override fun onServiceStopCommandReceived(intent: Intent) {
releaseWakeLock()
}
override fun onDestroy() {
super.onDestroy()
releaseWakeLock()
}
override fun onTimeout(startId: Int, fgsType: Int) {
Log.w(TAG, "BackupProgressService has timed out. startId: $startId, foregroundServiceType: $fgsType")
stop(context = this, fromTimeout = true)
}
private fun acquireWakeLock() {
try {
if (wakeLock?.isHeld != true) {
wakeLock = ContextCompat.getSystemService(this, PowerManager::class.java)
?.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG)
?.apply { acquire(WAKE_LOCK_TIMEOUT) }
}
} catch (e: Exception) {
Log.w(TAG, "Failed to acquire wake lock.", e)
}
}
private fun releaseWakeLock() {
try {
if (wakeLock?.isHeld == true) {
wakeLock?.release()
}
} catch (e: Exception) {
Log.w(TAG, "Failed to release wake lock.", e)
}
wakeLock = null
}
/**
* Use to update notification progress/state.
*/
@@ -12,13 +12,13 @@ import androidx.annotation.DrawableRes
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.IntentCompat
import org.signal.core.util.ForegroundServiceUtil
import org.signal.core.util.PendingIntentFlags.mutable
import org.signal.core.util.UnableToStartException
import org.signal.core.util.logging.Log
import org.signal.network.util.Preconditions
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil
import org.thoughtcrime.securesms.jobs.UnableToStartException
import org.thoughtcrime.securesms.notifications.NotificationChannels
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
@@ -5,8 +5,8 @@ import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import org.signal.core.util.UnableToStartException
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.jobs.UnableToStartException
import org.thoughtcrime.securesms.service.GenericForegroundService.Companion.stopForegroundTask
import org.thoughtcrime.securesms.service.GenericForegroundService.LocalBinder
import java.util.concurrent.atomic.AtomicReference
@@ -29,12 +29,12 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.ui.permissions.Permissions
import org.signal.core.util.PendingIntentFlags
import org.signal.core.util.SafeForegroundService
import org.signal.core.util.UnableToStartException
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.UnableToStartException
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.SafeForegroundService
import org.thoughtcrime.securesms.util.DeviceProperties
import org.thoughtcrime.securesms.util.TelephonyUtil
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder
@@ -0,0 +1,30 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.ui.compose
import android.app.Activity
import android.view.WindowManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalContext
/**
* Keeps the screen on while this composable is in the composition by toggling
* [WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON] on the host activity window.
*/
@Composable
fun KeepScreenOnEffect() {
val context = LocalContext.current
DisposableEffect(Unit) {
val window = (context as? Activity)?.window
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
onDispose {
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
}

Before

Width:  |  Height:  |  Size: 424 B

After

Width:  |  Height:  |  Size: 424 B

Before

Width:  |  Height:  |  Size: 296 B

After

Width:  |  Height:  |  Size: 296 B

Before

Width:  |  Height:  |  Size: 537 B

After

Width:  |  Height:  |  Size: 537 B

Before

Width:  |  Height:  |  Size: 760 B

After

Width:  |  Height:  |  Size: 760 B

Before

Width:  |  Height:  |  Size: 1015 B

After

Width:  |  Height:  |  Size: 1015 B

+1
View File
@@ -14,6 +14,7 @@ dependencies {
implementation(libs.androidx.sqlite)
implementation(libs.androidx.documentfile)
implementation(libs.google.jsr305)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.exifinterface)
implementation(libs.kotlinx.serialization.json)
@@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.jobs
package org.signal.core.util
import android.app.AlarmManager
import android.app.ForegroundServiceStartNotAllowedException
@@ -10,12 +10,7 @@ import android.os.Build
import android.os.SystemClock
import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat
import org.signal.core.util.AppForegroundObserver
import org.signal.core.util.PendingIntentFlags
import org.signal.core.util.ServiceUtil
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.service.GenericForegroundService
import org.thoughtcrime.securesms.service.NotificationController
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
@@ -118,17 +113,6 @@ object ForegroundServiceUtil {
}
}
/**
* Does its best to start a foreground service with your task name, including possibly blocking and waiting until we are able to.
* However, it is always possible that the attempt will fail, so always handle the [UnableToStartException].
*/
@Throws(UnableToStartException::class)
@JvmStatic
fun startGenericTaskWhenCapable(context: Context, task: String): NotificationController {
blockUntilCapable(context)
return GenericForegroundService.startForegroundTask(context, task)
}
/**
* Waits until we're capable of starting a foreground service.
* @return True if you should expect to be able to start a foreground service, otherwise false. Please note that this isn't *guaranteed*, just what we believe
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.service
package org.signal.core.util
import android.annotation.SuppressLint
import android.app.Notification
@@ -16,8 +16,6 @@ import android.os.IBinder
import androidx.annotation.RequiresApi
import androidx.core.app.ServiceCompat
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil
import org.thoughtcrime.securesms.jobs.UnableToStartException
import java.util.concurrent.locks.ReentrantLock
import javax.annotation.CheckReturnValue
import kotlin.concurrent.withLock
@@ -38,6 +36,7 @@ abstract class SafeForegroundService : Service() {
private const val ACTION_TIMEOUT = "timeout"
private var states: MutableMap<Class<out SafeForegroundService>, State> = mutableMapOf()
private val restartExtras: MutableMap<Class<out SafeForegroundService>, Bundle> = mutableMapOf()
private val stateLock = ReentrantLock()
/**
@@ -78,6 +77,7 @@ abstract class SafeForegroundService : Service() {
State.STOPPING -> {
Log.d(TAG, "[start] Attempted to start while the service is stopping. Enqueueing a restart.")
states[serviceClass] = State.NEEDS_RESTART
restartExtras[serviceClass] = Bundle(extras)
true
}
}
@@ -124,6 +124,7 @@ abstract class SafeForegroundService : Service() {
State.NEEDS_RESTART -> {
Log.i(TAG, "[stop] Clearing pending restart.")
states[serviceClass] = State.STOPPING
restartExtras.remove(serviceClass)
false
}
}
@@ -232,7 +233,8 @@ abstract class SafeForegroundService : Service() {
State.NEEDS_RESTART -> {
Log.i(TAG, "[onDestroy] Restarting service!")
states[javaClass] = State.STOPPED
if (!start(this, javaClass)) {
val extras = restartExtras.remove(javaClass) ?: Bundle.EMPTY
if (!start(this, javaClass, extras)) {
Log.w(TAG, "[onDestroy] Failed to restart service.")
}
}
@@ -406,7 +406,7 @@ class DemoStorageController(private val context: Context) : StorageController {
// Decrypt + parse the backup proto to prove the round-trip worked. We intentionally do NOT
// import the frames into a database -- that is the app's full backup-restore pipeline.
trySend(LinkAndSyncProgress.Restoring)
trySend(LinkAndSyncProgress.Finalizing)
val downloadedBytes = tempFile.length()
var frameCount = 0
EncryptedBackupReader.createForLocalOrLinking(
@@ -51,6 +51,7 @@ import kotlinx.coroutines.launch
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.KeepScreenOnEffect
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.util.mebiBytes
@@ -83,6 +84,10 @@ fun LocalBackupRestoreScreen(
}
}
if (state.restorePhase == LocalBackupRestoreState.RestorePhase.Preparing || state.restorePhase == LocalBackupRestoreState.RestorePhase.InProgress) {
KeepScreenOnEffect()
}
when (state.restorePhase) {
LocalBackupRestoreState.RestorePhase.SelectFolder -> {
SelectFolderContent(onEvent = onEvent, modifier = modifier)
@@ -18,8 +18,11 @@ sealed interface LinkAndSyncProgress {
/** Downloading the backup from the primary/CDN. */
data class Downloading(val bytesDownloaded: ByteSize, val totalBytes: ByteSize) : LinkAndSyncProgress
/** Importing/restoring messages from the downloaded backup. */
data object Restoring : LinkAndSyncProgress
/** Importing/restoring messages from the downloaded backup, with determinate progress. */
data class Restoring(val bytesRestored: ByteSize, val totalBytes: ByteSize) : LinkAndSyncProgress
/** Post-restore finalization work that does not report granular progress. */
data object Finalizing : LinkAndSyncProgress
/** The link-and-sync restore completed (or there was nothing to restore). */
data object Complete : LinkAndSyncProgress
@@ -44,6 +44,7 @@ import org.signal.core.ui.assumedFormFactor
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.KeepScreenOnEffect
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.rememberWindowBreakpoint
@@ -54,6 +55,7 @@ import org.signal.registration.screens.OnePaneRegistrationScaffold
import org.signal.registration.screens.RegistrationScaffold
import org.signal.registration.screens.TwoPaneRegistrationScaffold
import org.signal.registration.screens.attachDebugLogHelper
import org.signal.registration.screens.messagesync.MessageSyncScreenState.Stage
import org.signal.registration.test.TestTags
/**
@@ -67,6 +69,10 @@ fun MessageSyncScreen(
) {
val layoutParams = RegistrationScaffold.rememberLayoutParams()
if (!state.showSyncFailedDialog) {
KeepScreenOnEffect()
}
Surface(modifier = modifier.testTag(TestTags.MESSAGE_SYNC_SCREEN)) {
when (layoutParams) {
is RegistrationScaffold.Params.OnePane -> OnePane(layoutParams, state, onEvent)
@@ -175,32 +181,37 @@ private fun FirstPaneContent(
modifier = Modifier.padding(top = 16.dp)
)
val showDownloadProgress = state.totalBytes.bytes > 0 && !state.isFinishing
val progressModifier = Modifier
.padding(top = 48.dp, bottom = 16.dp)
.widthIn(max = 415.dp)
.fillMaxWidth()
if (showDownloadProgress) {
LinearProgressIndicator(
progress = { state.downloadedBytes.percentageOf(state.totalBytes) },
when (val stage = state.stage) {
is Stage.Downloading -> LinearProgressIndicator(
progress = { stage.downloaded.percentageOf(stage.total) },
drawStopIndicator = {},
gapSize = 0.dp,
modifier = progressModifier
)
} else {
LinearProgressIndicator(modifier = progressModifier)
is Stage.Restoring -> LinearProgressIndicator(
progress = { stage.restored.percentageOf(stage.total) },
drawStopIndicator = {},
gapSize = 0.dp,
modifier = progressModifier
)
Stage.Preparing, Stage.Finishing -> LinearProgressIndicator(modifier = progressModifier)
}
Text(
text = when {
state.isFinishing -> stringResource(R.string.MessageSyncScreen__finishing)
showDownloadProgress -> stringResource(
text = when (val stage = state.stage) {
Stage.Preparing -> stringResource(R.string.MessageSyncScreen__preparing)
is Stage.Downloading -> stringResource(
R.string.MessageSyncScreen__downloading_s_of_s,
state.downloadedBytes.toUnitString(),
state.totalBytes.toUnitString()
stage.downloaded.toUnitString(),
stage.total.toUnitString()
)
else -> stringResource(R.string.MessageSyncScreen__preparing)
is Stage.Restoring -> stringResource(R.string.MessageSyncScreen__restoring)
Stage.Finishing -> stringResource(R.string.MessageSyncScreen__finishing)
},
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
@@ -369,8 +380,7 @@ private fun MessageSyncScreenPreview() {
Previews.Preview {
MessageSyncScreen(
state = MessageSyncScreenState(
downloadedBytes = 1.mebiBytes,
totalBytes = 3300.kibiBytes
stage = Stage.Downloading(downloaded = 1.mebiBytes, total = 3300.kibiBytes)
),
onEvent = {}
)
@@ -6,11 +6,34 @@
package org.signal.registration.screens.messagesync
import org.signal.core.util.ByteSize
import org.signal.core.util.bytes
/**
* UI state for the message-sync screen.
*
* [stage] is the single source of truth for which phase of the link-and-sync restore we're showing.
* [showSyncFailedDialog] is an independent overlay shown when the restore fails.
*/
data class MessageSyncScreenState(
val downloadedBytes: ByteSize = 0.bytes,
val totalBytes: ByteSize = 0.bytes,
val isFinishing: Boolean = false,
val stage: Stage = Stage.Preparing,
val showSyncFailedDialog: Boolean = false
)
) {
/** Whether we've moved past cancelable progress into non-cancelable finalization. */
val isFinishing: Boolean
get() = stage is Stage.Finishing
/** The phase of the restore currently surfaced to the user. */
sealed interface Stage {
/** Waiting for the first progress event; shown as an indeterminate bar. */
data object Preparing : Stage
/** Downloading the backup from the primary/CDN; shown as determinate progress. */
data class Downloading(val downloaded: ByteSize, val total: ByteSize) : Stage
/** Importing messages from the downloaded backup; shown as determinate progress. */
data class Restoring(val restored: ByteSize, val total: ByteSize) : Stage
/** Post-restore finalization that doesn't report granular progress; shown as an indeterminate bar. */
data object Finishing : Stage
}
}
@@ -19,12 +19,12 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.signal.core.ui.compose.EventDrivenViewModel
import org.signal.core.util.bytes
import org.signal.core.util.logging.Log
import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.RegistrationRoute
import org.signal.registration.screens.messagesync.MessageSyncScreenState.Stage
import org.signal.registration.screens.util.navigateTo
/**
@@ -62,16 +62,21 @@ class MessageSyncViewModel(
when (progress) {
is LinkAndSyncProgress.Waiting -> Unit
is LinkAndSyncProgress.Downloading -> _state.update {
it.copy(downloadedBytes = progress.bytesDownloaded, totalBytes = progress.totalBytes)
it.copy(stage = Stage.Downloading(downloaded = progress.bytesDownloaded, total = progress.totalBytes))
}
is LinkAndSyncProgress.Restoring -> _state.update {
it.copy(stage = Stage.Restoring(restored = progress.bytesRestored, total = progress.totalBytes))
}
is LinkAndSyncProgress.Finalizing -> _state.update {
it.copy(stage = Stage.Finishing)
}
is LinkAndSyncProgress.Restoring -> _state.update { it.copy(isFinishing = true) }
is LinkAndSyncProgress.Complete -> {
Log.i(TAG, "[MessageSync] Link-and-sync complete; restoring from storage service then completing.")
finish()
}
is LinkAndSyncProgress.Failed -> {
Log.w(TAG, "[MessageSync] Link-and-sync failed; prompting the user to retry or continue without messages.", progress.cause)
_state.update { it.copy(isFinishing = false, showSyncFailedDialog = true) }
_state.update { it.copy(showSyncFailedDialog = true) }
}
is LinkAndSyncProgress.RelinkRequired -> {
Log.w(TAG, "[MessageSync] Primary requested re-link; wiping local data and restarting.")
@@ -88,7 +93,7 @@ class MessageSyncViewModel(
}
finishJob = viewModelScope.launch {
_state.update { it.copy(isFinishing = true) }
_state.update { it.copy(stage = Stage.Finishing) }
if (cancelDownload) {
restoreJob?.cancelAndJoin()
}
@@ -107,19 +112,19 @@ class MessageSyncViewModel(
MessageSyncScreenEvent.LearnMoreClick -> error("This event is handled in the nav-entry.")
MessageSyncScreenEvent.CancelClick -> {
Log.i(TAG, "[MessageSync] User cancelled message sync; awaiting restore cancellation, then restoring from storage service and completing.")
stateEmitter(state.copy(isFinishing = true))
stateEmitter(state.copy(stage = Stage.Finishing))
finish(cancelDownload = true)
state.copy(isFinishing = true)
state.copy(stage = Stage.Finishing)
}
MessageSyncScreenEvent.RetryClick -> {
Log.i(TAG, "[MessageSync] User retrying link-and-sync after a failure.")
startRestore()
state.copy(showSyncFailedDialog = false, isFinishing = false, downloadedBytes = 0.bytes, totalBytes = 0.bytes)
state.copy(showSyncFailedDialog = false, stage = Stage.Preparing)
}
MessageSyncScreenEvent.ContinueWithoutMessagesClick -> {
Log.i(TAG, "[MessageSync] User continuing without message history after a failed link-and-sync.")
finish()
state.copy(showSyncFailedDialog = false, isFinishing = true)
state.copy(showSyncFailedDialog = false, stage = Stage.Finishing)
}
}
stateEmitter(result)
@@ -38,6 +38,7 @@ import org.signal.core.models.AccountEntropyPool
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.KeepScreenOnEffect
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.theme.SignalTheme
@@ -57,6 +58,10 @@ fun RemoteRestoreScreen(
onEvent: (RemoteBackupRestoreScreenEvents) -> Unit,
modifier: Modifier = Modifier
) {
if (state.restoreState == RemoteBackupRestoreState.RestoreState.InProgress) {
KeepScreenOnEffect()
}
when (state.loadState) {
RemoteBackupRestoreState.LoadState.Loading -> {
Dialogs.IndeterminateProgressDialog(
@@ -443,6 +443,8 @@
<string name="MessageSyncScreen__preparing">Preparing…</string>
<!-- Progress label shown under the sync progress bar while messages are being imported and the rest of the account is restored, after the download finishes -->
<string name="MessageSyncScreen__finishing">Finishing…</string>
<!-- Progress label shown while messages are being imported/restored from the downloaded backup -->
<string name="MessageSyncScreen__restoring">Restoring…</string>
<!-- Informational notice in the sync screen footer, indicating the sync is end-to-end encrypted. Precedes a separate "Learn more" link. -->
<string name="MessageSyncScreen__messages_and_chat_info_are_protected_by_e2ee">Messages and chat info are protected by end-to-end encryption, including the syncing process.</string>
<!-- Inline link in the sync screen footer that opens documentation about end-to-end encryption during sync -->
@@ -12,6 +12,7 @@ import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.RtlPreview
import org.signal.core.util.kibiBytes
import org.signal.core.util.mebiBytes
import org.signal.registration.screens.messagesync.MessageSyncScreenState.Stage
class MessageSyncScreenScreenshotTests {
@PreviewTest
@@ -22,11 +23,51 @@ class MessageSyncScreenScreenshotTests {
Previews.Preview {
MessageSyncScreen(
state = MessageSyncScreenState(
downloadedBytes = 1.mebiBytes,
totalBytes = 3300.kibiBytes
stage = Stage.Downloading(downloaded = 1.mebiBytes, total = 3300.kibiBytes)
),
onEvent = {}
)
}
}
@PreviewTest
@AllDevicePreviews
@RtlPreview
@Composable
fun MessageSyncScreenPreparingPreview() {
Previews.Preview {
MessageSyncScreen(
state = MessageSyncScreenState(stage = Stage.Preparing),
onEvent = {}
)
}
}
@PreviewTest
@AllDevicePreviews
@RtlPreview
@Composable
fun MessageSyncScreenRestoringPreview() {
Previews.Preview {
MessageSyncScreen(
state = MessageSyncScreenState(
stage = Stage.Restoring(restored = 2.mebiBytes, total = 3300.kibiBytes)
),
onEvent = {}
)
}
}
@PreviewTest
@AllDevicePreviews
@RtlPreview
@Composable
fun MessageSyncScreenFinishingPreview() {
Previews.Preview {
MessageSyncScreen(
state = MessageSyncScreenState(stage = Stage.Finishing),
onEvent = {}
)
}
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f278203913e71bdf003996c021290c9c5fd2f5b4d171fc03559530f780cc1353
size 131987
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c4843e9203cdc31b21fffaa97abcdaef2771db26946898142d62e2adaa03fe1c
size 128373
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:52d7ad7b4fdcf39a2ae0ede7c1841e0977e8f52505ce9b3652601a65308b0586
size 132584
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:01eab7a28187e31052fffb823c0927056a364919cd687befb627c8438fa2b5a6
size 49229
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4463d008a1c661567ed9072200225299be5651186055505d0361fc6679d4c618
size 49224
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:65327400793ee00bafcf3083de0f35944347434621c144178824741de3860c51
size 58385
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc0dcd4e4fe04d78b96f49b42c04803b6a240d2646496d6f40dbcc878bae6e4e
size 68648
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e5645d7f8e80adfc70b87f8089e72bb5d433928d206786659a8ac125b6750619
size 113827
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e8755ec06c301ba7f2ec55e0393fcfcb61fd04515181837a88e9feb6183bea81
size 112957
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bfd2484d3140e836609f6471d99ce6458a83d08217248e09ae21cd956dd57779
size 104939
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2ff47e333b71686dcd97f5c8dd710dd21d4ecdc232949ed51defae2d691c4392
size 114956
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c7241546eeb98ffa220afb2f57914ca5d36b7ea0760e0623c40321777664a68e
size 95954
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b66f017a10e2cab1d676e2d3cb0eccabaf489078b8a4f1733c0a828d6ff7eec3
size 121562
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:566aef6885cf79f4778d198d1a58da70339d19f4975432d649df3b6cfff43ab3
size 133355
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a7d00202dedccf68454d48bba04c87d64951496c85db3e07d4fba337f0ea53a
size 129920
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:453536e56d62df9da83e9992def0e2e418765b6bab8008f25d74cb33be0c399e
size 133954
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4aa109cdb3d90928af7e6658eb9d5ba62e05140054746e348d8bf831d8baee1b
size 50506
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5b1a3ec2f034cbb91e51841a55a444f24afb1b44c0fe767c47fa0f8152dbfe6a
size 50731
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6c25d96c4f3aac30c4a71583862e3788a9d6df857499ebb6c89d847a4e033a03
size 59528
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:156d77b033986b5a459684fc56b363629d55251b8c51f35c52e4a550061d7928
size 67437
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0c75df96242b6753c01ac0d71c1f158758cc5fcd60e8efb932273da5e2bbcece
size 115207
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8328095d98186d69cb549de2c9c813334e3cd3daf499e63685184ff9cae28ffd
size 114495
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94103353e323be41177b5d323f2aeb7ef349fcb1e177063f62eda053b59bdd7d
size 106317
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1ff00796f1e8f3617c9bb50461f6a6ba5a3064e54335b2bf93507745f53559a9
size 116472
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:252a603ca156958850b13c39a87c31fc0fb0df01e33b3598c9dc8ee2d896089a
size 97583
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c7fa06a697f18d8a1ae6c9e9a151098cf1b7208d85587c87b603d84c6ea64140
size 123110
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:21fe9a94ad9b7f7e3a4daa834eed440be6967896db1e8c4691bc578417f10188
size 134027
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5f2e83f2f47c21e356ebb22e408764f9c1c31e8ce1da89981a981131e5e56fe
size 130546
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d6df760824910a93714d5159f02540582e33f024d53ab4be682e95ae4dc8740c
size 134610
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8c7ee3d2ab87950f5ba97b9dfcee32cbc314975f67cf14731fca2197550fb1f2
size 51094
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:143279658bf04623eed661cd2b1dd3b4f58fc83b791a227272a5a4dd1b251b85
size 51297
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:540c5e3ddf9c0e4d7abd00928498f71f893e05e6bda6bcd6594631b95176a517
size 59993
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fa8378f9482503c5cd38d8d981cf2b737616eee64412b10b011fd336bcfec71b
size 67623
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:42c642fc44f38da4bed19c25d40f9c1c21da375be70ffff6aa0d3f2fb4830efc
size 115856
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6dc72a139219c38bec594ae0404ceadd5bcbebbcf105df42d89468a873bfa5ad
size 115092
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e263b66becb610374fde851fe639eb9a167138b18717bf53806fe32dadb975d8
size 106913
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3d84aae5f7ba7500fb84ba6c0fc2d3a6090b83bda65135ea1e40627c39700a9c
size 117184
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9a54b6bcf3fdf67312813e45d386301c21077b34b3af80bb28459bbccf91ee96
size 98198
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:82cb6e56c2f9efd858c5ea2704dc82207cefe112de1d3c072d8e92726386d9fa
size 123724
@@ -42,7 +42,7 @@ class MessageSyncScreenTest {
composeTestRule.setContent {
SignalTheme {
MessageSyncScreen(
state = MessageSyncScreenState(downloadedBytes = 1.mebiBytes, totalBytes = 3300.kibiBytes),
state = MessageSyncScreenState(stage = MessageSyncScreenState.Stage.Downloading(downloaded = 1.mebiBytes, total = 3300.kibiBytes)),
onEvent = {}
)
}
@@ -59,7 +59,7 @@ class MessageSyncScreenTest {
composeTestRule.setContent {
SignalTheme {
MessageSyncScreen(
state = MessageSyncScreenState(downloadedBytes = 1.mebiBytes, totalBytes = 3300.kibiBytes),
state = MessageSyncScreenState(stage = MessageSyncScreenState.Stage.Downloading(downloaded = 1.mebiBytes, total = 3300.kibiBytes)),
onEvent = { event -> emittedEvent = event }
)
}
@@ -75,7 +75,7 @@ class MessageSyncScreenTest {
composeTestRule.setContent {
SignalTheme {
MessageSyncScreen(
state = MessageSyncScreenState(downloadedBytes = 1.mebiBytes, totalBytes = 3300.kibiBytes),
state = MessageSyncScreenState(stage = MessageSyncScreenState.Stage.Downloading(downloaded = 1.mebiBytes, total = 3300.kibiBytes)),
onEvent = {}
)
}
@@ -89,7 +89,7 @@ class MessageSyncScreenTest {
composeTestRule.setContent {
SignalTheme {
MessageSyncScreen(
state = MessageSyncScreenState(isFinishing = true),
state = MessageSyncScreenState(stage = MessageSyncScreenState.Stage.Finishing),
onEvent = {}
)
}
@@ -8,6 +8,7 @@ package org.signal.registration.screens.messagesync
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.doesNotContain
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isTrue
import io.mockk.coVerify
@@ -136,10 +137,10 @@ class MessageSyncViewModelTest {
}
@Test
fun `restore Restoring switches to indeterminate finishing state`() = runTest(testDispatcher) {
fun `restore Finalizing switches to indeterminate finishing state`() = runTest(testDispatcher) {
every { mockRepository.restoreLinkAndSyncBackup() } returns flowOf(
LinkAndSyncProgress.Downloading(bytesDownloaded = 5.bytes, totalBytes = 10.bytes),
LinkAndSyncProgress.Restoring
LinkAndSyncProgress.Finalizing
)
val viewModel = createViewModel()
@@ -147,6 +148,18 @@ class MessageSyncViewModelTest {
assertThat(viewModel.state.value.isFinishing).isTrue()
}
@Test
fun `restore Restoring surfaces determinate restore progress without finishing`() = runTest(testDispatcher) {
every { mockRepository.restoreLinkAndSyncBackup() } returns flowOf(
LinkAndSyncProgress.Restoring(bytesRestored = 3.bytes, totalBytes = 10.bytes)
)
val viewModel = createViewModel()
assertThat(viewModel.state.value.stage).isEqualTo(MessageSyncScreenState.Stage.Restoring(restored = 3.bytes, total = 10.bytes))
assertThat(viewModel.state.value.isFinishing).isFalse()
}
private fun TestScope.createViewModel(): MessageSyncViewModel {
val viewModel = MessageSyncViewModel(
repository = mockRepository,