Fix legacy restore options in new restore flows.

This commit is contained in:
Cody Henthorne
2025-06-18 15:47:32 -04:00
committed by Michelle Tang
parent 3ea2c01c15
commit be45bdb562
16 changed files with 107 additions and 29 deletions

View File

@@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.EmojiSearchTable;
import org.thoughtcrime.securesms.database.GroupReceiptTable;
import org.thoughtcrime.securesms.database.KeyValueDatabase;
import org.thoughtcrime.securesms.database.KyberPreKeyTable;
import org.thoughtcrime.securesms.database.MentionTable;
import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.OneTimePreKeyTable;
@@ -88,6 +89,7 @@ public class FullBackupExporter extends FullBackupBase {
private static final Set<String> TABLE_CONTENT_BLOCKLIST = SetUtil.newHashSet(
SignedPreKeyTable.TABLE_NAME,
OneTimePreKeyTable.TABLE_NAME,
KyberPreKeyTable.TABLE_NAME,
SessionTable.TABLE_NAME,
SearchTable.FTS_TABLE_NAME,
EmojiSearchTable.TABLE_NAME,

View File

@@ -29,7 +29,10 @@ import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.EmojiSearchTable;
import org.thoughtcrime.securesms.database.KeyValueDatabase;
import org.thoughtcrime.securesms.database.KyberPreKeyTable;
import org.thoughtcrime.securesms.database.OneTimePreKeyTable;
import org.thoughtcrime.securesms.database.SearchTable;
import org.thoughtcrime.securesms.database.SignedPreKeyTable;
import org.thoughtcrime.securesms.database.StickerTable;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
@@ -60,11 +63,15 @@ import java.util.Queue;
import java.util.Set;
import java.util.stream.Collectors;
import kotlin.collections.SetsKt;
public class FullBackupImporter extends FullBackupBase {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(FullBackupImporter.class);
private static final Set<String> KEY_TABLES = SetsKt.setOf(KyberPreKeyTable.TABLE_NAME, OneTimePreKeyTable.TABLE_NAME, SignedPreKeyTable.TABLE_NAME);
public static boolean validatePassphrase(@NonNull Context context,
@NonNull Uri uri,
@NonNull String passphrase)
@@ -83,17 +90,25 @@ public class FullBackupImporter extends FullBackupBase {
}
}
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
public static void importFile(@NonNull Context context,
@NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase db,
@NonNull Uri uri,
@NonNull String passphrase,
boolean excludeKeyTables)
throws IOException
{
try (InputStream is = getInputStream(context, uri)) {
importFile(context, attachmentSecret, db, is, passphrase);
importFile(context, attachmentSecret, db, is, passphrase, excludeKeyTables);
}
}
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase db, @NonNull InputStream is, @NonNull String passphrase)
public static void importFile(@NonNull Context context,
@NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase db,
@NonNull InputStream is,
@NonNull String passphrase,
boolean excludeKeyTables)
throws IOException
{
int count = 0;
@@ -106,7 +121,7 @@ public class FullBackupImporter extends FullBackupBase {
try {
BackupRecordInputStream inputStream = new BackupRecordInputStream(is, passphrase);
dropAllTables(db);
dropAllTables(db, excludeKeyTables);
BackupFrame frame;
@@ -115,7 +130,7 @@ public class FullBackupImporter extends FullBackupBase {
count++;
if (frame.version != null) processVersion(db, frame.version);
else if (frame.statement != null) processStatement(db, frame.statement);
else if (frame.statement != null) processStatement(db, frame.statement, excludeKeyTables);
else if (frame.preference != null) processPreference(context, frame.preference);
else if (frame.attachment != null) processAttachment(context, attachmentSecret, db, frame.attachment, inputStream);
else if (frame.sticker != null) processSticker(context, attachmentSecret, db, frame.sticker, inputStream);
@@ -162,7 +177,7 @@ public class FullBackupImporter extends FullBackupBase {
db.setVersion(version.version);
}
private static void processStatement(@NonNull SQLiteDatabase db, SqlStatement statement) {
private static void processStatement(@NonNull SQLiteDatabase db, SqlStatement statement, boolean excludeKeyTables) {
if (statement.statement == null) {
Log.w(TAG, "Null statement!");
return;
@@ -172,8 +187,9 @@ public class FullBackupImporter extends FullBackupBase {
boolean isForEmojiSecretTable = statement.statement.contains(EmojiSearchTable.TABLE_NAME + "_");
boolean isForSqliteSecretTable = statement.statement.toLowerCase().startsWith("create table sqlite_");
boolean isForRemoteMegaphoneTable = statement.statement.toLowerCase().startsWith("insert into remote_megaphone");
boolean isForExcludedKeyTable = excludeKeyTables && KEY_TABLES.stream().anyMatch(table -> statement.statement.toLowerCase().contains(table));
if (isForMmsFtsSecretTable || isForEmojiSecretTable || isForSqliteSecretTable || isForRemoteMegaphoneTable) {
if (isForMmsFtsSecretTable || isForEmojiSecretTable || isForSqliteSecretTable || isForRemoteMegaphoneTable || isForExcludedKeyTable) {
Log.i(TAG, "Ignoring import for statement: " + statement.statement);
return;
}
@@ -315,12 +331,18 @@ public class FullBackupImporter extends FullBackupBase {
}
}
private static void dropAllTables(@NonNull SQLiteDatabase db) {
private static void dropAllTables(@NonNull SQLiteDatabase db, boolean excludeKeyTables) {
for (String trigger : SqlUtil.getAllTriggers(db)) {
Log.i(TAG, "Dropping trigger: " + trigger);
db.execSQL("DROP TRIGGER IF EXISTS " + trigger);
}
for (String table : getTablesToDropInOrder(db)) {
if (excludeKeyTables && KEY_TABLES.contains(table)) {
Log.i(TAG, "Skipping table: " + table);
continue;
}
Log.i(TAG, "Dropping table: " + table);
db.execSQL("DROP TABLE IF EXISTS " + table);
}

View File

@@ -351,7 +351,7 @@ private fun AppSettingsContent(
onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment)
},
enabled = isRegisteredAndUpToDate
enabled = state.legacyLocalBackupsEnabled || isRegisteredAndUpToDate
)
}
@@ -683,7 +683,8 @@ private fun AppSettingsContentPreview() {
showPayments = true,
showAppUpdates = true,
showBackups = true,
backupFailureState = BackupFailureState.OUT_OF_STORAGE_SPACE
backupFailureState = BackupFailureState.OUT_OF_STORAGE_SPACE,
legacyLocalBackupsEnabled = false
),
bannerManager = BannerManager(
banners = listOf(TestBanner())

View File

@@ -16,7 +16,8 @@ data class AppSettingsState(
val showPayments: Boolean = SignalStore.payments.paymentsAvailability.showPaymentsMenu(),
val showAppUpdates: Boolean = Environment.IS_NIGHTLY,
val showBackups: Boolean = RemoteConfig.messageBackups,
val backupFailureState: BackupFailureState = BackupFailureState.NONE
val backupFailureState: BackupFailureState = BackupFailureState.NONE,
val legacyLocalBackupsEnabled: Boolean
) {
fun isRegisteredAndUpToDate(): Boolean {
return !userUnregistered && !clientDeprecated

View File

@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.livedata.Store
@@ -22,11 +23,12 @@ class AppSettingsViewModel : ViewModel() {
private val store = Store(
AppSettingsState(
0,
SignalStore.inAppPayments.getExpiredGiftBadge() != null,
SignalStore.inAppPayments.isLikelyASustainer() || InAppDonations.hasAtLeastOnePaymentMethodAvailable(),
TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application) || !SignalStore.account.isRegistered,
SignalStore.misc.isClientDeprecated
unreadPaymentsCount = 0,
hasExpiredGiftBadge = SignalStore.inAppPayments.getExpiredGiftBadge() != null,
allowUserToGoToDonationManagementScreen = SignalStore.inAppPayments.isLikelyASustainer() || InAppDonations.hasAtLeastOnePaymentMethodAvailable(),
userUnregistered = TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application) || !SignalStore.account.isRegistered,
clientDeprecated = SignalStore.misc.isClientDeprecated,
legacyLocalBackupsEnabled = !RemoteConfig.messageBackups && SignalStore.settings.isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(AppDependencies.application)
)
)

View File

@@ -125,7 +125,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
clickPref(
title = DSLSettingsText.from(R.string.preferences_chats__transfer_account),
summary = DSLSettingsText.from(R.string.preferences_chats__transfer_account_to_a_new_android_device),
isEnabled = state.isNotDeprecatedOrUnregistered(),
isEnabled = state.canTransferWhileUnregistered || state.isNotDeprecatedOrUnregistered(),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_oldDeviceTransferActivity)
}

View File

@@ -6,7 +6,8 @@ data class AccountSettingsState(
val pinRemindersEnabled: Boolean,
val registrationLockEnabled: Boolean,
val userUnregistered: Boolean,
val clientDeprecated: Boolean
val clientDeprecated: Boolean,
val canTransferWhileUnregistered: Boolean
) {
fun isNotDeprecatedOrUnregistered(): Boolean {
return !(userUnregistered || clientDeprecated)

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.livedata.Store
@@ -23,7 +24,8 @@ class AccountSettingsViewModel : ViewModel() {
pinRemindersEnabled = SignalStore.pin.arePinRemindersEnabled() && SignalStore.svr.hasPin(),
registrationLockEnabled = SignalStore.svr.isRegistrationLockEnabled,
userUnregistered = TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application),
clientDeprecated = SignalStore.misc.isClientDeprecated
clientDeprecated = SignalStore.misc.isClientDeprecated,
canTransferWhileUnregistered = RemoteConfig.restoreAfterRegistration
)
}
}

View File

@@ -34,6 +34,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
switchPref(
title = DSLSettingsText.from(R.string.preferences__generate_link_previews),
summary = DSLSettingsText.from(R.string.preferences__retrieve_link_previews_from_websites_for_messages),
isEnabled = state.isRegisteredAndUpToDate(),
isChecked = state.generateLinkPreviews,
onClick = {
viewModel.setGenerateLinkPreviewsEnabled(!state.generateLinkPreviews)
@@ -43,6 +44,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
switchPref(
title = DSLSettingsText.from(R.string.preferences__pref_use_address_book_photos),
summary = DSLSettingsText.from(R.string.preferences__display_contact_photos_from_your_address_book_if_available),
isEnabled = state.isRegisteredAndUpToDate(),
isChecked = state.useAddressBook,
onClick = {
viewModel.setUseAddressBook(!state.useAddressBook)
@@ -52,6 +54,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
switchPref(
title = DSLSettingsText.from(R.string.preferences__pref_keep_muted_chats_archived),
summary = DSLSettingsText.from(R.string.preferences__muted_chats_that_are_archived_will_remain_archived),
isEnabled = state.isRegisteredAndUpToDate(),
isChecked = state.keepMutedChatsArchived,
onClick = {
viewModel.setKeepMutedChatsArchived(!state.keepMutedChatsArchived)
@@ -65,6 +68,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
if (state.folderCount == 1) {
clickPref(
title = DSLSettingsText.from(R.string.ChatsSettingsFragment__add_chat_folder),
isEnabled = state.isRegisteredAndUpToDate(),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_chatFoldersFragment)
}
@@ -73,6 +77,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
clickPref(
title = DSLSettingsText.from(R.string.ChatsSettingsFragment__add_edit_chat_folder),
summary = DSLSettingsText.from(resources.getQuantityString(R.plurals.ChatsSettingsFragment__d_folder, state.folderCount, state.folderCount)),
isEnabled = state.isRegisteredAndUpToDate(),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_chatFoldersFragment)
}
@@ -85,6 +90,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
switchPref(
title = DSLSettingsText.from(R.string.preferences_advanced__use_system_emoji),
isEnabled = state.isRegisteredAndUpToDate(),
isChecked = state.useSystemEmoji,
onClick = {
viewModel.setUseSystemEmoji(!state.useSystemEmoji)
@@ -93,6 +99,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
switchPref(
title = DSLSettingsText.from(R.string.ChatsSettingsFragment__send_with_enter),
isEnabled = state.isRegisteredAndUpToDate(),
isChecked = state.enterKeySends,
onClick = {
viewModel.setEnterKeySends(!state.enterKeySends)
@@ -107,6 +114,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
clickPref(
title = DSLSettingsText.from(R.string.preferences_chats__chat_backups),
summary = DSLSettingsText.from(if (state.localBackupsEnabled) R.string.arrays__enabled else R.string.arrays__disabled),
isEnabled = state.localBackupsEnabled || state.isRegisteredAndUpToDate(),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_backupsPreferenceFragment)
}

View File

@@ -7,5 +7,11 @@ data class ChatsSettingsState(
val useSystemEmoji: Boolean,
val enterKeySends: Boolean,
val localBackupsEnabled: Boolean,
val folderCount: Int
)
val folderCount: Int,
val userUnregistered: Boolean,
val clientDeprecated: Boolean
) {
fun isRegisteredAndUpToDate(): Boolean {
return !userUnregistered && !clientDeprecated
}
}

View File

@@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.ThrottledDebouncer
import org.thoughtcrime.securesms.util.livedata.Store
@@ -27,7 +28,9 @@ class ChatsSettingsViewModel @JvmOverloads constructor(
useSystemEmoji = SignalStore.settings.isPreferSystemEmoji,
enterKeySends = SignalStore.settings.isEnterKeySends,
localBackupsEnabled = SignalStore.settings.isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(AppDependencies.application),
folderCount = 0
folderCount = 0,
userUnregistered = TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application) || !SignalStore.account.isRegistered,
clientDeprecated = SignalStore.misc.isClientDeprecated
)
)

View File

@@ -18,7 +18,9 @@ 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.util.RemoteConfig;
import java.io.IOException;
import java.io.InputStream;
@@ -42,14 +44,20 @@ final class NewDeviceServerTask implements ServerTask {
DataRestoreConstraint.setRestoringData(true);
SQLiteDatabase database = SignalDatabase.getBackupDatabase();
String passphrase = "deadbeef";
String passphrase;
if (RemoteConfig.restoreAfterRegistration()) {
passphrase = SignalStore.account().getAccountEntropyPool().getValue();
} else {
passphrase = "deadbeef";
}
BackupPassphrase.set(context, passphrase);
FullBackupImporter.importFile(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
database,
inputStream,
passphrase);
passphrase,
RemoteConfig.restoreAfterRegistration());
SignalDatabase.runPostBackupRestoreTasks(database);
NotificationChannels.getInstance().restoreContactNotificationChannels();

View File

@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor;
import org.thoughtcrime.securesms.util.RemoteConfig;
import java.io.IOException;
import java.io.OutputStream;
@@ -39,11 +40,18 @@ final class OldDeviceClientTask implements ClientTask {
EventBus.getDefault().register(this);
try {
String passphrase;
if (RemoteConfig.restoreAfterRegistration()) {
passphrase = SignalStore.account().getAccountEntropyPool().getValue();
} else {
passphrase = "deadbeef";
}
FullBackupExporter.transfer(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
SignalDatabase.getBackupDatabase(),
outputStream,
"deadbeef");
passphrase);
} catch (Exception e) {
DeviceTransferBlockingInterceptor.getInstance().unblockNetwork();
throw e;

View File

@@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.JavaTimeExtensionsKt;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -100,6 +101,8 @@ public class BackupsPreferenceFragment extends Fragment {
formatter.setMaximumFractionDigits(1);
EventBus.getDefault().register(this);
updateToggle();
}
@Override
@@ -301,11 +304,20 @@ public class BackupsPreferenceFragment extends Fragment {
timeLabel.setText(JavaTimeExtensionsKt.formatHours(time, requireContext()));
}
private void updateToggle() {
boolean userUnregistered = TextSecurePreferences.isUnauthorizedReceived(AppDependencies.getApplication()) || !SignalStore.account().isRegistered();
boolean clientDeprecated = SignalStore.misc().isClientDeprecated();
boolean legacyLocalBackupsEnabled = SignalStore.settings().isBackupEnabled() && BackupUtil.canUserAccessBackupDirectory(AppDependencies.getApplication());
toggle.setEnabled(legacyLocalBackupsEnabled || (!userUnregistered && !clientDeprecated));
}
private void setBackupsEnabled() {
toggle.setText(R.string.BackupsPreferenceFragment__turn_off);
create.setVisibility(View.VISIBLE);
verify.setVisibility(View.VISIBLE);
timer.setVisibility(View.VISIBLE);
updateToggle();
updateTimeLabel();
setBackupFolderName();
}
@@ -316,6 +328,7 @@ public class BackupsPreferenceFragment extends Fragment {
folder.setVisibility(View.GONE);
verify.setVisibility(View.GONE);
timer.setVisibility(View.GONE);
updateToggle();
AppDependencies.getJobManager().cancelAllInQueue(LocalBackupJob.QUEUE);
}
}

View File

@@ -60,7 +60,8 @@ object RestoreRepository {
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
database,
backupFileUri,
passphrase
passphrase,
SignalStore.registration.localRegistrationMetadata != null
)
Log.i(TAG, "Backup importer complete.")

View File

@@ -21,8 +21,8 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob
import org.thoughtcrime.securesms.keyvalue.Completed
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
import org.thoughtcrime.securesms.restore.RestoreRepository
/**