From 5a5702302f47b5713096a3c79b4090dafa3f35ac Mon Sep 17 00:00:00 2001 From: Anton Chekulaev Date: Fri, 11 Sep 2020 16:29:08 +1000 Subject: [PATCH] Backup file database table. New backup util class. --- .../securesms/RegistrationActivity.java | 15 ++- .../securesms/backup/BackupDialog.java | 3 +- .../securesms/database/DatabaseFactory.java | 6 + .../database/LokiBackupFilesDatabase.kt | 105 ++++++++++++++++++ .../database/helpers/SQLCipherOpenHelper.java | 9 +- .../database/model/BackupFileRecord.kt | 12 ++ .../securesms/jobs/LocalBackupJob.java | 4 +- .../preferences/ChatsPreferenceFragment.java | 8 +- .../thoughtcrime/securesms/util/BackupUtil.kt | 38 +++++++ .../{BackupUtil.java => BackupUtilOld.java} | 5 +- 10 files changed, 188 insertions(+), 17 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/database/LokiBackupFilesDatabase.kt create mode 100644 src/org/thoughtcrime/securesms/database/model/BackupFileRecord.kt create mode 100644 src/org/thoughtcrime/securesms/util/BackupUtil.kt rename src/org/thoughtcrime/securesms/util/{BackupUtil.java => BackupUtilOld.java} (96%) diff --git a/src/org/thoughtcrime/securesms/RegistrationActivity.java b/src/org/thoughtcrime/securesms/RegistrationActivity.java index fdabe5d229..42307afe83 100644 --- a/src/org/thoughtcrime/securesms/RegistrationActivity.java +++ b/src/org/thoughtcrime/securesms/RegistrationActivity.java @@ -57,7 +57,7 @@ import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.push.AccountManagerFactory; import org.thoughtcrime.securesms.registration.CaptchaActivity; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; -import org.thoughtcrime.securesms.util.BackupUtil; +import org.thoughtcrime.securesms.util.BackupUtilOld; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.ServiceUtil; @@ -76,7 +76,6 @@ import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.internal.push.LockedException; import java.io.IOException; -import java.lang.ref.WeakReference; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -275,11 +274,11 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif if (getIntent().getBooleanExtra(RE_REGISTRATION_EXTRA, false)) return; - new AsyncTask() { + new AsyncTask() { @Override - protected @Nullable BackupUtil.BackupInfo doInBackground(Void... voids) { + protected @Nullable BackupUtilOld.BackupInfo doInBackground(Void... voids) { try { - return BackupUtil.getLatestBackup(RegistrationActivity.this); + return BackupUtilOld.getLatestBackup(RegistrationActivity.this); } catch (NoExternalStorageException e) { Log.w(TAG, e); return null; @@ -287,7 +286,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif } @Override - protected void onPostExecute(@Nullable BackupUtil.BackupInfo backup) { + protected void onPostExecute(@Nullable BackupUtilOld.BackupInfo backup) { if (backup != null) displayRestoreView(backup); } }.execute(); @@ -304,7 +303,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif } @SuppressLint("StaticFieldLeak") - private void handleRestore(BackupUtil.BackupInfo backup) { + private void handleRestore(BackupUtilOld.BackupInfo backup) { View view = LayoutInflater.from(this).inflate(R.layout.enter_backup_passphrase_dialog, null); EditText prompt = view.findViewById(R.id.restore_passphrase_input); @@ -661,7 +660,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif finish(); } - private void displayRestoreView(@NonNull BackupUtil.BackupInfo backup) { + private void displayRestoreView(@NonNull BackupUtilOld.BackupInfo backup) { title.animate().translationX(title.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() { @Override public void onAnimationEnd(Animator animation) { diff --git a/src/org/thoughtcrime/securesms/backup/BackupDialog.java b/src/org/thoughtcrime/securesms/backup/BackupDialog.java index 1da08039f8..7f5a68f597 100644 --- a/src/org/thoughtcrime/securesms/backup/BackupDialog.java +++ b/src/org/thoughtcrime/securesms/backup/BackupDialog.java @@ -15,6 +15,7 @@ import network.loki.messenger.R; import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; import org.thoughtcrime.securesms.service.LocalBackupListener; import org.thoughtcrime.securesms.util.BackupUtil; +import org.thoughtcrime.securesms.util.BackupUtilOld; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; @@ -77,7 +78,7 @@ public class BackupDialog { .setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> { BackupPassphrase.set(context, null); TextSecurePreferences.setBackupEnabled(context, false); - BackupUtil.deleteAllBackups(context); + BackupUtilOld.deleteAllBackups(context); preference.setChecked(false); }) .create() diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 164f60762f..de065a819b 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -74,6 +74,7 @@ public class DatabaseFactory { private final LokiMessageDatabase lokiMessageDatabase; private final LokiThreadDatabase lokiThreadDatabase; private final LokiUserDatabase lokiUserDatabase; + private final LokiBackupFilesDatabase lokiBackupFilesDatabase; private final SharedSenderKeysDatabase sskDatabase; public static DatabaseFactory getInstance(Context context) { @@ -190,6 +191,10 @@ public class DatabaseFactory { return getInstance(context).lokiUserDatabase; } + public static LokiBackupFilesDatabase getLokiBackupFilesDatabase(Context context) { + return getInstance(context).lokiBackupFilesDatabase; + } + public static SharedSenderKeysDatabase getSSKDatabase(Context context) { return getInstance(context).sskDatabase; } @@ -232,6 +237,7 @@ public class DatabaseFactory { this.lokiMessageDatabase = new LokiMessageDatabase(context, databaseHelper); this.lokiThreadDatabase = new LokiThreadDatabase(context, databaseHelper); this.lokiUserDatabase = new LokiUserDatabase(context, databaseHelper); + this.lokiBackupFilesDatabase = new LokiBackupFilesDatabase(context, databaseHelper); this.sskDatabase = new SharedSenderKeysDatabase(context, databaseHelper); } diff --git a/src/org/thoughtcrime/securesms/database/LokiBackupFilesDatabase.kt b/src/org/thoughtcrime/securesms/database/LokiBackupFilesDatabase.kt new file mode 100644 index 0000000000..815e138151 --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/LokiBackupFilesDatabase.kt @@ -0,0 +1,105 @@ +package org.thoughtcrime.securesms.database + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.net.Uri +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.database.model.BackupFileRecord +import java.util.* +import kotlin.collections.ArrayList + +/** + * Keeps track of the backup files saved by the app. + * Uses [BackupFileRecord] as an entry data projection. + */ +class LokiBackupFilesDatabase(context: Context?, databaseHelper: SQLCipherOpenHelper?) + : Database(context, databaseHelper) { + + companion object { + private const val TABLE_NAME = "backup_files" + private const val COLUMN_ID = "_id" + private const val COLUMN_URI = "uri" + private const val COLUMN_FILE_SIZE = "file_size" + private const val COLUMN_TIMESTAMP = "timestamp" + + private val allColumns = arrayOf(COLUMN_ID, COLUMN_URI, COLUMN_FILE_SIZE, COLUMN_TIMESTAMP) + + @JvmStatic + val createTableCommand = """ + CREATE TABLE $TABLE_NAME ( + $COLUMN_ID INTEGER PRIMARY KEY, + $COLUMN_URI TEXT NOT NULL, + $COLUMN_FILE_SIZE INTEGER NOT NULL, + $COLUMN_TIMESTAMP INTEGER NOT NULL + ); + """.trimIndent() + + private fun mapCursorToRecord(cursor: Cursor): BackupFileRecord { + val id = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_ID)) + val uriRaw = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_URI)) + val fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_FILE_SIZE)) + val timestampRaw = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_TIMESTAMP)) + return BackupFileRecord(id, Uri.parse(uriRaw), fileSize, Date(timestampRaw)) + } + + private fun mapRecordToValues(record: BackupFileRecord): ContentValues { + val contentValues = ContentValues() + if (record.id >= 0) { contentValues.put(COLUMN_ID, record.id) } + contentValues.put(COLUMN_URI, record.uri.toString()) + contentValues.put(COLUMN_FILE_SIZE, record.fileSize) + contentValues.put(COLUMN_TIMESTAMP, record.timestamp.time) + return contentValues + } + } + + fun getBackupFiles(): List { + databaseHelper.readableDatabase.query(TABLE_NAME, allColumns, null, null, null, null, null).use { + val records = ArrayList() + while (it != null && it.moveToFirst()) { + val record = mapCursorToRecord(it) + records.add(record) + } + return records + } + } + + fun insertBackupFile(record: BackupFileRecord): Long { + val contentValues = mapRecordToValues(record) + return databaseHelper.writableDatabase.insertOrThrow(TABLE_NAME, null, contentValues) + } + + fun getLastBackupFileTime(): Date? { + // SELECT $COLUMN_TIMESTAMP FROM $TABLE_NAME ORDER BY $COLUMN_TIMESTAMP DESC LIMIT 1 + databaseHelper.readableDatabase.query( + TABLE_NAME, + arrayOf(COLUMN_TIMESTAMP), + null, null, null, null, + "$COLUMN_TIMESTAMP DESC", + "1" + ).use { + if (it !== null && it.moveToFirst()) { + return Date(it.getLong(0)) + } else { + return null + } + } + } + + fun getLastBackupFile(): BackupFileRecord? { + // SELECT * FROM $TABLE_NAME ORDER BY $COLUMN_TIMESTAMP DESC LIMIT 1 + databaseHelper.readableDatabase.query( + TABLE_NAME, + allColumns, + null, null, null, null, + "$COLUMN_TIMESTAMP DESC", + "1" + ).use { + if (it != null && it.moveToFirst()) { + return mapCursorToRecord(it) + } else { + return null + } + } + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 698910d674..9e105f45a3 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupReceiptDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.JobDatabase; +import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase; import org.thoughtcrime.securesms.database.PushDatabase; @@ -89,8 +90,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV11 = 32; private static final int lokiV12 = 33; private static final int lokiV13 = 34; + private static final int lokiV14_BACKUP_FILES = 35; - private static final int DATABASE_VERSION = lokiV13; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes + private static final int DATABASE_VERSION = lokiV14_BACKUP_FILES; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Session makes any database changes private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -161,6 +163,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand()); db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand()); db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); + db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand()); db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupRatchetTableCommand()); db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeyTableCommand()); @@ -619,6 +622,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesTable3Command()); } + if (oldVersion < lokiV14_BACKUP_FILES) { + db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand()); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/src/org/thoughtcrime/securesms/database/model/BackupFileRecord.kt b/src/org/thoughtcrime/securesms/database/model/BackupFileRecord.kt new file mode 100644 index 0000000000..83948d3530 --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/model/BackupFileRecord.kt @@ -0,0 +1,12 @@ +package org.thoughtcrime.securesms.database.model + +import android.net.Uri +import java.util.* + +/** + * Represents a record for a backup file in the [org.thoughtcrime.securesms.database.LokiBackupFilesDatabase]. + */ +data class BackupFileRecord(val id: Long, val uri: Uri, val fileSize: Long, val timestamp: Date) { + + constructor(uri: Uri, fileSize: Long, timestamp: Date): this(-1, uri, fileSize, timestamp) +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java b/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java index ba19d8e2a5..234d12c3b8 100644 --- a/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java +++ b/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java @@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.service.GenericForegroundService; -import org.thoughtcrime.securesms.util.BackupUtil; +import org.thoughtcrime.securesms.util.BackupUtilOld; import org.thoughtcrime.securesms.util.ExternalStorageUtil; import java.io.File; @@ -98,7 +98,7 @@ public class LocalBackupJob extends BaseJob { throw new IOException("Renaming temporary backup file failed!"); } - BackupUtil.deleteOldBackups(context); + BackupUtilOld.deleteOldBackups(context); } finally { GenericForegroundService.stopForegroundTask(context); } diff --git a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java index 97c14b4279..123f3262cb 100644 --- a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java @@ -5,12 +5,13 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; +import android.text.TextUtils; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.preference.EditTextPreference; import androidx.preference.Preference; -import android.text.TextUtils; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -106,8 +107,9 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { } private void setBackupSummary() { - findPreference(TextSecurePreferences.BACKUP_NOW) - .setSummary(String.format(getString(R.string.ChatsPreferenceFragment_last_backup_s), BackupUtil.getLastBackupTime(getContext(), Locale.US))); + findPreference(TextSecurePreferences.BACKUP_NOW).setSummary( + String.format(getString(R.string.ChatsPreferenceFragment_last_backup_s), + BackupUtil.getLastBackupTimeString(getContext(), Locale.getDefault()))); } private void setMediaDownloadSummaries() { diff --git a/src/org/thoughtcrime/securesms/util/BackupUtil.kt b/src/org/thoughtcrime/securesms/util/BackupUtil.kt new file mode 100644 index 0000000000..3025c2e3db --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/BackupUtil.kt @@ -0,0 +1,38 @@ +package org.thoughtcrime.securesms.util + +import android.content.Context +import network.loki.messenger.R +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.model.BackupFileRecord +import org.whispersystems.libsignal.util.ByteUtil +import java.security.SecureRandom +import java.util.* + +object BackupUtil { + + @JvmStatic + fun getLastBackupTimeString(context: Context, locale: Locale): String { + val timestamp = DatabaseFactory.getLokiBackupFilesDatabase(context).getLastBackupFileTime() + if (timestamp == null) { + return context.getString(R.string.BackupUtil_never) + } + return DateUtils.getExtendedRelativeTimeSpanString(context, locale, timestamp.time) + } + + @JvmStatic + fun getLastBackup(context: Context): BackupFileRecord? { + return DatabaseFactory.getLokiBackupFilesDatabase(context).getLastBackupFile() + } + + @JvmStatic + fun generateBackupPassphrase(): Array { + val result = arrayOfNulls(6) + val random = ByteArray(30) + SecureRandom().nextBytes(random) + for (i in 0..5) { + result[i] = String.format("%05d", ByteUtil.byteArray5ToLong(random, i * 5) % 100000) + } + @Suppress("UNCHECKED_CAST") + return result as Array + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/util/BackupUtil.java b/src/org/thoughtcrime/securesms/util/BackupUtilOld.java similarity index 96% rename from src/org/thoughtcrime/securesms/util/BackupUtil.java rename to src/org/thoughtcrime/securesms/util/BackupUtilOld.java index 5df025a3bc..adaecb8f4d 100644 --- a/src/org/thoughtcrime/securesms/util/BackupUtil.java +++ b/src/org/thoughtcrime/securesms/util/BackupUtilOld.java @@ -17,9 +17,10 @@ import java.util.Calendar; import java.util.Locale; //TODO AC: Needs to be refactored to use Storage Access Framework or Media Store API. -public class BackupUtil { +/** @deprecated in favor of {@link BackupUtil} */ +public class BackupUtilOld { - private static final String TAG = BackupUtil.class.getSimpleName(); + private static final String TAG = BackupUtilOld.class.getSimpleName(); public static @NonNull String getLastBackupTime(@NonNull Context context, @NonNull Locale locale) { try {