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.AttachmentSecretProvider
|
||||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
|
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
|
||||||
import org.thoughtcrime.securesms.database.DistributionListTables
|
import org.thoughtcrime.securesms.database.DistributionListTables
|
||||||
|
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.groups.GroupId
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.util.toMillis
|
import org.thoughtcrime.securesms.util.toMillis
|
||||||
|
@ -87,7 +89,8 @@ object BackupRepository {
|
||||||
|
|
||||||
private val TAG = Log.tag(BackupRepository::class.java)
|
private val TAG = Log.tag(BackupRepository::class.java)
|
||||||
private const val VERSION = 1L
|
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 ->
|
private val resetInitializedStateErrorAction: StatusCodeErrorAction = { error ->
|
||||||
when (error.code) {
|
when (error.code) {
|
||||||
|
@ -113,7 +116,7 @@ object BackupRepository {
|
||||||
private fun createSignalDatabaseSnapshot(): SignalDatabase {
|
private fun createSignalDatabaseSnapshot(): SignalDatabase {
|
||||||
// Need to do a WAL checkpoint to ensure that the database file we're copying has all pending writes
|
// Need to do a WAL checkpoint to ensure that the database file we're copying has all pending writes
|
||||||
if (!SignalDatabase.rawDatabase.fullWalCheckpoint()) {
|
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
|
// 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 context = AppDependencies.application
|
||||||
|
|
||||||
val existingDbFile = context.getDatabasePath(SignalDatabase.DATABASE_NAME)
|
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 {
|
try {
|
||||||
existingDbFile.copyTo(targetFile, overwrite = true)
|
existingDbFile.copyTo(targetFile, overwrite = true)
|
||||||
|
@ -134,21 +137,54 @@ object BackupRepository {
|
||||||
context = context,
|
context = context,
|
||||||
databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context),
|
databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context),
|
||||||
attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
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() {
|
private fun deleteDatabaseSnapshot() {
|
||||||
val targetFile = AppDependencies.application.getDatabasePath(DB_SNAPSHOT_NAME)
|
val targetFile = AppDependencies.application.getDatabasePath(MAIN_DB_SNAPSHOT_NAME)
|
||||||
if (!targetFile.delete()) {
|
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) {
|
fun export(outputStream: OutputStream, append: (ByteArray) -> Unit, plaintext: Boolean = false) {
|
||||||
val eventTimer = EventTimer()
|
val eventTimer = EventTimer()
|
||||||
val dbSnapshot: SignalDatabase = createSignalDatabaseSnapshot()
|
val dbSnapshot: SignalDatabase = createSignalDatabaseSnapshot()
|
||||||
|
val signalStoreSnapshot: SignalStore = createSignalStoreSnapshot()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val writer: BackupExportWriter = if (plaintext) {
|
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,
|
// 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.
|
// writes from other threads are blocked. This is something to think more about.
|
||||||
dbSnapshot.rawWritableDatabase.withinTransaction {
|
dbSnapshot.rawWritableDatabase.withinTransaction {
|
||||||
AccountDataProcessor.export(dbSnapshot) {
|
AccountDataProcessor.export(dbSnapshot, signalStoreSnapshot) {
|
||||||
writer.write(it)
|
writer.write(it)
|
||||||
eventTimer.emit("account")
|
eventTimer.emit("account")
|
||||||
}
|
}
|
||||||
|
|
||||||
RecipientBackupProcessor.export(dbSnapshot, exportState) {
|
RecipientBackupProcessor.export(dbSnapshot, signalStoreSnapshot, exportState) {
|
||||||
writer.write(it)
|
writer.write(it)
|
||||||
eventTimer.emit("recipient")
|
eventTimer.emit("recipient")
|
||||||
}
|
}
|
||||||
|
@ -209,6 +245,7 @@ object BackupRepository {
|
||||||
Log.d(TAG, "export() ${eventTimer.stop().summary}")
|
Log.d(TAG, "export() ${eventTimer.stop().summary}")
|
||||||
} finally {
|
} finally {
|
||||||
deleteDatabaseSnapshot()
|
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.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
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.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||||
|
@ -32,14 +33,15 @@ import java.util.Currency
|
||||||
|
|
||||||
object AccountDataProcessor {
|
object AccountDataProcessor {
|
||||||
|
|
||||||
fun export(db: SignalDatabase, emitter: BackupFrameEmitter) {
|
fun export(db: SignalDatabase, signalStore: SignalStore, emitter: BackupFrameEmitter) {
|
||||||
val context = AppDependencies.application
|
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 selfRecord = db.recipientTable.getRecordForSync(selfId)!!
|
||||||
|
|
||||||
val donationCurrency = SignalStore.donations.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.DONATION)
|
val donationCurrency = signalStore.donationsValues.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||||
val donationSubscriber = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode(donationCurrency.currencyCode, InAppPaymentSubscriberRecord.Type.DONATION)
|
val donationSubscriber = db.inAppPaymentSubscriberTable.getByCurrencyCode(donationCurrency.currencyCode, InAppPaymentSubscriberRecord.Type.DONATION)
|
||||||
|
val donationLatestSubscription = db.inAppPaymentTable.getLatestInAppPaymentByType(InAppPaymentSubscriberRecord.Type.DONATION.inAppPaymentType)
|
||||||
|
|
||||||
emitter.emit(
|
emitter.emit(
|
||||||
Frame(
|
Frame(
|
||||||
|
@ -50,28 +52,28 @@ object AccountDataProcessor {
|
||||||
avatarUrlPath = selfRecord.signalProfileAvatar ?: "",
|
avatarUrlPath = selfRecord.signalProfileAvatar ?: "",
|
||||||
username = selfRecord.username,
|
username = selfRecord.username,
|
||||||
accountSettings = AccountData.AccountSettings(
|
accountSettings = AccountData.AccountSettings(
|
||||||
storyViewReceiptsEnabled = SignalStore.story.viewedReceiptsEnabled,
|
storyViewReceiptsEnabled = signalStore.storyValues.viewedReceiptsEnabled,
|
||||||
typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(context),
|
typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(context),
|
||||||
readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context),
|
readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context),
|
||||||
sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
|
sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
|
||||||
linkPreviews = SignalStore.settings.isLinkPreviewsEnabled,
|
linkPreviews = signalStore.settingsValues.isLinkPreviewsEnabled,
|
||||||
notDiscoverableByPhoneNumber = SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE,
|
notDiscoverableByPhoneNumber = signalStore.phoneNumberPrivacyValues.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE,
|
||||||
phoneNumberSharingMode = SignalStore.phoneNumberPrivacy.phoneNumberSharingMode.toBackupPhoneNumberSharingMode(),
|
phoneNumberSharingMode = signalStore.phoneNumberPrivacyValues.phoneNumberSharingMode.toBackupPhoneNumberSharingMode(),
|
||||||
preferContactAvatars = SignalStore.settings.isPreferSystemContactPhotos,
|
preferContactAvatars = signalStore.settingsValues.isPreferSystemContactPhotos,
|
||||||
universalExpireTimer = SignalStore.settings.universalExpireTimer,
|
universalExpireTimer = signalStore.settingsValues.universalExpireTimer,
|
||||||
preferredReactionEmoji = SignalStore.emoji.rawReactions,
|
preferredReactionEmoji = signalStore.emojiValues.rawReactions,
|
||||||
storiesDisabled = SignalStore.story.isFeatureDisabled,
|
storiesDisabled = signalStore.storyValues.isFeatureDisabled,
|
||||||
hasViewedOnboardingStory = SignalStore.story.userHasViewedOnboardingStory,
|
hasViewedOnboardingStory = signalStore.storyValues.userHasViewedOnboardingStory,
|
||||||
hasSetMyStoriesPrivacy = SignalStore.story.userHasBeenNotifiedAboutStories,
|
hasSetMyStoriesPrivacy = signalStore.storyValues.userHasBeenNotifiedAboutStories,
|
||||||
keepMutedChatsArchived = SignalStore.settings.shouldKeepMutedChatsArchived(),
|
keepMutedChatsArchived = signalStore.settingsValues.shouldKeepMutedChatsArchived(),
|
||||||
displayBadgesOnProfile = SignalStore.donations.getDisplayBadgesOnProfile(),
|
displayBadgesOnProfile = signalStore.donationsValues.getDisplayBadgesOnProfile(),
|
||||||
hasSeenGroupStoryEducationSheet = SignalStore.story.userHasSeenGroupStoryEducationSheet,
|
hasSeenGroupStoryEducationSheet = signalStore.storyValues.userHasSeenGroupStoryEducationSheet,
|
||||||
hasCompletedUsernameOnboarding = SignalStore.uiHints.hasCompletedUsernameOnboarding()
|
hasCompletedUsernameOnboarding = signalStore.uiHintValues.hasCompletedUsernameOnboarding()
|
||||||
),
|
),
|
||||||
donationSubscriberData = AccountData.SubscriberData(
|
donationSubscriberData = AccountData.SubscriberData(
|
||||||
subscriberId = donationSubscriber?.subscriberId?.bytes?.toByteString() ?: defaultAccountRecord.subscriberId,
|
subscriberId = donationSubscriber?.subscriberId?.bytes?.toByteString() ?: defaultAccountRecord.subscriberId,
|
||||||
currencyCode = donationSubscriber?.currency?.currencyCode ?: defaultAccountRecord.subscriberCurrencyCode,
|
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.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
|
||||||
typealias BackupRecipient = org.thoughtcrime.securesms.backup.v2.proto.Recipient
|
|
||||||
|
|
||||||
object RecipientBackupProcessor {
|
object RecipientBackupProcessor {
|
||||||
|
|
||||||
val TAG = Log.tag(RecipientBackupProcessor::class.java)
|
val TAG = Log.tag(RecipientBackupProcessor::class.java)
|
||||||
|
|
||||||
fun export(db: SignalDatabase, state: ExportState, emitter: BackupFrameEmitter) {
|
fun export(db: SignalDatabase, signalStore: SignalStore, state: ExportState, emitter: BackupFrameEmitter) {
|
||||||
val selfId = db.recipientTable.getByAci(SignalStore.account.aci!!).get().toLong()
|
val selfId = db.recipientTable.getByAci(signalStore.accountValues.aci!!).get().toLong()
|
||||||
val releaseChannelId = SignalStore.releaseChannel.releaseChannelRecipientId
|
val releaseChannelId = signalStore.releaseChannelValues.releaseChannelRecipientId
|
||||||
if (releaseChannelId != null) {
|
if (releaseChannelId != null) {
|
||||||
emitter.emit(
|
emitter.emit(
|
||||||
Frame(
|
Frame(
|
||||||
|
|
|
@ -32,7 +32,7 @@ public class KeyValueDatabase extends SQLiteOpenHelper implements SignalDatabase
|
||||||
private static final String TAG = Log.tag(KeyValueDatabase.class);
|
private static final String TAG = Log.tag(KeyValueDatabase.class);
|
||||||
|
|
||||||
private static final int DATABASE_VERSION = 1;
|
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 TABLE_NAME = "key_value";
|
||||||
private static final String ID = "_id";
|
private static final String ID = "_id";
|
||||||
|
@ -65,9 +65,16 @@ public class KeyValueDatabase extends SQLiteOpenHelper implements SignalDatabase
|
||||||
return context.getDatabasePath(DATABASE_NAME).exists();
|
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) {
|
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;
|
this.application = application;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies.application
|
||||||
/**
|
/**
|
||||||
* Simple, encrypted key-value store.
|
* Simple, encrypted key-value store.
|
||||||
*/
|
*/
|
||||||
class SignalStore private constructor(private val store: KeyValueStore) {
|
class SignalStore(private val store: KeyValueStore) {
|
||||||
|
|
||||||
val accountValues = AccountValues(store)
|
val accountValues = AccountValues(store)
|
||||||
val svrValues = SvrValues(store)
|
val svrValues = SvrValues(store)
|
||||||
|
|
Loading…
Add table
Reference in a new issue