Use a snapshot of the SignalStore during backups.
This commit is contained in:
parent
863b443317
commit
362cdfc463
5 changed files with 79 additions and 35 deletions
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue