Hook up message backup restore flow to reg v2.
Co-authored-by: Nicholas Tinsley <nicholas@signal.org>
This commit is contained in:
parent
26bd59c378
commit
66c50bef44
38 changed files with 1314 additions and 242 deletions
|
@ -1537,7 +1537,7 @@ class ImportExportTest {
|
|||
val frameReader = EncryptedBackupReader(
|
||||
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
|
||||
aci = selfData.aci,
|
||||
streamLength = import.size.toLong(),
|
||||
length = import.size.toLong(),
|
||||
dataStream = inputFactory
|
||||
)
|
||||
val frames = ArrayList<Frame>()
|
||||
|
|
|
@ -988,8 +988,8 @@
|
|||
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".backup.v2.ui.subscription.MessageBackupsTestRestoreActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
<activity android:name=".registration.v2.ui.restore.RemoteRestoreActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".profiles.manage.EditProfileActivity"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
19
app/src/main/res/drawable/ic_continue_no_restore_48.xml
Normal file
19
app/src/main/res/drawable/ic_continue_no_restore_48.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:pathData="M30.944,1.6H17.056C15.704,1.613 14.412,2.162 13.464,3.126C12.516,4.091 11.989,5.392 12,6.744V41.256C11.989,42.608 12.516,43.91 13.464,44.874C14.412,45.839 15.704,46.387 17.056,46.4H30.944C32.296,46.387 33.588,45.839 34.536,44.874C35.484,43.91 36.011,42.608 36,41.256V6.744C36.011,5.392 35.484,4.091 34.536,3.126C33.588,2.162 32.296,1.613 30.944,1.6ZM17.056,44.8C16.128,44.787 15.243,44.407 14.595,43.743C13.947,43.078 13.589,42.184 13.6,41.256V6.744C13.589,5.816 13.947,4.922 14.595,4.257C15.243,3.593 16.128,3.213 17.056,3.2H30.944C31.872,3.213 32.757,3.593 33.405,4.257C34.053,4.922 34.411,5.816 34.4,6.744V41.256C34.411,42.184 34.053,43.078 33.405,43.743C32.757,44.407 31.872,44.787 30.944,44.8H17.056Z"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#2C58C3"
|
||||
android:strokeColor="#2C58C3"/>
|
||||
<path
|
||||
android:pathData="M29.279,18.868C29.628,19.092 29.73,19.556 29.507,19.904L23.267,29.654C23.136,29.859 22.913,29.988 22.67,29.999C22.428,30.011 22.194,29.904 22.044,29.712L18.534,25.227C18.279,24.901 18.337,24.43 18.663,24.174C18.989,23.919 19.46,23.977 19.716,24.303L22.574,27.955L28.243,19.096C28.467,18.747 28.93,18.645 29.279,18.868Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#2C58C3"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2C58C3"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
14
app/src/main/res/drawable/ic_transfer_local_48.xml
Normal file
14
app/src/main/res/drawable/ic_transfer_local_48.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!--
|
||||
~ Copyright 2024 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:pathData="M12.935,36.608C10.02,36.608 8.556,35.171 8.556,32.285V15.285C8.556,12.482 9.965,11.088 12.405,11.088H16.379C17.76,11.088 18.471,11.339 19.392,12.12L20.242,12.817C20.94,13.417 21.511,13.64 22.459,13.64H35.847C38.748,13.64 40.226,15.09 40.226,17.963V32.285C40.226,35.158 38.762,36.608 36.265,36.608H12.935ZM10.801,15.411V18.883H37.981V18.088C37.981,16.638 37.186,15.885 35.805,15.885H21.874C20.493,15.885 19.754,15.62 18.848,14.867L17.997,14.156C17.286,13.556 16.728,13.319 15.808,13.319H12.865C11.526,13.319 10.801,14.03 10.801,15.411ZM12.963,34.363H35.805C37.186,34.363 37.981,33.624 37.981,32.187V20.989H10.801V32.173C10.801,33.624 11.568,34.363 12.963,34.363Z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
244
app/src/main/res/layout/activity_remote_restore.xml
Normal file
244
app/src/main/res/layout/activity_remote_restore.xml
Normal file
|
@ -0,0 +1,244 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingTop="@dimen/transfer_top_padding"
|
||||
android:paddingEnd="32dp"
|
||||
android:paddingBottom="24dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/TransferOrRestoreFragment__transfer_or_restore_account"
|
||||
android:textAppearance="@style/Signal.Text.HeadlineMedium" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/transfer_item_spacing"
|
||||
android:gravity="center"
|
||||
android:text="@string/TransferOrRestoreFragment__if_you_have_previously_registered_a_signal_account"
|
||||
android:textAppearance="@style/Signal.Text.BodyLarge"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ClippedCardView
|
||||
android:id="@+id/transfer_or_restore_fragment_transfer_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/signal_colorSurface2"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="@color/transfer_option_stroke_color_selector"
|
||||
app:strokeWidth="2dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/transfer_or_restore_fragment_transfer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="16dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/transfer_or_restore_fragment_transfer_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_transfer_phone_48"
|
||||
app:tint="@color/signal_colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_fragment_transfer_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/TransferOrRestoreFragment__transfer_from_android_device"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toTopOf="@+id/transfer_or_restore_fragment_transfer_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@+id/transfer_or_restore_fragment_transfer_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_fragment_transfer_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="@+id/transfer_or_restore_fragment_transfer_header"
|
||||
app:layout_constraintTop_toBottomOf="@+id/transfer_or_restore_fragment_transfer_header"
|
||||
tools:text="@string/TransferOrRestoreFragment__transfer_your_account_and_messages_from_your_old_android_device" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</org.thoughtcrime.securesms.components.ClippedCardView>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ClippedCardView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_remote_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:visibility="gone"
|
||||
app:cardBackgroundColor="@color/signal_colorSurface2"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="@color/transfer_option_stroke_color_selector"
|
||||
app:strokeWidth="2dp"
|
||||
tools:visibility="visible">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_remote"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="16dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_remote_icon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="18dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/symbol_backup_light"
|
||||
app:tint="@color/signal_colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_remote_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/TransferOrRestoreFragment__restore_from_backup"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toTopOf="@+id/transfer_or_restore_fragment_restore_remote_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@+id/transfer_or_restore_fragment_restore_remote_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_remote_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/TransferOrRestoreFragment__restore_your_messages_from_a_local_backup"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="@+id/transfer_or_restore_fragment_restore_remote_header"
|
||||
app:layout_constraintTop_toBottomOf="@+id/transfer_or_restore_fragment_restore_remote_header" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</org.thoughtcrime.securesms.components.ClippedCardView>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ClippedCardView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/signal_colorSurface2"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="@color/transfer_option_stroke_color_selector"
|
||||
app:strokeWidth="2dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/transfer_or_restore_fragment_restore"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="16dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_icon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="18dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/symbol_backup_light"
|
||||
app:tint="@color/signal_colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/TransferOrRestoreFragment__restore_from_backup"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toTopOf="@+id/transfer_or_restore_fragment_restore_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@+id/transfer_or_restore_fragment_restore_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/TransferOrRestoreFragment__restore_your_messages_from_a_local_backup"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="@+id/transfer_or_restore_fragment_restore_header"
|
||||
app:layout_constraintTop_toBottomOf="@+id/transfer_or_restore_fragment_restore_header" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</org.thoughtcrime.securesms.components.ClippedCardView>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/transfer_or_restore_fragment_more_options"
|
||||
style="@style/Widget.Signal.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/TransferOrRestoreFragment__more_options"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/transfer_or_restore_fragment_next"
|
||||
style="@style/Signal.Widget.Button.Large.Tonal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/RegistrationActivity_next" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
|
@ -129,7 +129,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/TransferOrRestoreFragment__restore_from_backup"
|
||||
android:text="@string/TransferOrRestoreFragment__restore_from_signal_backup"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toTopOf="@+id/transfer_or_restore_fragment_restore_remote_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -143,7 +143,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/TransferOrRestoreFragment__restore_your_messages_from_a_local_backup"
|
||||
android:text="@string/TransferOrRestoreFragment__restore_your_messages_from_a_signal_backup"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -173,22 +173,22 @@
|
|||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_icon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/symbol_backup_light"
|
||||
app:srcCompat="@drawable/ic_transfer_local_48"
|
||||
app:tint="@color/signal_colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/TransferOrRestoreFragment__restore_from_backup"
|
||||
android:text="@string/TransferOrRestoreFragment__restore_from_local_backup"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toTopOf="@+id/transfer_or_restore_fragment_restore_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2024 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/signal_colorSurface1"
|
||||
>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pull_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="48dp"
|
||||
android:src="@drawable/bottom_sheet_handle"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ClippedCardView
|
||||
android:id="@+id/transfer_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/signal_colorSurface"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="@color/transfer_option_stroke_color_selector"
|
||||
app:strokeWidth="2dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/transfer_or_restore_fragment_transfer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="16dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/transfer_or_restore_fragment_transfer_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_transfer_phone_48"
|
||||
app:tint="@color/signal_colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_fragment_transfer_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/TransferOrRestoreFragment__transfer_from_android_device"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toTopOf="@+id/transfer_or_restore_fragment_transfer_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@+id/transfer_or_restore_fragment_transfer_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_fragment_transfer_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="@+id/transfer_or_restore_fragment_transfer_header"
|
||||
app:layout_constraintTop_toBottomOf="@+id/transfer_or_restore_fragment_transfer_header"
|
||||
android:text="@string/TransferOrRestoreFragment__transfer_your_account_and_messages_from_your_old_android_device" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</org.thoughtcrime.securesms.components.ClippedCardView>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ClippedCardView
|
||||
android:id="@+id/local_restore_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/signal_colorSurface"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="@color/transfer_option_stroke_color_selector"
|
||||
app:strokeWidth="2dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="16dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_transfer_local_48"
|
||||
app:tint="@color/signal_colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/TransferOrRestoreFragment__restore_from_local_backup"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toTopOf="@+id/transfer_or_restore_fragment_restore_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@+id/transfer_or_restore_fragment_restore_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/TransferOrRestoreFragment__restore_your_messages_from_a_local_backup"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="@+id/transfer_or_restore_fragment_restore_header"
|
||||
app:layout_constraintTop_toBottomOf="@+id/transfer_or_restore_fragment_restore_header" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</org.thoughtcrime.securesms.components.ClippedCardView>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ClippedCardView
|
||||
android:id="@+id/skip_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/signal_colorSurface"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="@color/transfer_option_stroke_color_selector"
|
||||
app:strokeWidth="2dp"
|
||||
tools:visibility="visible">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="16dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_remote_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_continue_no_restore_48"
|
||||
app:tint="@color/signal_colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_remote_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/TransferOrRestoreFragment__skip_transfer"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toTopOf="@+id/transfer_or_restore_fragment_restore_remote_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@+id/transfer_or_restore_fragment_restore_remote_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/transfer_or_restore_fragment_restore_remote_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/TransferOrRestoreFragment__skip_transfer_description"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="@+id/transfer_or_restore_fragment_restore_remote_header"
|
||||
app:layout_constraintTop_toBottomOf="@+id/transfer_or_restore_fragment_restore_remote_header" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</org.thoughtcrime.securesms.components.ClippedCardView>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel"
|
||||
style="@style/Widget.Signal.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:text="@string/TransferOrRestoreFragment__cancel"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/transfer_or_restore_fragment_next"
|
||||
style="@style/Signal.Widget.Button.Large.Tonal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/RegistrationActivity_next" />
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
|
@ -4143,9 +4143,16 @@
|
|||
<string name="TransferOrRestoreFragment__you_need_access_to_your_old_device">You need access to your old device.</string>
|
||||
<string name="TransferOrRestoreFragment__restore_from_backup">Restore from backup</string>
|
||||
<string name="TransferOrRestoreFragment__restore_your_messages_from_a_local_backup">Restore your messages from a local backup. If you don’t restore now, you won\'t be able to restore later.</string>
|
||||
<string name="TransferOrRestoreFragment__restore_from_local_backup">Restore local backup</string>
|
||||
<string name="TransferOrRestoreFragment__restore_from_signal_backup">Restore Signal backup</string>
|
||||
<string name="TransferOrRestoreFragment__restore_your_messages_from_a_signal_backup">Restore all your text messages + your last 30 days of media</string>
|
||||
<!-- Button label for more options -->
|
||||
<string name="TransferOrRestoreFragment__more_options">More options</string>
|
||||
|
||||
<string name="TransferOrRestoreFragment__cancel">Cancel</string>
|
||||
<string name="TransferOrRestoreFragment__skip_transfer">Log in without transferring</string>
|
||||
<string name="TransferOrRestoreFragment__skip_transfer_description">Continue without transferring your messages and media</string>
|
||||
|
||||
<!-- NewDeviceTransferInstructionsFragment -->
|
||||
<string name="NewDeviceTransferInstructions__open_signal_on_your_old_android_phone">Open Signal on your old Android phone</string>
|
||||
<string name="NewDeviceTransferInstructions__continue">Continue</string>
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
|||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
|
@ -224,6 +225,10 @@ public class SignalServiceMessageReceiver {
|
|||
socket.retrieveBackup(cdnNumber, headers, cdnPath, destination, 1_000_000_000L, listener);
|
||||
}
|
||||
|
||||
public boolean checkBackupExistence(int cdnNumber, Map<String, String> headers, String cdnPath) throws MissingConfigurationException, IOException {
|
||||
return socket.checkForBackup(cdnNumber, headers, cdnPath);
|
||||
}
|
||||
|
||||
public InputStream retrieveSticker(byte[] packId, byte[] packKey, int stickerId)
|
||||
throws IOException, InvalidMessageException
|
||||
{
|
||||
|
|
|
@ -257,7 +257,7 @@ class ArchiveApi(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getZkCredential(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): BackupAuthCredential {
|
||||
fun getZkCredential(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): BackupAuthCredential {
|
||||
val backupAuthResponse = BackupAuthCredentialResponse(serviceCredential.credential)
|
||||
val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid)
|
||||
|
||||
|
|
|
@ -948,6 +948,10 @@ public class PushServiceSocket {
|
|||
downloadFromCdn(destination, cdnNumber, headers, cdnPath, maxSizeBytes, listener);
|
||||
}
|
||||
|
||||
public boolean checkForBackup(int cdnNumber, Map<String, String> headers, String cdnPath) throws PushNetworkException, MissingConfigurationException, NonSuccessfulResponseCodeException {
|
||||
return checkExistsOnCdn(cdnNumber, headers, cdnPath);
|
||||
}
|
||||
|
||||
public void retrieveAttachment(int cdnNumber, Map<String, String> headers, SignalServiceAttachmentRemoteId cdnPath, File destination, long maxSizeBytes, ProgressListener listener)
|
||||
throws IOException, MissingConfigurationException
|
||||
{
|
||||
|
@ -1709,6 +1713,51 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean checkExistsOnCdn(int cdnNumber, Map<String, String> headers, String path) throws MissingConfigurationException, PushNetworkException, NonSuccessfulResponseCodeException {
|
||||
ConnectionHolder[] cdnNumberClients = cdnClientsMap.get(cdnNumber);
|
||||
if (cdnNumberClients == null) {
|
||||
throw new MissingConfigurationException("Attempted to download from unsupported CDN number: " + cdnNumber + ", Our configuration supports: " + cdnClientsMap.keySet());
|
||||
}
|
||||
ConnectionHolder connectionHolder = getRandom(cdnNumberClients, random);
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
.newBuilder()
|
||||
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + "/" + path).get();
|
||||
|
||||
if (connectionHolder.getHostHeader().isPresent()) {
|
||||
request.addHeader("Host", connectionHolder.getHostHeader().get());
|
||||
}
|
||||
|
||||
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||
request.addHeader(header.getKey(), header.getValue());
|
||||
}
|
||||
|
||||
Call call = okHttpClient.newCall(request.build());
|
||||
|
||||
synchronized (connections) {
|
||||
connections.add(call);
|
||||
}
|
||||
|
||||
try (Response response = call.execute()) {
|
||||
if (response.isSuccessful()) {
|
||||
return true;
|
||||
} else {
|
||||
throw new NonSuccessfulResponseCodeException(response.code(), "Response: " + response);
|
||||
}
|
||||
} catch (NonSuccessfulResponseCodeException | PushNetworkException e) {
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
throw new PushNetworkException(e);
|
||||
} finally {
|
||||
synchronized (connections) {
|
||||
connections.remove(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AttachmentDigest uploadToCdn0(String path, String acl, String key, String policy, String algorithm,
|
||||
String credential, String date, String signature,
|
||||
InputStream data, String contentType, long length, boolean incremental,
|
||||
|
|
Loading…
Add table
Reference in a new issue