Hook up message backup restore flow to reg v2.

Co-authored-by: Nicholas Tinsley <nicholas@signal.org>
This commit is contained in:
Clark 2024-06-05 16:54:09 -04:00 committed by Alex Hart
parent 26bd59c378
commit 66c50bef44
38 changed files with 1314 additions and 242 deletions

View file

@ -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>()

View file

@ -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"

View file

@ -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());
}

View file

@ -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
}
}
}

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
)
}
}

View file

@ -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) {

View file

@ -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()
}

View file

@ -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

View file

@ -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

View file

@ -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);
}

View file

@ -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);

View file

@ -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);

View file

@ -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 ->

View file

@ -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))
}
}

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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
)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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}")

View file

@ -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
}
}

View file

@ -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()

View file

@ -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)

View file

@ -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);
}
/**

View 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>

View 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>

View 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>

View file

@ -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"

View file

@ -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>

View file

@ -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 dont 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>

View file

@ -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
{

View file

@ -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)

View file

@ -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,