mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Restore a Local Backup v2
This commit is contained in:
committed by
Greyson Parrelli
parent
947ab7d48b
commit
62cf3feeaa
@@ -844,6 +844,13 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".restore.RestoreActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".revealable.ViewOnceMessageActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNum
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
|
||||
import org.thoughtcrime.securesms.keyvalue.InternalValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
|
||||
@@ -29,6 +30,7 @@ import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2Activity;
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
@@ -53,6 +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 SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
@@ -127,8 +130,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
}
|
||||
|
||||
private void routeApplicationState(boolean locked) {
|
||||
Intent intent = getIntentForState(getApplicationState(locked));
|
||||
final int applicationState = getApplicationState(locked);
|
||||
Intent intent = getIntentForState(applicationState);
|
||||
if (intent != null) {
|
||||
Log.d(TAG, "routeApplicationState(), intent: " + intent.getComponent());
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
@@ -148,6 +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();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -161,6 +167,8 @@ 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()) {
|
||||
@@ -233,6 +241,11 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return getRoutedIntent(CreateSvrPinActivity.class, intent);
|
||||
}
|
||||
|
||||
private Intent getRestoreIntent() {
|
||||
Intent intent = RestoreActivity.getIntentForRestore(this);
|
||||
return getRoutedIntent(intent, getIntent());
|
||||
}
|
||||
|
||||
private Intent getCreateProfileNameIntent() {
|
||||
Intent intent = CreateProfileActivity.getIntentForUserProfile(this);
|
||||
return getRoutedIntent(intent, getIntent());
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -270,6 +271,13 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (FeatureFlags.registrationV2()) {
|
||||
if (SignalStore.account().getKeysToIncludeInBackup().contains(keyValue.key)) {
|
||||
Log.i(TAG, "[regv2] skipping restore of " + keyValue.key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (keyValue.blobValue != null) {
|
||||
dataSet.putBlob(keyValue.key, keyValue.blobValue.toByteArray());
|
||||
} else if (keyValue.booleanValue != null) {
|
||||
|
||||
@@ -31,6 +31,7 @@ public final class InternalValues extends SignalStoreValues {
|
||||
public static final String FORCE_WEBSOCKET_MODE = "internal.force_websocket_mode";
|
||||
public static final String LAST_SCROLL_POSITION = "internal.last_scroll_position";
|
||||
public static final String CONVERSATION_ITEM_V2_MEDIA = "internal.conversation_item_v2_media";
|
||||
public static final String FORCE_ENTER_RESTORE_V2_FLOW = "internal.force_enter_restore_v2_flow";
|
||||
|
||||
InternalValues(KeyValueStore store) {
|
||||
super(store);
|
||||
@@ -210,4 +211,12 @@ public final class InternalValues extends SignalStoreValues {
|
||||
public boolean useConversationItemV2Media() {
|
||||
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.registrationV2() && getBoolean(FORCE_ENTER_RESTORE_V2_FLOW, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.pin.PinOptOutDialog;
|
||||
@@ -151,8 +152,11 @@ public abstract class BaseSvrPinFragment<ViewModel extends BaseSvrPinViewModel>
|
||||
|
||||
protected void closeNavGraphBranch() {
|
||||
Intent activityIntent = requireActivity().getIntent();
|
||||
if (activityIntent != null && activityIntent.hasExtra("next_intent")) {
|
||||
startActivity(activityIntent.getParcelableExtra("next_intent"));
|
||||
if (activityIntent != null && activityIntent.hasExtra(PassphraseRequiredActivity.NEXT_INTENT_EXTRA)) {
|
||||
final Intent nextIntent = activityIntent.getParcelableExtra(PassphraseRequiredActivity.NEXT_INTENT_EXTRA);
|
||||
if (nextIntent != null) {
|
||||
startActivity(nextIntent);
|
||||
}
|
||||
}
|
||||
|
||||
requireActivity().finish();
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.jobs.RotateCertificateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository.onRegistrationComplete
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -188,7 +188,7 @@ object RegistrationRepository {
|
||||
TextSecurePreferences.setUnauthorizedReceived(context, false)
|
||||
NotificationManagerCompat.from(context).cancel(NotificationIds.UNREGISTERED_NOTIFICATION_ID)
|
||||
|
||||
onRegistrationComplete(response.masterKey, response.pin, hasPin, reglockEnabled)
|
||||
SvrRepository.onRegistrationComplete(response.masterKey, response.pin, hasPin, reglockEnabled)
|
||||
|
||||
ApplicationDependencies.closeConnections()
|
||||
ApplicationDependencies.getIncomingMessageObserver()
|
||||
@@ -335,7 +335,11 @@ object RegistrationRepository {
|
||||
val eventBus = EventBus.getDefault()
|
||||
eventBus.register(subscriber)
|
||||
|
||||
val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc).successOrThrow() // TODO: error handling
|
||||
val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc)
|
||||
if (sessionCreationResponse !is NetworkResult.Success) {
|
||||
return@withContext sessionCreationResponse
|
||||
}
|
||||
|
||||
val receivedPush = subscriber.latch.await(PUSH_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
eventBus.unregister(subscriber)
|
||||
|
||||
@@ -343,7 +347,7 @@ object RegistrationRepository {
|
||||
val challenge = subscriber.challenge
|
||||
if (challenge != null) {
|
||||
Log.w(TAG, "Push challenge token received.")
|
||||
return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.body.id, challenge)
|
||||
return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.result.body.id, challenge)
|
||||
} else {
|
||||
Log.w(TAG, "Push received but challenge token was null.")
|
||||
}
|
||||
|
||||
@@ -9,15 +9,15 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2ViewModel
|
||||
|
||||
/**
|
||||
* Activity to hold the entire registration process.
|
||||
*/
|
||||
class RegistrationV2Activity : AppCompatActivity() {
|
||||
class RegistrationV2Activity : BaseActivity() {
|
||||
|
||||
private val TAG = Log.tag(RegistrationV2Activity::class.java)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
@@ -91,9 +92,12 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
|
||||
|
||||
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()
|
||||
}
|
||||
sharedViewModel.setInProgress(false)
|
||||
|
||||
val startIntent = MainActivity.clearTop(activity).apply {
|
||||
if (needsPin) {
|
||||
@@ -105,8 +109,8 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "Launching ${startIntent.component}")
|
||||
activity.startActivity(startIntent)
|
||||
sharedViewModel.setInProgress(false)
|
||||
activity.finish()
|
||||
ActivityNavigator.applyPopAnimationsToPendingTransition(activity)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.restore
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import org.signal.core.util.getParcelableExtraCompat
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Activity to hold the restore from backup flow.
|
||||
*/
|
||||
class RestoreActivity : BaseActivity() {
|
||||
|
||||
private val sharedViewModel: RestoreViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_restore)
|
||||
intent.getParcelableExtraCompat(PassphraseRequiredActivity.NEXT_INTENT_EXTRA, Intent::class.java)?.let {
|
||||
sharedViewModel.setNextIntent(it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getIntentForRestore(context: Context): Intent {
|
||||
return Intent(context, RestoreActivity::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.restore
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.AppInitialization
|
||||
import org.thoughtcrime.securesms.backup.BackupPassphrase
|
||||
import org.thoughtcrime.securesms.backup.FullBackupImporter
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Repository to handle restoring a backup of a user's message history.
|
||||
*/
|
||||
object RestoreRepository {
|
||||
private val TAG = Log.tag(RestoreRepository.javaClass)
|
||||
|
||||
suspend fun getLocalBackupFromUri(context: Context, uri: Uri): BackupUtil.BackupInfo? = withContext(Dispatchers.IO) {
|
||||
BackupUtil.getBackupInfoFromSingleUri(context, uri)
|
||||
}
|
||||
|
||||
suspend fun restoreBackupAsynchronously(context: Context, backupFileUri: Uri, passphrase: String): BackupImportResult = withContext(Dispatchers.IO) {
|
||||
// TODO [regv2]: migrate this to a service
|
||||
try {
|
||||
Log.i(TAG, "Starting backup restore.")
|
||||
DataRestoreConstraint.isRestoringData = true
|
||||
|
||||
val database = SignalDatabase.backupDatabase
|
||||
|
||||
BackupPassphrase.set(context, passphrase)
|
||||
|
||||
if (!FullBackupImporter.validatePassphrase(context, backupFileUri, passphrase)) {
|
||||
// TODO [regv2]: implement a specific, user-visible error for wrong passphrase.
|
||||
return@withContext BackupImportResult.FAILURE_UNKNOWN
|
||||
}
|
||||
|
||||
FullBackupImporter.importFile(
|
||||
context,
|
||||
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
||||
database,
|
||||
backupFileUri,
|
||||
passphrase
|
||||
)
|
||||
|
||||
SignalDatabase.runPostBackupRestoreTasks(database)
|
||||
NotificationChannels.getInstance().restoreContactNotificationChannels()
|
||||
|
||||
if (BackupUtil.canUserAccessBackupDirectory(context)) {
|
||||
LocalBackupListener.setNextBackupTimeToIntervalFromNow(context)
|
||||
SignalStore.settings().isBackupEnabled = true
|
||||
LocalBackupListener.schedule(context)
|
||||
}
|
||||
|
||||
AppInitialization.onPostBackupRestore(context)
|
||||
|
||||
Log.i(TAG, "Backup restore complete.")
|
||||
return@withContext BackupImportResult.SUCCESS
|
||||
} catch (e: FullBackupImporter.DatabaseDowngradeException) {
|
||||
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e)
|
||||
return@withContext BackupImportResult.FAILURE_VERSION_DOWNGRADE
|
||||
} catch (e: FullBackupImporter.ForeignKeyViolationException) {
|
||||
Log.w(TAG, "Failed due to foreign key constraint violations.", e)
|
||||
return@withContext BackupImportResult.FAILURE_FOREIGN_KEY
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
return@withContext BackupImportResult.FAILURE_UNKNOWN
|
||||
} finally {
|
||||
DataRestoreConstraint.isRestoringData = false
|
||||
}
|
||||
}
|
||||
|
||||
enum class BackupImportResult {
|
||||
SUCCESS,
|
||||
FAILURE_VERSION_DOWNGRADE,
|
||||
FAILURE_FOREIGN_KEY,
|
||||
FAILURE_UNKNOWN
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.restore
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
|
||||
|
||||
/**
|
||||
* Shared state holder for the restore flow.
|
||||
*/
|
||||
data class RestoreState(val restorationType: BackupRestorationType = BackupRestorationType.LOCAL_BACKUP, val backupFile: Uri? = null, val nextIntent: Intent? = null)
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.restore
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
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
|
||||
|
||||
/**
|
||||
* Shared view model for the restore flow.
|
||||
*/
|
||||
class RestoreViewModel : ViewModel() {
|
||||
private val store = MutableStateFlow(RestoreState())
|
||||
val uiState = store.asLiveData()
|
||||
|
||||
fun setNextIntent(nextIntent: Intent) {
|
||||
store.update {
|
||||
it.copy(nextIntent = nextIntent)
|
||||
}
|
||||
}
|
||||
|
||||
fun onTransferFromAndroidDeviceSelected() {
|
||||
store.update {
|
||||
it.copy(restorationType = BackupRestorationType.DEVICE_TRANSFER)
|
||||
}
|
||||
}
|
||||
|
||||
fun onRestoreFromLocalBackupSelected() {
|
||||
store.update {
|
||||
it.copy(restorationType = BackupRestorationType.LOCAL_BACKUP)
|
||||
}
|
||||
}
|
||||
|
||||
fun onRestoreFromRemoteBackupSelected() {
|
||||
store.update {
|
||||
it.copy(restorationType = BackupRestorationType.REMOTE_BACKUP)
|
||||
}
|
||||
}
|
||||
|
||||
fun getBackupRestorationType(): BackupRestorationType {
|
||||
return store.value.restorationType
|
||||
}
|
||||
|
||||
fun setBackupFileUri(backupFileUri: Uri) {
|
||||
store.update {
|
||||
it.copy(backupFile = backupFileUri)
|
||||
}
|
||||
}
|
||||
|
||||
fun getBackupFileUri(): Uri? = store.value.backupFile
|
||||
|
||||
fun getNextIntent(): Intent? = store.value.nextIntent
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.restore.choosebackup
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.DocumentsContract
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.View
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentChooseBackupV2Binding
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
|
||||
import org.thoughtcrime.securesms.restore.RestoreViewModel
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* This fragment presents a button to the user to browse their local file system for a legacy backup file.
|
||||
*/
|
||||
class ChooseBackupV2Fragment : LoggingFragment(R.layout.fragment_choose_backup_v2) {
|
||||
private val sharedViewModel by activityViewModels<RestoreViewModel>()
|
||||
private val binding: FragmentChooseBackupV2Binding by ViewBinderDelegate(FragmentChooseBackupV2Binding::bind)
|
||||
|
||||
private val pickMedia = registerForActivityResult(BackupFileContract()) {
|
||||
if (it != null) {
|
||||
onUserChoseBackupFile(it)
|
||||
} else {
|
||||
Log.i(TAG, "Null URI returned for backup file selection.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.chooseBackupFragmentTitle)
|
||||
binding.chooseBackupFragmentButton.setOnClickListener { onChooseBackupSelected() }
|
||||
|
||||
binding.chooseBackupFragmentLearnMore.text = HtmlCompat.fromHtml(String.format("<a href=\"%s\">%s</a>", getString(R.string.backup_support_url), getString(R.string.ChooseBackupFragment__learn_more)), 0)
|
||||
binding.chooseBackupFragmentLearnMore.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
private fun onChooseBackupSelected() {
|
||||
pickMedia.launch("application/octet-stream")
|
||||
}
|
||||
|
||||
private fun onUserChoseBackupFile(backupFileUri: Uri) {
|
||||
sharedViewModel.setBackupFileUri(backupFileUri)
|
||||
NavHostFragment.findNavController(this).safeNavigate(ChooseBackupV2FragmentDirections.actionChooseLocalBackupFragmentToRestoreLocalBackupFragment())
|
||||
}
|
||||
|
||||
private class BackupFileContract : ActivityResultContracts.GetContent() {
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
return super.createIntent(context, input).apply {
|
||||
putExtra(Intent.EXTRA_LOCAL_ONLY, true)
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
putExtra(DocumentsContract.EXTRA_INITIAL_URI, SignalStore.settings().latestSignalBackupDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChooseBackupV2Fragment::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.restore.restorelocalbackup
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.BackupEvent
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRestoreLocalBackupV2Binding
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment.PassphraseAsYouTypeFormatter
|
||||
import org.thoughtcrime.securesms.restore.RestoreRepository
|
||||
import org.thoughtcrime.securesms.restore.RestoreViewModel
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.ViewModelFactory
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* This fragment is used to monitor and manage an in-progress backup restore.
|
||||
*/
|
||||
class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_local_backup_v2) {
|
||||
private val navigationViewModel: RestoreViewModel by activityViewModels()
|
||||
private val restoreLocalBackupViewModel: RestoreLocalBackupViewModel by viewModels(
|
||||
factoryProducer = ViewModelFactory.factoryProducer {
|
||||
val fileBackupUri = navigationViewModel.getBackupFileUri()!!
|
||||
RestoreLocalBackupViewModel(fileBackupUri)
|
||||
}
|
||||
)
|
||||
private val binding: FragmentRestoreLocalBackupV2Binding by ViewBinderDelegate(FragmentRestoreLocalBackupV2Binding::bind)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setDebugLogSubmitMultiTapView(binding.verifyHeader)
|
||||
Log.i(TAG, "Backup restore.")
|
||||
|
||||
restoreLocalBackupViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
|
||||
fragmentState.backupInfo?.let {
|
||||
presentBackupFileInfo(backupSize = it.size, backupTimestamp = it.timestamp)
|
||||
if (fragmentState.backupPassphrase.isEmpty()) {
|
||||
presentBackupPassPhrasePromptDialog()
|
||||
}
|
||||
}
|
||||
|
||||
if (fragmentState.restoreInProgress) {
|
||||
presentRestoreProgress(fragmentState.backupProgressCount)
|
||||
} else {
|
||||
presentProgressEnded()
|
||||
}
|
||||
|
||||
if (fragmentState.backupRestoreComplete) {
|
||||
val importResult = fragmentState.backupImportResult
|
||||
if (importResult == null) {
|
||||
onBackupCompletedSuccessfully()
|
||||
} else {
|
||||
handleBackupImportResult(importResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
restoreLocalBackupViewModel.startRestore(requireContext())
|
||||
}
|
||||
|
||||
private fun onBackupCompletedSuccessfully() {
|
||||
Log.d(TAG, "onBackupCompletedSuccessfully()")
|
||||
SignalStore.internalValues().setForceEnterRestoreV2Flow(false)
|
||||
val activity = requireActivity()
|
||||
navigationViewModel.getNextIntent()?.let {
|
||||
Log.d(TAG, "Launching ${it.component}")
|
||||
activity.startActivity(it)
|
||||
}
|
||||
activity.finish()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onEvent(event: BackupEvent) {
|
||||
restoreLocalBackupViewModel.onBackupProgressUpdate(event)
|
||||
}
|
||||
|
||||
private fun handleBackupImportResult(importResult: RestoreRepository.BackupImportResult) {
|
||||
when (importResult) {
|
||||
RestoreRepository.BackupImportResult.FAILURE_VERSION_DOWNGRADE -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show()
|
||||
RestoreRepository.BackupImportResult.FAILURE_FOREIGN_KEY -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_foreign_key, Toast.LENGTH_LONG).show()
|
||||
RestoreRepository.BackupImportResult.FAILURE_UNKNOWN -> Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show()
|
||||
RestoreRepository.BackupImportResult.SUCCESS -> Log.w(TAG, "Successful backup import should not be handled here.", IllegalStateException())
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentProgressEnded() {
|
||||
binding.restoreButton.cancelSpinning()
|
||||
binding.cancelLocalRestoreButton.visible = true
|
||||
binding.backupProgressText.text = ""
|
||||
}
|
||||
|
||||
private fun presentRestoreProgress(backupProgressCount: Long) {
|
||||
binding.restoreButton.setSpinning()
|
||||
binding.cancelLocalRestoreButton.visibility = View.INVISIBLE
|
||||
if (backupProgressCount > 0L) {
|
||||
binding.backupProgressText.text = getString(R.string.RegistrationActivity_d_messages_so_far, backupProgressCount)
|
||||
} else {
|
||||
binding.backupProgressText.setText(R.string.RegistrationActivity_checking)
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentBackupPassPhrasePromptDialog() {
|
||||
val view = LayoutInflater.from(requireContext()).inflate(R.layout.enter_backup_passphrase_dialog, null)
|
||||
val prompt = view.findViewById<EditText>(R.id.restore_passphrase_input)
|
||||
|
||||
prompt.addTextChangedListener(PassphraseAsYouTypeFormatter())
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RegistrationActivity_enter_backup_passphrase)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.RegistrationActivity_restore) { _, _ ->
|
||||
ViewUtil.hideKeyboard(requireContext(), prompt)
|
||||
|
||||
val passphrase = prompt.getText().toString()
|
||||
restoreLocalBackupViewModel.confirmPassphrase(requireContext(), passphrase)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
|
||||
Log.i(TAG, "Prompt for backup passphrase shown to user.")
|
||||
}
|
||||
|
||||
private fun presentBackupFileInfo(backupSize: Long, backupTimestamp: Long) {
|
||||
if (backupSize > 0) {
|
||||
binding.backupSizeText.text = getString(R.string.RegistrationActivity_backup_size_s, Util.getPrettyFileSize(backupSize))
|
||||
}
|
||||
|
||||
if (backupTimestamp > 0) {
|
||||
binding.backupCreatedText.text = getString(R.string.RegistrationActivity_backup_timestamp_s, DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), backupTimestamp))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RestoreLocalBackupFragment::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.restore.restorelocalbackup
|
||||
|
||||
import android.net.Uri
|
||||
import org.thoughtcrime.securesms.restore.RestoreRepository
|
||||
import org.thoughtcrime.securesms.util.BackupUtil.BackupInfo
|
||||
|
||||
/**
|
||||
* State holder for a backup restore.
|
||||
*/
|
||||
data class RestoreLocalBackupState(
|
||||
val uri: Uri,
|
||||
val backupInfo: BackupInfo? = null,
|
||||
val backupPassphrase: String = "",
|
||||
val restoreInProgress: Boolean = false,
|
||||
val backupVerifyingInProgress: Boolean = false,
|
||||
val backupProgressCount: Long = -1,
|
||||
val backupEstimatedTotalCount: Long = -1,
|
||||
val backupRestoreComplete: Boolean = false,
|
||||
val backupImportResult: RestoreRepository.BackupImportResult? = null,
|
||||
val abort: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.restore.restorelocalbackup
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.BackupEvent
|
||||
import org.thoughtcrime.securesms.restore.RestoreRepository
|
||||
|
||||
/**
|
||||
* ViewModel for [RestoreLocalBackupFragment]
|
||||
*/
|
||||
class RestoreLocalBackupViewModel(fileBackupUri: Uri) : ViewModel() {
|
||||
private val store = MutableStateFlow(RestoreLocalBackupState(fileBackupUri))
|
||||
val uiState = store.asLiveData()
|
||||
|
||||
fun startRestore(context: Context) {
|
||||
val backupFileUri = store.value.uri
|
||||
viewModelScope.launch {
|
||||
val backupInfo = RestoreRepository.getLocalBackupFromUri(context, backupFileUri)
|
||||
|
||||
if (backupInfo == null) {
|
||||
abort()
|
||||
return@launch
|
||||
}
|
||||
|
||||
store.update {
|
||||
it.copy(
|
||||
backupInfo = backupInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun abort() {
|
||||
store.update {
|
||||
it.copy(abort = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun confirmPassphrase(context: Context, passphrase: String) {
|
||||
store.update {
|
||||
it.copy(
|
||||
backupPassphrase = passphrase,
|
||||
restoreInProgress = true
|
||||
)
|
||||
}
|
||||
|
||||
val backupFileUri = store.value.backupInfo?.uri
|
||||
val backupPassphrase = store.value.backupPassphrase
|
||||
if (backupFileUri == null) {
|
||||
Log.w(TAG, "Could not begin backup import because backup file URI was null!")
|
||||
abort()
|
||||
return
|
||||
}
|
||||
|
||||
if (backupPassphrase.isEmpty()) {
|
||||
Log.w(TAG, "Could not begin backup import because backup passphrase was empty!")
|
||||
abort()
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val importResult = RestoreRepository.restoreBackupAsynchronously(context, backupFileUri, backupPassphrase)
|
||||
|
||||
store.update {
|
||||
it.copy(
|
||||
backupImportResult = if (importResult == RestoreRepository.BackupImportResult.SUCCESS) null else importResult,
|
||||
restoreInProgress = false,
|
||||
backupRestoreComplete = true,
|
||||
backupEstimatedTotalCount = -1L,
|
||||
backupProgressCount = -1L,
|
||||
backupVerifyingInProgress = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onBackupProgressUpdate(event: BackupEvent) {
|
||||
store.update {
|
||||
it.copy(
|
||||
backupProgressCount = event.count,
|
||||
backupEstimatedTotalCount = event.estimatedTotalCount,
|
||||
backupVerifyingInProgress = event.type == BackupEvent.Type.PROGRESS_VERIFYING,
|
||||
backupRestoreComplete = event.type == BackupEvent.Type.FINISHED,
|
||||
restoreInProgress = event.type != BackupEvent.Type.FINISHED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RestoreLocalBackupViewModel::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.View
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
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.registration.fragments.RegistrationViewDelegate
|
||||
import org.thoughtcrime.securesms.restore.RestoreViewModel
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* This presents a list of options for the user to restore (or skip) a backup.
|
||||
*/
|
||||
class TransferOrRestoreV2Fragment : LoggingFragment(R.layout.fragment_transfer_restore_v2) {
|
||||
private val sharedViewModel by activityViewModels<RestoreViewModel>()
|
||||
private val binding: FragmentTransferRestoreV2Binding by ViewBinderDelegate(FragmentTransferRestoreV2Binding::bind)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.transferOrRestoreTitle)
|
||||
binding.transferOrRestoreFragmentTransfer.setOnClickListener { sharedViewModel.onTransferFromAndroidDeviceSelected() }
|
||||
binding.transferOrRestoreFragmentRestore.setOnClickListener { sharedViewModel.onRestoreFromLocalBackupSelected() }
|
||||
binding.transferOrRestoreFragmentRestoreRemote.setOnClickListener { sharedViewModel.onRestoreFromRemoteBackupSelected() }
|
||||
binding.transferOrRestoreFragmentNext.setOnClickListener { launchSelection(sharedViewModel.getBackupRestorationType()) }
|
||||
binding.transferOrRestoreFragmentMoreOptions.setOnClickListener {
|
||||
Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
|
||||
}
|
||||
|
||||
binding.transferOrRestoreFragmentRestoreRemoteCard.visible = FeatureFlags.messageBackups()
|
||||
binding.transferOrRestoreFragmentMoreOptions.visible = FeatureFlags.messageBackups()
|
||||
|
||||
val description = getString(R.string.TransferOrRestoreFragment__transfer_your_account_and_messages_from_your_old_android_device)
|
||||
val toBold = getString(R.string.TransferOrRestoreFragment__you_need_access_to_your_old_device)
|
||||
|
||||
binding.transferOrRestoreFragmentTransferDescription.text = SpanUtil.boldSubstring(description, toBold)
|
||||
|
||||
sharedViewModel.uiState.observe(viewLifecycleOwner) { state ->
|
||||
updateSelection(state.restorationType)
|
||||
}
|
||||
|
||||
// TODO [regv2]: port backup file detection to here
|
||||
}
|
||||
|
||||
private fun updateSelection(restorationType: BackupRestorationType) {
|
||||
binding.transferOrRestoreFragmentTransferCard.isSelected = restorationType == BackupRestorationType.DEVICE_TRANSFER
|
||||
binding.transferOrRestoreFragmentRestoreCard.isSelected = restorationType == BackupRestorationType.LOCAL_BACKUP
|
||||
binding.transferOrRestoreFragmentRestoreRemoteCard.isSelected = restorationType == BackupRestorationType.REMOTE_BACKUP
|
||||
}
|
||||
|
||||
private fun launchSelection(restorationType: BackupRestorationType) {
|
||||
when (restorationType) {
|
||||
BackupRestorationType.DEVICE_TRANSFER -> {
|
||||
// TODO [regv2]
|
||||
Log.w(TAG, "Not yet implemented!", NotImplementedError())
|
||||
Toast.makeText(requireContext(), "Not yet implemented!", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
BackupRestorationType.LOCAL_BACKUP -> {
|
||||
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()
|
||||
}
|
||||
else -> {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(TransferOrRestoreV2Fragment::class.java)
|
||||
}
|
||||
}
|
||||
25
app/src/main/res/layout/activity_restore.xml
Normal file
25
app/src/main/res/layout/activity_restore.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2024 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
||||
tools:context=".restore.RestoreActivity">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navGraph="@navigation/restore" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
77
app/src/main/res/layout/fragment_choose_backup_v2.xml
Normal file
77
app/src/main/res/layout/fragment_choose_backup_v2.xml
Normal file
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/choose_backup_fragment_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="64dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/ChooseBackupFragment__restore_from_backup"
|
||||
android:textAppearance="@style/Signal.Text.HeadlineMedium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/choose_backup_fragment_message"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/ChooseBackupFragment__restore_your_messages_and_media"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/choose_backup_fragment_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/choose_backup_fragment_learn_more"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/ChooseBackupFragment__learn_more"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/choose_backup_fragment_message" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/choose_backup_fragment_icon"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/circle_tintable"
|
||||
android:contentDescription="@string/ChooseBackupFragment__icon_content_description"
|
||||
android:padding="30dp"
|
||||
app:backgroundTint="@color/signal_colorSurfaceVariant"
|
||||
app:layout_constraintBottom_toTopOf="@+id/choose_backup_fragment_button"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_max="120dp"
|
||||
app:layout_constraintHorizontal_bias="0.498"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/choose_backup_fragment_learn_more"
|
||||
app:layout_constraintWidth_max="120dp"
|
||||
app:srcCompat="@drawable/ic_backup_outline_60"
|
||||
app:tint="@color/signal_colorPrimary" />
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
|
||||
android:id="@+id/choose_backup_fragment_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:circularProgressMaterialButton__label="@string/ChooseBackupFragment__choose_backup"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:materialThemeOverlay="@style/ThemeOverlay.Signal.CircularProgressIndicator.Primary" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
107
app/src/main/res/layout/fragment_restore_local_backup_v2.xml
Normal file
107
app/src/main/res/layout/fragment_restore_local_backup_v2.xml
Normal file
@@ -0,0 +1,107 @@
|
||||
<?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"
|
||||
tools:context=".registration.fragments.RestoreBackupFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel_local_restore_button"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:paddingStart="30dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="@string/RegistrationActivity_cancel"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/restore_button" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verify_subheader"
|
||||
style="@style/Signal.Text.Body.Registration"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/RegistrationActivity_restore_your_messages_and_media_from_a_local_backup"
|
||||
app:layout_constraintTop_toBottomOf="@+id/verify_header"
|
||||
tools:layout_editor_absoluteX="0dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/backup_created_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
app:layout_constraintStart_toStartOf="@+id/verify_subheader"
|
||||
app:layout_constraintTop_toBottomOf="@+id/verify_subheader"
|
||||
tools:text="Backup created: 1 min ago" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/backup_size_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintStart_toStartOf="@+id/backup_created_text"
|
||||
app:layout_constraintTop_toBottomOf="@+id/backup_created_text"
|
||||
tools:text="Backup size: 899 KB" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/backup_progress_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/backup_size_text"
|
||||
tools:text="100 messages so far..." />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verify_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="40dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/RegistrationActivity_restore_from_backup"
|
||||
android:textAppearance="@style/Signal.Text.HeadlineMedium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
|
||||
android:id="@+id/restore_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:circularProgressMaterialButton__label="@string/registration_activity__restore_backup"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/backup_progress_text" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
244
app/src/main/res/layout/fragment_transfer_restore_v2.xml
Normal file
244
app/src/main/res/layout/fragment_transfer_restore_v2.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>
|
||||
41
app/src/main/res/navigation/restore.xml
Normal file
41
app/src/main/res/navigation/restore.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2024 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
<navigation 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:id="@+id/restore"
|
||||
app:startDestination="@id/transfer_or_restore_v2_fragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/transfer_or_restore_v2_fragment"
|
||||
android:name="org.thoughtcrime.securesms.restore.transferorrestore.TransferOrRestoreV2Fragment"
|
||||
android:label="transfer_or_restore"
|
||||
tools:layout="@layout/fragment_transfer_restore_v2">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_transfer_or_restore_to_restore"
|
||||
app:destination="@id/choose_local_backup_fragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/choose_local_backup_fragment"
|
||||
android:name="org.thoughtcrime.securesms.restore.choosebackup.ChooseBackupV2Fragment"
|
||||
android:label="choose_local_backup"
|
||||
tools:layout="@layout/fragment_choose_backup_v2">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_choose_local_backup_fragment_to_restore_local_backup_fragment"
|
||||
app:destination="@id/restore_local_backup_fragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/restore_local_backup_fragment"
|
||||
android:name="org.thoughtcrime.securesms.restore.restorelocalbackup.RestoreLocalBackupFragment"
|
||||
android:label="restore_local_backup"
|
||||
tools:layout="@layout/fragment_restore_local_backup_v2">
|
||||
|
||||
</fragment>
|
||||
</navigation>
|
||||
Reference in New Issue
Block a user