Use a snapshot of the SignalStore during backups.

This commit is contained in:
Greyson Parrelli 2024-06-20 12:23:19 -04:00
parent 863b443317
commit 362cdfc463
5 changed files with 79 additions and 35 deletions

View file

@ -47,11 +47,13 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.Recurring
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
import org.thoughtcrime.securesms.database.DistributionListTables
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.toMillis
@ -87,7 +89,8 @@ object BackupRepository {
private val TAG = Log.tag(BackupRepository::class.java)
private const val VERSION = 1L
private const val DB_SNAPSHOT_NAME = "signal-snapshot.db"
private const val MAIN_DB_SNAPSHOT_NAME = "signal-snapshot.db"
private const val KEYVALUE_DB_SNAPSHOT_NAME = "key-value-snapshot.db"
private val resetInitializedStateErrorAction: StatusCodeErrorAction = { error ->
when (error.code) {
@ -113,7 +116,7 @@ object BackupRepository {
private fun createSignalDatabaseSnapshot(): SignalDatabase {
// Need to do a WAL checkpoint to ensure that the database file we're copying has all pending writes
if (!SignalDatabase.rawDatabase.fullWalCheckpoint()) {
Log.w(TAG, "Failed to checkpoint WAL! Not guaranteed to be using the most recent data.")
Log.w(TAG, "Failed to checkpoint WAL for main database! Not guaranteed to be using the most recent data.")
}
// We make a copy of the database within a transaction to ensure that no writes occur while we're copying the file
@ -121,7 +124,7 @@ object BackupRepository {
val context = AppDependencies.application
val existingDbFile = context.getDatabasePath(SignalDatabase.DATABASE_NAME)
val targetFile = File(existingDbFile.parentFile, DB_SNAPSHOT_NAME)
val targetFile = File(existingDbFile.parentFile, MAIN_DB_SNAPSHOT_NAME)
try {
existingDbFile.copyTo(targetFile, overwrite = true)
@ -134,21 +137,54 @@ object BackupRepository {
context = context,
databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context),
attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
name = DB_SNAPSHOT_NAME
name = MAIN_DB_SNAPSHOT_NAME
)
}
}
private fun createSignalStoreSnapshot(): SignalStore {
val context = AppDependencies.application
// Need to do a WAL checkpoint to ensure that the database file we're copying has all pending writes
if (!KeyValueDatabase.getInstance(context).writableDatabase.fullWalCheckpoint()) {
Log.w(TAG, "Failed to checkpoint WAL for KeyValueDatabase! Not guaranteed to be using the most recent data.")
}
// We make a copy of the database within a transaction to ensure that no writes occur while we're copying the file
return KeyValueDatabase.getInstance(context).writableDatabase.withinTransaction {
val existingDbFile = context.getDatabasePath(KeyValueDatabase.DATABASE_NAME)
val targetFile = File(existingDbFile.parentFile, KEYVALUE_DB_SNAPSHOT_NAME)
try {
existingDbFile.copyTo(targetFile, overwrite = true)
} catch (e: IOException) {
// TODO [backup] Gracefully handle this error
throw IllegalStateException("Failed to copy database file!", e)
}
val db = KeyValueDatabase.createWithName(context, KEYVALUE_DB_SNAPSHOT_NAME)
SignalStore(KeyValueStore(db))
}
}
private fun deleteDatabaseSnapshot() {
val targetFile = AppDependencies.application.getDatabasePath(DB_SNAPSHOT_NAME)
val targetFile = AppDependencies.application.getDatabasePath(MAIN_DB_SNAPSHOT_NAME)
if (!targetFile.delete()) {
Log.w(TAG, "Failed to delete database snapshot!")
Log.w(TAG, "Failed to delete main database snapshot!")
}
}
private fun deleteSignalStoreSnapshot() {
val targetFile = AppDependencies.application.getDatabasePath(KEYVALUE_DB_SNAPSHOT_NAME)
if (!targetFile.delete()) {
Log.w(TAG, "Failed to delete key value database snapshot!")
}
}
fun export(outputStream: OutputStream, append: (ByteArray) -> Unit, plaintext: Boolean = false) {
val eventTimer = EventTimer()
val dbSnapshot: SignalDatabase = createSignalDatabaseSnapshot()
val signalStoreSnapshot: SignalStore = createSignalStoreSnapshot()
try {
val writer: BackupExportWriter = if (plaintext) {
@ -174,12 +210,12 @@ object BackupRepository {
// Note: Without a transaction, we may export inconsistent state. But because we have a transaction,
// writes from other threads are blocked. This is something to think more about.
dbSnapshot.rawWritableDatabase.withinTransaction {
AccountDataProcessor.export(dbSnapshot) {
AccountDataProcessor.export(dbSnapshot, signalStoreSnapshot) {
writer.write(it)
eventTimer.emit("account")
}
RecipientBackupProcessor.export(dbSnapshot, exportState) {
RecipientBackupProcessor.export(dbSnapshot, signalStoreSnapshot, exportState) {
writer.write(it)
eventTimer.emit("recipient")
}
@ -209,6 +245,7 @@ object BackupRepository {
Log.d(TAG, "export() ${eventTimer.stop().summary}")
} finally {
deleteDatabaseSnapshot()
deleteSignalStoreSnapshot()
}
}

View file

@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaym
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
@ -32,14 +33,15 @@ import java.util.Currency
object AccountDataProcessor {
fun export(db: SignalDatabase, emitter: BackupFrameEmitter) {
fun export(db: SignalDatabase, signalStore: SignalStore, emitter: BackupFrameEmitter) {
val context = AppDependencies.application
val selfId = db.recipientTable.getByAci(SignalStore.account.aci!!).get()
val selfId = db.recipientTable.getByAci(signalStore.accountValues.aci!!).get()
val selfRecord = db.recipientTable.getRecordForSync(selfId)!!
val donationCurrency = SignalStore.donations.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.DONATION)
val donationSubscriber = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode(donationCurrency.currencyCode, InAppPaymentSubscriberRecord.Type.DONATION)
val donationCurrency = signalStore.donationsValues.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.DONATION)
val donationSubscriber = db.inAppPaymentSubscriberTable.getByCurrencyCode(donationCurrency.currencyCode, InAppPaymentSubscriberRecord.Type.DONATION)
val donationLatestSubscription = db.inAppPaymentTable.getLatestInAppPaymentByType(InAppPaymentSubscriberRecord.Type.DONATION.inAppPaymentType)
emitter.emit(
Frame(
@ -50,28 +52,28 @@ object AccountDataProcessor {
avatarUrlPath = selfRecord.signalProfileAvatar ?: "",
username = selfRecord.username,
accountSettings = AccountData.AccountSettings(
storyViewReceiptsEnabled = SignalStore.story.viewedReceiptsEnabled,
storyViewReceiptsEnabled = signalStore.storyValues.viewedReceiptsEnabled,
typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(context),
readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context),
sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
linkPreviews = SignalStore.settings.isLinkPreviewsEnabled,
notDiscoverableByPhoneNumber = SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE,
phoneNumberSharingMode = SignalStore.phoneNumberPrivacy.phoneNumberSharingMode.toBackupPhoneNumberSharingMode(),
preferContactAvatars = SignalStore.settings.isPreferSystemContactPhotos,
universalExpireTimer = SignalStore.settings.universalExpireTimer,
preferredReactionEmoji = SignalStore.emoji.rawReactions,
storiesDisabled = SignalStore.story.isFeatureDisabled,
hasViewedOnboardingStory = SignalStore.story.userHasViewedOnboardingStory,
hasSetMyStoriesPrivacy = SignalStore.story.userHasBeenNotifiedAboutStories,
keepMutedChatsArchived = SignalStore.settings.shouldKeepMutedChatsArchived(),
displayBadgesOnProfile = SignalStore.donations.getDisplayBadgesOnProfile(),
hasSeenGroupStoryEducationSheet = SignalStore.story.userHasSeenGroupStoryEducationSheet,
hasCompletedUsernameOnboarding = SignalStore.uiHints.hasCompletedUsernameOnboarding()
linkPreviews = signalStore.settingsValues.isLinkPreviewsEnabled,
notDiscoverableByPhoneNumber = signalStore.phoneNumberPrivacyValues.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE,
phoneNumberSharingMode = signalStore.phoneNumberPrivacyValues.phoneNumberSharingMode.toBackupPhoneNumberSharingMode(),
preferContactAvatars = signalStore.settingsValues.isPreferSystemContactPhotos,
universalExpireTimer = signalStore.settingsValues.universalExpireTimer,
preferredReactionEmoji = signalStore.emojiValues.rawReactions,
storiesDisabled = signalStore.storyValues.isFeatureDisabled,
hasViewedOnboardingStory = signalStore.storyValues.userHasViewedOnboardingStory,
hasSetMyStoriesPrivacy = signalStore.storyValues.userHasBeenNotifiedAboutStories,
keepMutedChatsArchived = signalStore.settingsValues.shouldKeepMutedChatsArchived(),
displayBadgesOnProfile = signalStore.donationsValues.getDisplayBadgesOnProfile(),
hasSeenGroupStoryEducationSheet = signalStore.storyValues.userHasSeenGroupStoryEducationSheet,
hasCompletedUsernameOnboarding = signalStore.uiHintValues.hasCompletedUsernameOnboarding()
),
donationSubscriberData = AccountData.SubscriberData(
subscriberId = donationSubscriber?.subscriberId?.bytes?.toByteString() ?: defaultAccountRecord.subscriberId,
currencyCode = donationSubscriber?.currency?.currencyCode ?: defaultAccountRecord.subscriberCurrencyCode,
manuallyCancelled = InAppPaymentsRepository.isUserManuallyCancelled(InAppPaymentSubscriberRecord.Type.DONATION)
manuallyCancelled = donationLatestSubscription?.data?.cancellation?.reason?.let { it == InAppPaymentData.Cancellation.Reason.MANUAL } ?: SignalStore.donations.isUserManuallyCancelled()
)
)
)

View file

@ -24,15 +24,13 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
typealias BackupRecipient = org.thoughtcrime.securesms.backup.v2.proto.Recipient
object RecipientBackupProcessor {
val TAG = Log.tag(RecipientBackupProcessor::class.java)
fun export(db: SignalDatabase, state: ExportState, emitter: BackupFrameEmitter) {
val selfId = db.recipientTable.getByAci(SignalStore.account.aci!!).get().toLong()
val releaseChannelId = SignalStore.releaseChannel.releaseChannelRecipientId
fun export(db: SignalDatabase, signalStore: SignalStore, state: ExportState, emitter: BackupFrameEmitter) {
val selfId = db.recipientTable.getByAci(signalStore.accountValues.aci!!).get().toLong()
val releaseChannelId = signalStore.releaseChannelValues.releaseChannelRecipientId
if (releaseChannelId != null) {
emitter.emit(
Frame(

View file

@ -32,7 +32,7 @@ public class KeyValueDatabase extends SQLiteOpenHelper implements SignalDatabase
private static final String TAG = Log.tag(KeyValueDatabase.class);
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "signal-key-value.db";
public static final String DATABASE_NAME = "signal-key-value.db";
private static final String TABLE_NAME = "key_value";
private static final String ID = "_id";
@ -65,9 +65,16 @@ public class KeyValueDatabase extends SQLiteOpenHelper implements SignalDatabase
return context.getDatabasePath(DATABASE_NAME).exists();
}
public static KeyValueDatabase createWithName(@NonNull Application application, @NonNull String name) {
return new KeyValueDatabase(application, DatabaseSecretProvider.getOrCreateDatabaseSecret(application), name);
}
private KeyValueDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) {
super(application, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, 0,new SqlCipherErrorHandler(DATABASE_NAME), new SqlCipherDatabaseHook(), true);
this(application, databaseSecret, DATABASE_NAME);
}
private KeyValueDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret, @NonNull String name) {
super(application, name, databaseSecret.asString(), null, DATABASE_VERSION, 0, new SqlCipherErrorHandler(name), new SqlCipherDatabaseHook(), true);
this.application = application;
}

View file

@ -9,7 +9,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies.application
/**
* Simple, encrypted key-value store.
*/
class SignalStore private constructor(private val store: KeyValueStore) {
class SignalStore(private val store: KeyValueStore) {
val accountValues = AccountValues(store)
val svrValues = SvrValues(store)