diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 05afaa5027..d5e2a02899 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -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() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataProcessor.kt index 70f1f3908b..0abbd0cc72 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataProcessor.kt @@ -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() ) ) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/RecipientBackupProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/RecipientBackupProcessor.kt index da81924383..8ce704c43b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/RecipientBackupProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/RecipientBackupProcessor.kt @@ -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( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java index 238f540cc0..0e61379133 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java @@ -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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.kt index 1ff9e7bc10..d3dcf5adcb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.kt @@ -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)