mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-27 21:24:42 +00:00
Hook up message backup restore flow to reg v2.
Co-authored-by: Nicholas Tinsley <nicholas@signal.org>
This commit is contained in:
@@ -55,7 +55,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
private static final int STATE_TRANSFER_ONGOING = 8;
|
||||
private static final int STATE_TRANSFER_LOCKED = 9;
|
||||
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
|
||||
private static final int STATE_RESTORE_BACKUP = 11;
|
||||
private static final int STATE_TRANSFER_OR_RESTORE = 11;
|
||||
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
@@ -153,7 +153,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
|
||||
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
|
||||
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
|
||||
case STATE_RESTORE_BACKUP: return getRestoreIntent();
|
||||
case STATE_TRANSFER_OR_RESTORE: return getTransferOrRestoreIntent();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -167,12 +167,12 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return STATE_UI_BLOCKING_UPGRADE;
|
||||
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
|
||||
return STATE_WELCOME_PUSH_SCREEN;
|
||||
} else if (SignalStore.internalValues().enterRestoreV2Flow()) {
|
||||
return STATE_RESTORE_BACKUP;
|
||||
} else if (SignalStore.storageService().needsAccountRestore()) {
|
||||
return STATE_ENTER_SIGNAL_PIN;
|
||||
} else if (userHasSkippedOrForgottenPin()) {
|
||||
return STATE_CREATE_SIGNAL_PIN;
|
||||
} else if (userCanTransferOrRestore()) {
|
||||
return STATE_TRANSFER_OR_RESTORE;
|
||||
} else if (userMustSetProfileName()) {
|
||||
return STATE_CREATE_PROFILE_NAME;
|
||||
} else if (userMustCreateSignalPin()) {
|
||||
@@ -188,6 +188,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
}
|
||||
}
|
||||
|
||||
private boolean userCanTransferOrRestore() {
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && FeatureFlags.restoreAfterRegistration() && !SignalStore.registrationValues().hasSkippedTransferOrRestore();
|
||||
}
|
||||
|
||||
private boolean userMustCreateSignalPin() {
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.svr().hasPin() && !SignalStore.svr().lastPinCreateFailed() && !SignalStore.svr().hasOptedOut();
|
||||
}
|
||||
@@ -241,8 +245,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return getRoutedIntent(CreateSvrPinActivity.class, intent);
|
||||
}
|
||||
|
||||
private Intent getRestoreIntent() {
|
||||
Intent intent = RestoreActivity.getIntentForRestore(this);
|
||||
private Intent getTransferOrRestoreIntent() {
|
||||
Intent intent = RestoreActivity.getIntentForTransferOrRestore(this);
|
||||
return getRoutedIntent(intent, getIntent());
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.EventTimer
|
||||
import org.signal.core.util.LongSerializer
|
||||
@@ -14,6 +15,7 @@ import org.signal.libsignal.messagebackup.MessageBackup
|
||||
import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
|
||||
import org.signal.libsignal.messagebackup.MessageBackupKey
|
||||
import org.signal.libsignal.protocol.ServiceId.Aci
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
@@ -58,6 +60,7 @@ import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.Exception
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
object BackupRepository {
|
||||
@@ -152,12 +155,12 @@ object BackupRepository {
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
val frameReader = if (plaintext) {
|
||||
PlainTextBackupReader(inputStreamFactory())
|
||||
PlainTextBackupReader(inputStreamFactory(), length)
|
||||
} else {
|
||||
EncryptedBackupReader(
|
||||
key = backupKey,
|
||||
aci = selfData.aci,
|
||||
streamLength = length,
|
||||
length = length,
|
||||
dataStream = inputStreamFactory
|
||||
)
|
||||
}
|
||||
@@ -190,6 +193,7 @@ object BackupRepository {
|
||||
val backupState = BackupState(backupKey)
|
||||
val chatItemInserter: ChatItemImportInserter = ChatItemBackupProcessor.beginImport(backupState)
|
||||
|
||||
val totalLength = frameReader.getStreamLength()
|
||||
for (frame in frameReader) {
|
||||
when {
|
||||
frame.account != null -> {
|
||||
@@ -220,6 +224,7 @@ object BackupRepository {
|
||||
|
||||
else -> Log.w(TAG, "Unrecognized frame")
|
||||
}
|
||||
EventBus.getDefault().post(RestoreV2Event(RestoreV2Event.Type.PROGRESS_RESTORE, frameReader.getBytesRead(), totalLength))
|
||||
}
|
||||
|
||||
if (chatItemInserter.flush()) {
|
||||
@@ -263,6 +268,21 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBackupTier(): NetworkResult<MessageBackupTier> {
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.map { credential ->
|
||||
val zkCredential = api.getZkCredential(backupKey, credential)
|
||||
if (zkCredential.backupLevel == BackupLevel.MEDIA) {
|
||||
MessageBackupTier.PAID
|
||||
} else {
|
||||
MessageBackupTier.FREE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object with details about the remote backup state.
|
||||
*/
|
||||
@@ -331,6 +351,24 @@ object BackupRepository {
|
||||
} is NetworkResult.Success
|
||||
}
|
||||
|
||||
fun checkForBackupFile(): Boolean {
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
api.getBackupInfo(backupKey, credential)
|
||||
}
|
||||
.then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } }
|
||||
.then { pair ->
|
||||
val (cdnCredentials, info) = pair
|
||||
val messageReceiver = AppDependencies.signalServiceMessageReceiver
|
||||
NetworkResult.fromFetch {
|
||||
messageReceiver.checkBackupExistence(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}")
|
||||
}
|
||||
} is NetworkResult.Success
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object with details about the remote backup state.
|
||||
*/
|
||||
@@ -560,6 +598,24 @@ object BackupRepository {
|
||||
.also { Log.i(TAG, "getCdnReadCredentialsResult: $it") }
|
||||
}
|
||||
|
||||
fun restoreBackupTier(): MessageBackupTier? {
|
||||
// TODO: more complete error handling
|
||||
try {
|
||||
checkForBackupFile()
|
||||
} catch (e: Exception) {
|
||||
Log.i(TAG, "Could not check for backup file.", e)
|
||||
SignalStore.backup().backupTier = null
|
||||
return null
|
||||
}
|
||||
SignalStore.backup().backupTier = try {
|
||||
getBackupTier().successOrThrow()
|
||||
} catch (e: Exception) {
|
||||
Log.i(TAG, "Could not retrieve backup tier.", e)
|
||||
null
|
||||
}
|
||||
return SignalStore.backup().backupTier
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves backupDir and mediaDir, preferring cached value if available.
|
||||
*
|
||||
@@ -693,13 +749,13 @@ enum class MessageBackupTier(val value: Int) {
|
||||
FREE(0),
|
||||
PAID(1);
|
||||
|
||||
companion object Serializer : LongSerializer<MessageBackupTier> {
|
||||
override fun serialize(data: MessageBackupTier): Long {
|
||||
return data.value.toLong()
|
||||
companion object Serializer : LongSerializer<MessageBackupTier?> {
|
||||
override fun serialize(data: MessageBackupTier?): Long {
|
||||
return data?.value?.toLong() ?: -1
|
||||
}
|
||||
|
||||
override fun deserialize(data: Long): MessageBackupTier {
|
||||
return values().firstOrNull { it.value == data.toInt() } ?: FREE
|
||||
override fun deserialize(data: Long): MessageBackupTier? {
|
||||
return values().firstOrNull { it.value == data.toInt() } ?: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
class RestoreV2Event(val type: Type, val count: Long, val estimatedTotalCount: Long) {
|
||||
enum class Type {
|
||||
PROGRESS_DOWNLOAD,
|
||||
PROGRESS_RESTORE,
|
||||
PROGRESS_MEDIA_RESTORE,
|
||||
FINISHED
|
||||
}
|
||||
|
||||
fun getProgress(): Float {
|
||||
if (estimatedTotalCount == 0L) {
|
||||
return 0f
|
||||
}
|
||||
return count.toFloat() / estimatedTotalCount.toFloat()
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,6 @@ import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
|
||||
interface BackupImportReader : Iterator<Frame>, AutoCloseable {
|
||||
fun getHeader(): BackupInfo?
|
||||
fun getBytesRead(): Long
|
||||
fun getStreamLength(): Long
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.stream
|
||||
|
||||
import com.google.common.io.CountingInputStream
|
||||
import org.signal.core.util.readFully
|
||||
import org.signal.core.util.readNBytesOrThrow
|
||||
import org.signal.core.util.readVarInt32
|
||||
@@ -32,21 +33,22 @@ import javax.crypto.spec.SecretKeySpec
|
||||
class EncryptedBackupReader(
|
||||
key: BackupKey,
|
||||
aci: ACI,
|
||||
streamLength: Long,
|
||||
val length: Long,
|
||||
dataStream: () -> InputStream
|
||||
) : BackupImportReader {
|
||||
|
||||
val backupInfo: BackupInfo?
|
||||
var next: Frame? = null
|
||||
val stream: InputStream
|
||||
val countingStream: CountingInputStream
|
||||
|
||||
init {
|
||||
val keyMaterial = key.deriveBackupSecrets(aci)
|
||||
|
||||
validateMac(keyMaterial.macKey, streamLength, dataStream())
|
||||
validateMac(keyMaterial.macKey, length, dataStream())
|
||||
|
||||
val inputStream = dataStream()
|
||||
val iv = inputStream.readNBytesOrThrow(16)
|
||||
countingStream = CountingInputStream(dataStream())
|
||||
val iv = countingStream.readNBytesOrThrow(16)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply {
|
||||
init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv))
|
||||
@@ -55,8 +57,8 @@ class EncryptedBackupReader(
|
||||
stream = GZIPInputStream(
|
||||
CipherInputStream(
|
||||
TruncatingInputStream(
|
||||
wrapped = inputStream,
|
||||
maxBytes = streamLength - MAC_SIZE
|
||||
wrapped = countingStream,
|
||||
maxBytes = length - MAC_SIZE
|
||||
),
|
||||
cipher
|
||||
)
|
||||
@@ -69,6 +71,10 @@ class EncryptedBackupReader(
|
||||
return backupInfo
|
||||
}
|
||||
|
||||
override fun getBytesRead() = countingStream.count
|
||||
|
||||
override fun getStreamLength() = length
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return next != null
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.stream
|
||||
|
||||
import com.google.common.io.CountingInputStream
|
||||
import org.signal.core.util.readNBytesOrThrow
|
||||
import org.signal.core.util.readVarInt32
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
@@ -15,12 +16,14 @@ import java.io.InputStream
|
||||
/**
|
||||
* Reads a plaintext backup import stream one frame at a time.
|
||||
*/
|
||||
class PlainTextBackupReader(val inputStream: InputStream) : BackupImportReader {
|
||||
class PlainTextBackupReader(val dataStream: InputStream, val length: Long) : BackupImportReader {
|
||||
|
||||
val backupInfo: BackupInfo?
|
||||
var next: Frame? = null
|
||||
val inputStream: CountingInputStream
|
||||
|
||||
init {
|
||||
inputStream = CountingInputStream(dataStream)
|
||||
backupInfo = readHeader()
|
||||
next = read()
|
||||
}
|
||||
@@ -29,6 +32,10 @@ class PlainTextBackupReader(val inputStream: InputStream) : BackupImportReader {
|
||||
return backupInfo
|
||||
}
|
||||
|
||||
override fun getBytesRead() = inputStream.count
|
||||
|
||||
override fun getStreamLength() = length
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return next != null
|
||||
}
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.util.getLength
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||
|
||||
class MessageBackupsTestRestoreActivity : BaseActivity() {
|
||||
companion object {
|
||||
fun getIntent(context: Context): Intent {
|
||||
return Intent(context, MessageBackupsTestRestoreActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel: MessageBackupsTestRestoreViewModel by viewModels()
|
||||
private lateinit var importFileLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
private fun onPlaintextClicked() {
|
||||
viewModel.onPlaintextToggled()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
importFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
contentResolver.getLength(uri)?.let { length ->
|
||||
viewModel.import(length) { contentResolver.openInputStream(uri)!! }
|
||||
}
|
||||
} ?: Toast.makeText(this, "No URI selected", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val state by viewModel.state
|
||||
Surface {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Buttons.LargePrimary(
|
||||
onClick = this@MessageBackupsTestRestoreActivity::restoreFromServer,
|
||||
enabled = !state.importState.inProgress
|
||||
) {
|
||||
Text("Restore")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
StateLabel(text = "Plaintext?")
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Switch(
|
||||
checked = state.plaintext,
|
||||
onCheckedChange = { onPlaintextClicked() }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = {
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_GET_CONTENT
|
||||
type = "application/octet-stream"
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
}
|
||||
|
||||
importFileLauncher.launch(intent)
|
||||
},
|
||||
enabled = !state.importState.inProgress
|
||||
) {
|
||||
Text("Import from file")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = { continueRegistration() },
|
||||
enabled = !state.importState.inProgress
|
||||
) {
|
||||
Text("Continue Reg Flow")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.importState == MessageBackupsTestRestoreViewModel.ImportState.RESTORED) {
|
||||
SideEffect {
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
AppDependencies.jobManager.add(ProfileUploadJob())
|
||||
startActivity(MainActivity.clearTop(this))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreFromServer() {
|
||||
viewModel.restore()
|
||||
}
|
||||
|
||||
private fun continueRegistration() {
|
||||
if (Recipient.self().profileName.isEmpty || !AvatarHelper.hasAvatar(this, Recipient.self().id)) {
|
||||
val main = MainActivity.clearTop(this)
|
||||
val profile = CreateProfileActivity.getIntentForUserProfile(this)
|
||||
profile.putExtra("next_intent", main)
|
||||
startActivity(profile)
|
||||
} else {
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
AppDependencies.jobManager.add(ProfileUploadJob())
|
||||
startActivity(MainActivity.clearTop(this))
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StateLabel(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -17,18 +17,23 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.RestoreV2Event
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.BackupRestoreJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.SyncArchivedMediaJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||
import java.io.InputStream
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class MessageBackupsTestRestoreViewModel : ViewModel() {
|
||||
class RemoteRestoreViewModel : ViewModel() {
|
||||
val disposables = CompositeDisposable()
|
||||
|
||||
private val _state: MutableState<ScreenState> = mutableStateOf(ScreenState(importState = ImportState.NONE, plaintext = false))
|
||||
private val _state: MutableState<ScreenState> = mutableStateOf(ScreenState(backupTier = SignalStore.backup().backupTier, importState = ImportState.NONE, restoreProgress = null))
|
||||
|
||||
val state: State<ScreenState> = _state
|
||||
|
||||
fun import(length: Long, inputStreamFactory: () -> InputStream) {
|
||||
@@ -37,7 +42,7 @@ class MessageBackupsTestRestoreViewModel : ViewModel() {
|
||||
val self = Recipient.self()
|
||||
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
|
||||
|
||||
disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData, plaintext = _state.value.plaintext) }
|
||||
disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
@@ -54,6 +59,7 @@ class MessageBackupsTestRestoreViewModel : ViewModel() {
|
||||
.then(SyncArchivedMediaJob())
|
||||
.then(BackupRestoreMediaJob())
|
||||
.enqueueAndBlockUntilCompletion(120.seconds.inWholeMilliseconds)
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@@ -62,8 +68,8 @@ class MessageBackupsTestRestoreViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onPlaintextToggled() {
|
||||
_state.value = _state.value.copy(plaintext = !_state.value.plaintext)
|
||||
fun updateRestoreProgress(restoreEvent: RestoreV2Event) {
|
||||
_state.value = _state.value.copy(restoreProgress = restoreEvent)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
@@ -71,8 +77,9 @@ class MessageBackupsTestRestoreViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
data class ScreenState(
|
||||
val backupTier: MessageBackupTier?,
|
||||
val importState: ImportState,
|
||||
val plaintext: Boolean
|
||||
val restoreProgress: RestoreV2Event?
|
||||
)
|
||||
|
||||
enum class ImportState(val inProgress: Boolean = false) {
|
||||
@@ -816,6 +816,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
SignalStore.account().setRegistered(false)
|
||||
SignalStore.registrationValues().clearRegistrationComplete()
|
||||
SignalStore.registrationValues().clearHasUploadedProfile()
|
||||
SignalStore.registrationValues().clearSkippedTransferOrRestore()
|
||||
Toast.makeText(context, "Unregistered!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.RestoreV2Event
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
@@ -71,6 +73,7 @@ class BackupRestoreJob private constructor(parameters: Parameters) : BaseJob(par
|
||||
progress = progress.toFloat() / total.toFloat(),
|
||||
indeterminate = false
|
||||
)
|
||||
EventBus.getDefault().post(RestoreV2Event(RestoreV2Event.Type.PROGRESS_DOWNLOAD, progress, total))
|
||||
}
|
||||
|
||||
override fun shouldCancel() = isCanceled
|
||||
|
||||
@@ -60,7 +60,7 @@ internal class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
var nextBackupTime: Long by longValue(KEY_NEXT_BACKUP_TIME, -1)
|
||||
var lastBackupTime: Long by longValue(KEY_LAST_BACKUP_TIME, -1)
|
||||
var backupFrequency: BackupFrequency by enumValue(KEY_BACKUP_FREQUENCY, BackupFrequency.MANUAL, BackupFrequency.Serializer)
|
||||
var backupTier: MessageBackupTier by enumValue(KEY_BACKUP_TIER, MessageBackupTier.FREE, MessageBackupTier.Serializer)
|
||||
var backupTier: MessageBackupTier? by enumValue(KEY_BACKUP_TIER, null, MessageBackupTier.Serializer)
|
||||
|
||||
val totalBackupSize: Long get() = lastBackupProtoSize + usedBackupMediaSpace
|
||||
|
||||
|
||||
@@ -199,14 +199,6 @@ public final class InternalValues extends SignalStoreValues {
|
||||
return FeatureFlags.internalUser() && getBoolean(CONVERSATION_ITEM_V2_MEDIA, false);
|
||||
}
|
||||
|
||||
public void setForceEnterRestoreV2Flow(boolean enter) {
|
||||
putBoolean(FORCE_ENTER_RESTORE_V2_FLOW, enter);
|
||||
}
|
||||
|
||||
public boolean enterRestoreV2Flow() {
|
||||
return FeatureFlags.restoreAfterRegistration() && getBoolean(FORCE_ENTER_RESTORE_V2_FLOW, false);
|
||||
}
|
||||
|
||||
public synchronized void setWebSocketShadowingStats(byte[] bytes) {
|
||||
putBlob(WEB_SOCKET_SHADOWING_STATS, bytes);
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ import java.util.List;
|
||||
|
||||
public final class RegistrationValues extends SignalStoreValues {
|
||||
|
||||
private static final String REGISTRATION_COMPLETE = "registration.complete";
|
||||
private static final String PIN_REQUIRED = "registration.pin_required";
|
||||
private static final String HAS_UPLOADED_PROFILE = "registration.has_uploaded_profile";
|
||||
private static final String SESSION_E164 = "registration.session_e164";
|
||||
private static final String SESSION_ID = "registration.session_id";
|
||||
private static final String REGISTRATION_COMPLETE = "registration.complete";
|
||||
private static final String PIN_REQUIRED = "registration.pin_required";
|
||||
private static final String HAS_UPLOADED_PROFILE = "registration.has_uploaded_profile";
|
||||
private static final String SESSION_E164 = "registration.session_e164";
|
||||
private static final String SESSION_ID = "registration.session_id";
|
||||
private static final String SKIPPED_TRANSFER_OR_RESTORE = "registration.has_skipped_transfer_or_restore";
|
||||
|
||||
RegistrationValues(@NonNull KeyValueStore store) {
|
||||
super(store);
|
||||
@@ -24,6 +25,7 @@ public final class RegistrationValues extends SignalStoreValues {
|
||||
.putBoolean(HAS_UPLOADED_PROFILE, false)
|
||||
.putBoolean(REGISTRATION_COMPLETE, false)
|
||||
.putBoolean(PIN_REQUIRED, true)
|
||||
.putBoolean(SKIPPED_TRANSFER_OR_RESTORE, false)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@@ -68,6 +70,18 @@ public final class RegistrationValues extends SignalStoreValues {
|
||||
putString(SESSION_ID, sessionId);
|
||||
}
|
||||
|
||||
public boolean hasSkippedTransferOrRestore() {
|
||||
return getBoolean(SKIPPED_TRANSFER_OR_RESTORE, false);
|
||||
}
|
||||
|
||||
public void markSkippedTransferOrRestore() {
|
||||
putBoolean(SKIPPED_TRANSFER_OR_RESTORE, true);
|
||||
}
|
||||
|
||||
public void clearSkippedTransferOrRestore() {
|
||||
putBoolean(SKIPPED_TRANSFER_OR_RESTORE, false);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSessionId() {
|
||||
return getString(SESSION_ID, null);
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTestRestoreActivity;
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -38,7 +38,9 @@ import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate;
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
@@ -238,8 +240,8 @@ public class PinRestoreEntryFragment extends LoggingFragment {
|
||||
|
||||
Activity activity = requireActivity();
|
||||
|
||||
if (BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED) {
|
||||
startActivity(MessageBackupsTestRestoreActivity.Companion.getIntent(activity));
|
||||
if (FeatureFlags.messageBackups()) {
|
||||
startActivity(RestoreActivity.getIntentForTransferOrRestore(activity));
|
||||
} else if (Recipient.self().getProfileName().isEmpty() || !AvatarHelper.hasAvatar(activity, Recipient.self().getId())) {
|
||||
final Intent main = MainActivity.clearTop(activity);
|
||||
final Intent profile = CreateProfileActivity.getIntentForUserProfile(activity);
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
import org.thoughtcrime.securesms.lock.v2.SvrConstants
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData
|
||||
@@ -35,7 +36,11 @@ class PinRestoreViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
disposables += Single
|
||||
.fromCallable { repo.restoreMasterKeyPostRegistration(pin, pinKeyboardType) }
|
||||
.fromCallable {
|
||||
val response = repo.restoreMasterKeyPostRegistration(pin, pinKeyboardType)
|
||||
BackupRepository.restoreBackupTier()
|
||||
response
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result ->
|
||||
|
||||
@@ -23,7 +23,9 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.SmsRetrieverReceiver
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
|
||||
/**
|
||||
* Activity to hold the entire registration process.
|
||||
@@ -76,8 +78,6 @@ class RegistrationV2Activity : BaseActivity() {
|
||||
|
||||
Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin")
|
||||
|
||||
SignalStore.internalValues().setForceEnterRestoreV2Flow(true)
|
||||
|
||||
if (!needsProfile && !needsPin) {
|
||||
sharedViewModel.completeRegistration()
|
||||
}
|
||||
@@ -86,9 +86,9 @@ class RegistrationV2Activity : BaseActivity() {
|
||||
val startIntent = MainActivity.clearTop(this).apply {
|
||||
if (needsPin) {
|
||||
putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationV2Activity))
|
||||
}
|
||||
|
||||
if (needsProfile) {
|
||||
} else if (!SignalStore.registrationValues().hasSkippedTransferOrRestore() && FeatureFlags.messageBackups()) {
|
||||
putExtra("next_intent", RemoteRestoreActivity.getIntent(this@RegistrationV2Activity))
|
||||
} else if (needsProfile) {
|
||||
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationV2Activity))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
|
||||
@@ -748,6 +749,8 @@ class RegistrationV2ViewModel : ViewModel() {
|
||||
Log.v(TAG, "onSuccessfulRegistration()")
|
||||
RegistrationRepository.registerAccountLocally(context, registrationData, remoteResult, reglockEnabled)
|
||||
|
||||
restoreBackupTier()
|
||||
|
||||
if (reglockEnabled) {
|
||||
SignalStore.onboarding().clearAll()
|
||||
val stopwatch = Stopwatch("RegistrationLockRestore")
|
||||
@@ -838,6 +841,12 @@ class RegistrationV2ViewModel : ViewModel() {
|
||||
companion object {
|
||||
private val TAG = Log.tag(RegistrationV2ViewModel::class.java)
|
||||
|
||||
private suspend fun restoreBackupTier() = withContext(Dispatchers.IO) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
BackupRepository.restoreBackupTier()
|
||||
Log.i(TAG, "Took " + (System.currentTimeMillis() - startTime) + " ms to restore the backup tier..")
|
||||
}
|
||||
|
||||
private suspend fun refreshFeatureFlags() = withContext(Dispatchers.IO) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
try {
|
||||
|
||||
@@ -104,7 +104,7 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
|
||||
when (welcomeAction) {
|
||||
WelcomeAction.CONTINUE -> findNavController().safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber())
|
||||
WelcomeAction.RESTORE_BACKUP -> {
|
||||
val restoreIntent = RestoreActivity.getIntentForRestore(requireActivity())
|
||||
val restoreIntent = RestoreActivity.getIntentForTransferOrRestore(requireActivity())
|
||||
launchRestoreActivity.launch(restoreIntent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.restore
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.RestoreV2Event
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeatureRow
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.RemoteRestoreViewModel
|
||||
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.restore.transferorrestore.TransferOrRestoreMoreOptionsDialog
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
|
||||
class RemoteRestoreActivity : BaseActivity() {
|
||||
companion object {
|
||||
fun getIntent(context: Context): Intent {
|
||||
return Intent(context, RemoteRestoreActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel: RemoteRestoreViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
val state by viewModel.state
|
||||
SignalTheme {
|
||||
Surface {
|
||||
RestoreFromBackupContent(
|
||||
features = getFeatureList(state.backupTier),
|
||||
onRestoreBackupClick = {
|
||||
viewModel.restore()
|
||||
},
|
||||
onCancelClick = {
|
||||
finish()
|
||||
},
|
||||
onMoreOptionsClick = {
|
||||
TransferOrRestoreMoreOptionsDialog.show(fragmentManager = supportFragmentManager, skipOnly = false)
|
||||
},
|
||||
state.backupTier,
|
||||
state.backupTier != MessageBackupTier.PAID
|
||||
)
|
||||
if (state.importState == RemoteRestoreViewModel.ImportState.RESTORED) {
|
||||
SideEffect {
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
AppDependencies.jobManager.add(ProfileUploadJob())
|
||||
startActivity(MainActivity.clearTop(this))
|
||||
}
|
||||
} else if (state.importState == RemoteRestoreViewModel.ImportState.IN_PROGRESS) {
|
||||
ProgressDialog(state.restoreProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EventBus.getDefault().registerForLifecycle(subscriber = this, lifecycleOwner = this)
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onEvent(restoreEvent: RestoreV2Event) {
|
||||
viewModel.updateRestoreProgress(restoreEvent)
|
||||
}
|
||||
|
||||
private fun getFeatureList(tier: MessageBackupTier?): ImmutableList<MessageBackupsTypeFeature> {
|
||||
return when (tier) {
|
||||
null -> persistentListOf()
|
||||
MessageBackupTier.PAID -> {
|
||||
persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "All of your media"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_recent_compact_bold_16,
|
||||
label = "All of your text messages"
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageBackupTier.FREE -> {
|
||||
persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Your last 30 days of media"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_recent_compact_bold_16,
|
||||
label = "All of your text messages"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A dialog that *just* shows a spinner. Useful for short actions where you need to
|
||||
* let the user know that some action is completing.
|
||||
*/
|
||||
@Composable
|
||||
fun ProgressDialog(restoreProgress: RestoreV2Event?) {
|
||||
androidx.compose.material3.AlertDialog(
|
||||
onDismissRequest = {},
|
||||
confirmButton = {},
|
||||
dismissButton = {},
|
||||
text = {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.wrapContentSize()
|
||||
) {
|
||||
if (restoreProgress == null) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.padding(top = 55.dp, bottom = 16.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
progress = restoreProgress.getProgress(),
|
||||
modifier = Modifier
|
||||
.padding(top = 55.dp, bottom = 16.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// TODO [message-backups] Finalized copy.
|
||||
val progressText = when (restoreProgress?.type) {
|
||||
RestoreV2Event.Type.PROGRESS_DOWNLOAD -> "Downloading backup..."
|
||||
RestoreV2Event.Type.PROGRESS_RESTORE -> "Restoring messages..."
|
||||
else -> "Restoring..."
|
||||
}
|
||||
|
||||
Text(
|
||||
text = progressText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
|
||||
if (restoreProgress != null) {
|
||||
val progressBytes = Util.getPrettyFileSize(restoreProgress.count)
|
||||
val totalBytes = Util.getPrettyFileSize(restoreProgress.estimatedTotalCount)
|
||||
Text(
|
||||
text = "$progressBytes of $totalBytes (%.2f%%)".format(restoreProgress.getProgress()),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.width(212.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ProgressDialogPreview() {
|
||||
Previews.Preview {
|
||||
ProgressDialog(RestoreV2Event(RestoreV2Event.Type.PROGRESS_RESTORE, 10, 1000))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RestoreFromBackupContentPreview() {
|
||||
Previews.Preview {
|
||||
RestoreFromBackupContent(
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Your last 30 days of media"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_recent_compact_bold_16,
|
||||
label = "All of your text messages"
|
||||
)
|
||||
),
|
||||
onRestoreBackupClick = {},
|
||||
onCancelClick = {},
|
||||
onMoreOptionsClick = {},
|
||||
MessageBackupTier.PAID,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RestoreFromBackupContent(
|
||||
features: ImmutableList<MessageBackupsTypeFeature>,
|
||||
onRestoreBackupClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onMoreOptionsClick: () -> Unit,
|
||||
tier: MessageBackupTier?,
|
||||
cancelable: Boolean
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
.padding(top = 40.dp, bottom = 24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Restore from backup", // TODO [message-backups] Finalized copy.
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
|
||||
val yourLastBackupText = buildAnnotatedString {
|
||||
append("Your last backup was made on March 5, 2024 at 9:00am.") // TODO [message-backups] Finalized copy.
|
||||
append(" ")
|
||||
if (tier != MessageBackupTier.PAID) {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.SemiBold)) {
|
||||
append("Only media sent or received in the past 30 days is included.") // TODO [message-backups] Finalized copy.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = yourLastBackupText,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 28.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(18.dp))
|
||||
.padding(horizontal = 20.dp)
|
||||
.padding(top = 20.dp, bottom = 18.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Your backup includes:", // TODO [message-backups] Finalized copy.
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(bottom = 6.dp)
|
||||
)
|
||||
|
||||
features.forEach {
|
||||
MessageBackupsTypeFeatureRow(
|
||||
messageBackupsTypeFeature = it,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onRestoreBackupClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Restore backup" // TODO [message-backups] Finalized copy.
|
||||
)
|
||||
}
|
||||
|
||||
if (cancelable) {
|
||||
TextButton(
|
||||
onClick = onCancelClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = android.R.string.cancel)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
TextButton(
|
||||
onClick = onMoreOptionsClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.TransferOrRestoreFragment__more_options)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreFromServer() {
|
||||
viewModel.restore()
|
||||
}
|
||||
|
||||
private fun continueRegistration() {
|
||||
if (Recipient.self().profileName.isEmpty || !AvatarHelper.hasAvatar(this, Recipient.self().id)) {
|
||||
val main = MainActivity.clearTop(this)
|
||||
val profile = CreateProfileActivity.getIntentForUserProfile(this)
|
||||
profile.putExtra("next_intent", main)
|
||||
startActivity(profile)
|
||||
} else {
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
AppDependencies.jobManager.add(ProfileUploadJob())
|
||||
startActivity(MainActivity.clearTop(this))
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StateLabel(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,10 @@ import org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermi
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* First screen that is displayed on the very first app launch.
|
||||
@@ -59,6 +61,7 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
|
||||
binding.welcomeContinueButton.setOnClickListener { onContinueClicked() }
|
||||
binding.welcomeTermsButton.setOnClickListener { onTermsClicked() }
|
||||
binding.welcomeTransferOrRestore.setOnClickListener { onTransferOrRestoreClicked() }
|
||||
binding.welcomeTransferOrRestore.visible = !FeatureFlags.restoreAfterRegistration()
|
||||
}
|
||||
|
||||
private fun onContinueClicked() {
|
||||
@@ -86,7 +89,7 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
|
||||
} else {
|
||||
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
|
||||
|
||||
val restoreIntent = RestoreActivity.getIntentForRestore(requireActivity())
|
||||
val restoreIntent = RestoreActivity.getIntentForTransferOrRestore(requireActivity())
|
||||
launchRestoreActivity.launch(restoreIntent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,14 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.navigation.findNavController
|
||||
import org.signal.core.util.getParcelableExtraCompat
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
|
||||
/**
|
||||
@@ -33,6 +37,13 @@ class RestoreActivity : BaseActivity() {
|
||||
intent.getParcelableExtraCompat(PassphraseRequiredActivity.NEXT_INTENT_EXTRA, Intent::class.java)?.let {
|
||||
sharedViewModel.setNextIntent(it)
|
||||
}
|
||||
|
||||
val navTarget = NavTarget.deserialize(intent.getIntExtra(EXTRA_NAV_TARGET, NavTarget.NONE.value))
|
||||
when (navTarget) {
|
||||
NavTarget.LOCAL_RESTORE -> findNavController(R.id.nav_host_fragment).navigate(R.id.choose_local_backup_fragment)
|
||||
NavTarget.TRANSFER -> findNavController(R.id.nav_host_fragment).navigate(R.id.newDeviceTransferInstructions)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -46,8 +57,41 @@ class RestoreActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
enum class NavTarget(val value: Int) {
|
||||
NONE(0),
|
||||
TRANSFER(1),
|
||||
LOCAL_RESTORE(2);
|
||||
|
||||
companion object {
|
||||
fun deserialize(value: Int): NavTarget {
|
||||
return values().firstOrNull { it.value == value } ?: NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val EXTRA_NAV_TARGET = "nav_target"
|
||||
|
||||
@JvmStatic
|
||||
fun getIntentForRestore(context: Context): Intent {
|
||||
fun getIntentForTransfer(context: Context): Intent {
|
||||
return Intent(context, RestoreActivity::class.java).apply {
|
||||
putExtra(EXTRA_NAV_TARGET, NavTarget.TRANSFER.value)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getIntentForLocalRestore(context: Context): Intent {
|
||||
return Intent(context, RestoreActivity::class.java).apply {
|
||||
putExtra(EXTRA_NAV_TARGET, NavTarget.LOCAL_RESTORE.value)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getIntentForTransferOrRestore(context: Context): Intent {
|
||||
val tier = SignalStore.backup().backupTier
|
||||
if (tier == MessageBackupTier.PAID) {
|
||||
return Intent(context, RemoteRestoreActivity::class.java)
|
||||
}
|
||||
return Intent(context, RestoreActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.view.View
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity
|
||||
|
||||
/**
|
||||
@@ -29,7 +28,6 @@ class RestoreCompleteV2Fragment : LoggingFragment(R.layout.fragment_registration
|
||||
|
||||
private fun onBackupCompletedSuccessfully() {
|
||||
Log.d(TAG, "onBackupCompletedSuccessfully()")
|
||||
SignalStore.internalValues().setForceEnterRestoreV2Flow(false)
|
||||
val activity = requireActivity() as RestoreActivity
|
||||
activity.finishActivitySuccessfully()
|
||||
}
|
||||
|
||||
@@ -111,7 +111,6 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc
|
||||
|
||||
private fun onBackupCompletedSuccessfully() {
|
||||
Log.d(TAG, "onBackupCompletedSuccessfully()")
|
||||
SignalStore.internalValues().setForceEnterRestoreV2Flow(false)
|
||||
val activity = requireActivity() as RestoreActivity
|
||||
navigationViewModel.getNextIntent()?.let {
|
||||
Log.d(TAG, "Launching ${it.component}")
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.restore.transferorrestore
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.databinding.TransferOrRestoreOptionsBottomSheetDialogFragmentBinding
|
||||
import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class TransferOrRestoreMoreOptionsDialog : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
override val peekHeightPercentage: Float = 1f
|
||||
|
||||
private val viewModel by viewModels<TransferOrRestoreViewModel>()
|
||||
private lateinit var binding: TransferOrRestoreOptionsBottomSheetDialogFragmentBinding
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG = "TRANSFER_OR_RESTORE_OPTIONS_DIALOG_FRAGMENT"
|
||||
const val ARG_SKIP_ONLY = "skip_only"
|
||||
|
||||
fun show(fragmentManager: FragmentManager, skipOnly: Boolean) {
|
||||
TransferOrRestoreMoreOptionsDialog().apply {
|
||||
arguments = bundleOf(ARG_SKIP_ONLY to skipOnly)
|
||||
}.show(fragmentManager, TAG)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = TransferOrRestoreOptionsBottomSheetDialogFragmentBinding.inflate(inflater.cloneInContext(ContextThemeWrapper(inflater.context, themeResId)), container, false)
|
||||
if (arguments?.getBoolean(ARG_SKIP_ONLY, false) ?: false) {
|
||||
binding.transferCard.visible = false
|
||||
binding.localRestoreCard.visible = false
|
||||
}
|
||||
binding.transferOrRestoreFragmentNext.setOnClickListener { launchSelection(viewModel.getBackupRestorationType()) }
|
||||
binding.transferCard.setOnClickListener { viewModel.onTransferFromAndroidDeviceSelected() }
|
||||
binding.localRestoreCard.setOnClickListener { viewModel.onRestoreFromLocalBackupSelected() }
|
||||
binding.skipCard.setOnClickListener { viewModel.onSkipRestoreOrTransferSelected() }
|
||||
binding.cancel.setOnClickListener { dismiss() }
|
||||
|
||||
viewModel.uiState.observe(viewLifecycleOwner) { state ->
|
||||
updateSelection(state.restorationType)
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun launchSelection(restorationType: BackupRestorationType?) {
|
||||
when (restorationType) {
|
||||
BackupRestorationType.DEVICE_TRANSFER -> {
|
||||
startActivity(RestoreActivity.getIntentForTransfer(requireContext()))
|
||||
}
|
||||
BackupRestorationType.LOCAL_BACKUP -> {
|
||||
startActivity(RestoreActivity.getIntentForLocalRestore(requireContext()))
|
||||
}
|
||||
BackupRestorationType.REMOTE_BACKUP -> {
|
||||
startActivity(RemoteRestoreActivity.getIntent(requireContext()))
|
||||
}
|
||||
BackupRestorationType.NONE -> {
|
||||
SignalStore.registrationValues().markSkippedTransferOrRestore()
|
||||
val startIntent = MainActivity.clearTop(requireContext()).apply {
|
||||
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(requireContext()))
|
||||
}
|
||||
startActivity(startIntent)
|
||||
}
|
||||
else -> {
|
||||
return
|
||||
}
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun updateSelection(restorationType: BackupRestorationType?) {
|
||||
binding.transferCard.isSelected = restorationType == BackupRestorationType.DEVICE_TRANSFER
|
||||
binding.localRestoreCard.isSelected = restorationType == BackupRestorationType.LOCAL_BACKUP
|
||||
binding.skipCard.isSelected = restorationType == BackupRestorationType.NONE
|
||||
binding.transferOrRestoreFragmentNext.isEnabled = restorationType != null
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.restore.transferorrestore
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.signal.core.util.logging.Log
|
||||
@@ -16,7 +15,9 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentTransferRestoreV2Binding
|
||||
import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.restore.RemoteRestoreActivity
|
||||
import org.thoughtcrime.securesms.restore.RestoreViewModel
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
@@ -39,7 +40,11 @@ class TransferOrRestoreV2Fragment : LoggingFragment(R.layout.fragment_transfer_r
|
||||
binding.transferOrRestoreFragmentRestoreRemote.setOnClickListener { sharedViewModel.onRestoreFromRemoteBackupSelected() }
|
||||
binding.transferOrRestoreFragmentNext.setOnClickListener { launchSelection(sharedViewModel.getBackupRestorationType()) }
|
||||
binding.transferOrRestoreFragmentMoreOptions.setOnClickListener {
|
||||
Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
|
||||
TransferOrRestoreMoreOptionsDialog.show(fragmentManager = childFragmentManager, skipOnly = true)
|
||||
}
|
||||
|
||||
if (SignalStore.backup().backupTier == null) {
|
||||
binding.transferOrRestoreFragmentRestoreRemoteCard.visible = false
|
||||
}
|
||||
|
||||
binding.transferOrRestoreFragmentRestoreRemoteCard.visible = FeatureFlags.messageBackups()
|
||||
@@ -72,9 +77,7 @@ class TransferOrRestoreV2Fragment : LoggingFragment(R.layout.fragment_transfer_r
|
||||
NavHostFragment.findNavController(this).safeNavigate(TransferOrRestoreV2FragmentDirections.actionTransferOrRestoreToRestore())
|
||||
}
|
||||
BackupRestorationType.REMOTE_BACKUP -> {
|
||||
// TODO [regv2]
|
||||
Log.w(TAG, "Not yet implemented!", NotImplementedError())
|
||||
Toast.makeText(requireContext(), "Not yet implemented!", Toast.LENGTH_LONG).show()
|
||||
startActivity(RemoteRestoreActivity.getIntent(requireContext()))
|
||||
}
|
||||
else -> {
|
||||
throw IllegalArgumentException()
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.restore.transferorrestore
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
|
||||
|
||||
class TransferOrRestoreViewModel : ViewModel() {
|
||||
|
||||
private val store = MutableStateFlow(State())
|
||||
val uiState = store.asLiveData()
|
||||
|
||||
fun onSkipRestoreOrTransferSelected() {
|
||||
store.update {
|
||||
it.copy(restorationType = BackupRestorationType.NONE)
|
||||
}
|
||||
}
|
||||
|
||||
fun onTransferFromAndroidDeviceSelected() {
|
||||
store.update {
|
||||
it.copy(restorationType = BackupRestorationType.DEVICE_TRANSFER)
|
||||
}
|
||||
}
|
||||
|
||||
fun onRestoreFromLocalBackupSelected() {
|
||||
store.update {
|
||||
it.copy(restorationType = BackupRestorationType.LOCAL_BACKUP)
|
||||
}
|
||||
}
|
||||
|
||||
fun getBackupRestorationType(): BackupRestorationType? {
|
||||
return store.value.restorationType
|
||||
}
|
||||
}
|
||||
|
||||
data class State(val restorationType: BackupRestorationType? = null)
|
||||
@@ -733,7 +733,7 @@ public final class FeatureFlags {
|
||||
|
||||
/** Whether or not to launch the restore activity after registration is complete, rather than before. */
|
||||
public static boolean restoreAfterRegistration() {
|
||||
return getBoolean(RESTORE_POST_REGISTRATION, false);
|
||||
return BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED || getBoolean(RESTORE_POST_REGISTRATION, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user