Keep device alive during backup restore flows and show progress for link and sync import.
@@ -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 |
@@ -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,
|
||||
|
||||