From 5a5702302f47b5713096a3c79b4090dafa3f35ac Mon Sep 17 00:00:00 2001 From: Anton Chekulaev Date: Fri, 11 Sep 2020 16:29:08 +1000 Subject: [PATCH 01/13] 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 { From 019b47b18f7cc5e58806622fe3b4ea2502e1b08b Mon Sep 17 00:00:00 2001 From: Anton Chekulaev Date: Mon, 14 Sep 2020 23:33:44 +1000 Subject: [PATCH 02/13] New backup util and backup dir selector. --- .../securesms/backup/BackupDialog.java | 25 ++- .../securesms/backup/BackupPassphrase.java | 2 +- .../securesms/backup/FullBackupExporter.java | 94 ++++---- .../database/LokiBackupFilesDatabase.kt | 20 +- .../securesms/jobs/LocalBackupJob.java | 49 +---- .../preferences/ChatsPreferenceFragment.java | 15 +- .../securesms/profiles/AvatarHelper.java | 6 +- .../thoughtcrime/securesms/util/BackupUtil.kt | 208 ++++++++++++++++++ .../securesms/util/BackupUtilOld.java | 124 ++--------- .../securesms/util/ContextProvider.kt | 35 +++ .../securesms/util/TextSecurePreferences.java | 9 + 11 files changed, 378 insertions(+), 209 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/util/ContextProvider.kt diff --git a/src/org/thoughtcrime/securesms/backup/BackupDialog.java b/src/org/thoughtcrime/securesms/backup/BackupDialog.java index 7f5a68f597..053aec7673 100644 --- a/src/org/thoughtcrime/securesms/backup/BackupDialog.java +++ b/src/org/thoughtcrime/securesms/backup/BackupDialog.java @@ -6,22 +6,28 @@ import android.content.ClipboardManager; import android.content.Context; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; + import android.widget.Button; import android.widget.CheckBox; import android.widget.TextView; import android.widget.Toast; import network.loki.messenger.R; + import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; import org.thoughtcrime.securesms.service.LocalBackupListener; +import org.thoughtcrime.securesms.util.BackupDirSelector; import org.thoughtcrime.securesms.util.BackupUtil; -import org.thoughtcrime.securesms.util.BackupUtilOld; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; public class BackupDialog { - public static void showEnableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) { + public static void showEnableBackupDialog( + @NonNull Context context, + @NonNull SwitchPreferenceCompat preference, + @NonNull BackupDirSelector backupDirSelector) { + String[] password = BackupUtil.generateBackupPassphrase(); AlertDialog dialog = new AlertDialog.Builder(context) .setTitle(R.string.BackupDialog_enable_local_backups) @@ -35,12 +41,14 @@ public class BackupDialog { button.setOnClickListener(v -> { CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check); if (confirmationCheckBox.isChecked()) { - BackupPassphrase.set(context, Util.join(password, " ")); - TextSecurePreferences.setBackupEnabled(context, true); - LocalBackupListener.schedule(context); + backupDirSelector.selectBackupDir(true, uri -> { + BackupPassphrase.set(context, Util.join(password, " ")); + TextSecurePreferences.setBackupEnabled(context, true); + LocalBackupListener.schedule(context); - preference.setChecked(true); - created.dismiss(); + preference.setChecked(true); + created.dismiss(); + }); } else { Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show(); } @@ -78,7 +86,8 @@ public class BackupDialog { .setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> { BackupPassphrase.set(context, null); TextSecurePreferences.setBackupEnabled(context, false); - BackupUtilOld.deleteAllBackups(context); + BackupUtil.deleteAllBackupFiles(context); + BackupUtil.setBackupDirUri(context, null); preference.setChecked(false); }) .create() diff --git a/src/org/thoughtcrime/securesms/backup/BackupPassphrase.java b/src/org/thoughtcrime/securesms/backup/BackupPassphrase.java index 0f24f378a0..aede7c441d 100644 --- a/src/org/thoughtcrime/securesms/backup/BackupPassphrase.java +++ b/src/org/thoughtcrime/securesms/backup/BackupPassphrase.java @@ -16,7 +16,7 @@ public class BackupPassphrase { private static final String TAG = BackupPassphrase.class.getSimpleName(); - public static String get(@NonNull Context context) { + public static @Nullable String get(@NonNull Context context) { String passphrase = TextSecurePreferences.getBackupPassphrase(context); String encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context); diff --git a/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java b/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java index bd0864f065..8d361b89c9 100644 --- a/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java +++ b/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java @@ -5,6 +5,8 @@ import android.content.Context; import android.database.Cursor; import androidx.annotation.NonNull; import androidx.annotation.Nullable; + +import android.net.Uri; import android.text.TextUtils; import com.annimon.stream.function.Consumer; @@ -36,9 +38,10 @@ import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.kdf.HKDFv3; import org.whispersystems.libsignal.util.ByteUtil; +import java.io.Closeable; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; +import java.io.Flushable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -64,49 +67,54 @@ public class FullBackupExporter extends FullBackupBase { public static void export(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase input, - @NonNull File output, + @NonNull Uri fileUri, @NonNull String passphrase) throws IOException { - BackupFrameOutputStream outputStream = new BackupFrameOutputStream(output, passphrase); - outputStream.writeDatabaseVersion(input.getVersion()); + OutputStream baseOutputStream = context.getContentResolver().openOutputStream(fileUri); + if (baseOutputStream == null) { + throw new IOException("Cannot open an output stream for the file URI: " + fileUri.toString()); + } - List tables = exportSchema(input, outputStream); - int count = 0; + try (BackupFrameOutputStream outputStream = new BackupFrameOutputStream(baseOutputStream, passphrase)) { + outputStream.writeDatabaseVersion(input.getVersion()); - for (String table : tables) { - if (table.equals(SmsDatabase.TABLE_NAME) || table.equals(MmsDatabase.TABLE_NAME)) { - count = exportTable(table, input, outputStream, cursor -> cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0, null, count); - } else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) { - count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count); - } else if (table.equals(AttachmentDatabase.TABLE_NAME)) { - count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), cursor -> exportAttachment(attachmentSecret, cursor, outputStream), count); - } else if (table.equals(StickerDatabase.TABLE_NAME)) { - count = exportTable(table, input, outputStream, cursor -> true, cursor -> exportSticker(attachmentSecret, cursor, outputStream), count); - } else if (!table.equals(SignedPreKeyDatabase.TABLE_NAME) && - !table.equals(OneTimePreKeyDatabase.TABLE_NAME) && - !table.equals(SessionDatabase.TABLE_NAME) && - !table.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME) && - !table.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME) && - !table.startsWith("sqlite_")) - { - count = exportTable(table, input, outputStream, null, null, count); + List tables = exportSchema(input, outputStream); + int count = 0; + + for (String table : tables) { + if (table.equals(SmsDatabase.TABLE_NAME) || table.equals(MmsDatabase.TABLE_NAME)) { + count = exportTable(table, input, outputStream, cursor -> cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0, null, count); + } else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) { + count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count); + } else if (table.equals(AttachmentDatabase.TABLE_NAME)) { + count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), cursor -> exportAttachment(attachmentSecret, cursor, outputStream), count); + } else if (table.equals(StickerDatabase.TABLE_NAME)) { + count = exportTable(table, input, outputStream, cursor -> true, cursor -> exportSticker(attachmentSecret, cursor, outputStream), count); + } else if (!table.equals(SignedPreKeyDatabase.TABLE_NAME) && + !table.equals(OneTimePreKeyDatabase.TABLE_NAME) && + !table.equals(SessionDatabase.TABLE_NAME) && + !table.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME) && + !table.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME) && + !table.startsWith("sqlite_")) + { + count = exportTable(table, input, outputStream, null, null, count); + } } - } - for (BackupProtos.SharedPreference preference : IdentityKeyUtil.getBackupRecord(context)) { - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); - outputStream.write(preference); - } + for (BackupProtos.SharedPreference preference : IdentityKeyUtil.getBackupRecord(context)) { + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); + outputStream.write(preference); + } - for (File avatar : AvatarHelper.getAvatarFiles(context)) { - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); - outputStream.write(avatar.getName(), new FileInputStream(avatar), avatar.length()); - } + for (File avatar : AvatarHelper.getAvatarFiles(context)) { + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); + outputStream.write(avatar.getName(), new FileInputStream(avatar), avatar.length()); + } - outputStream.writeEnd(); - outputStream.close(); - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count)); + outputStream.writeEnd(); + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count)); + } } private static List exportSchema(@NonNull SQLiteDatabase input, @NonNull BackupFrameOutputStream outputStream) @@ -228,8 +236,9 @@ public class FullBackupExporter extends FullBackupBase { byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM)); if (!TextUtils.isEmpty(data) && size > 0) { - InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0); - outputStream.writeSticker(rowId, inputStream, size); + try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) { + outputStream.writeSticker(rowId, inputStream, size); + } } } catch (IOException e) { Log.w(TAG, e); @@ -268,7 +277,7 @@ public class FullBackupExporter extends FullBackupBase { } - private static class BackupFrameOutputStream extends BackupStream { + private static class BackupFrameOutputStream extends BackupStream implements Closeable, Flushable { private final OutputStream outputStream; private final Cipher cipher; @@ -280,7 +289,7 @@ public class FullBackupExporter extends FullBackupBase { private byte[] iv; private int counter; - private BackupFrameOutputStream(@NonNull File output, @NonNull String passphrase) throws IOException { + private BackupFrameOutputStream(@NonNull OutputStream outputStream, @NonNull String passphrase) throws IOException { try { byte[] salt = Util.getSecretBytes(32); byte[] key = getBackupKey(passphrase, salt); @@ -292,7 +301,7 @@ public class FullBackupExporter extends FullBackupBase { this.cipher = Cipher.getInstance("AES/CTR/NoPadding"); this.mac = Mac.getInstance("HmacSHA256"); - this.outputStream = new FileOutputStream(output); + this.outputStream = outputStream; this.iv = Util.getSecretBytes(16); this.counter = Conversions.byteArrayToInt(iv); @@ -408,7 +417,12 @@ public class FullBackupExporter extends FullBackupBase { } } + @Override + public void flush() throws IOException { + outputStream.flush(); + } + @Override public void close() throws IOException { outputStream.close(); } diff --git a/src/org/thoughtcrime/securesms/database/LokiBackupFilesDatabase.kt b/src/org/thoughtcrime/securesms/database/LokiBackupFilesDatabase.kt index 815e138151..d68b61a6f1 100644 --- a/src/org/thoughtcrime/securesms/database/LokiBackupFilesDatabase.kt +++ b/src/org/thoughtcrime/securesms/database/LokiBackupFilesDatabase.kt @@ -4,8 +4,10 @@ import android.content.ContentValues import android.content.Context import android.database.Cursor import android.net.Uri +import androidx.annotation.NonNull import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.model.BackupFileRecord +import java.lang.IllegalArgumentException import java.util.* import kotlin.collections.ArrayList @@ -56,7 +58,7 @@ class LokiBackupFilesDatabase(context: Context?, databaseHelper: SQLCipherOpenHe fun getBackupFiles(): List { databaseHelper.readableDatabase.query(TABLE_NAME, allColumns, null, null, null, null, null).use { val records = ArrayList() - while (it != null && it.moveToFirst()) { + while (it != null && it.moveToNext()) { val record = mapCursorToRecord(it) records.add(record) } @@ -64,9 +66,10 @@ class LokiBackupFilesDatabase(context: Context?, databaseHelper: SQLCipherOpenHe } } - fun insertBackupFile(record: BackupFileRecord): Long { + fun insertBackupFile(record: BackupFileRecord): BackupFileRecord { val contentValues = mapRecordToValues(record) - return databaseHelper.writableDatabase.insertOrThrow(TABLE_NAME, null, contentValues) + val id = databaseHelper.writableDatabase.insertOrThrow(TABLE_NAME, null, contentValues) + return BackupFileRecord(id, record.uri, record.fileSize, record.timestamp) } fun getLastBackupFileTime(): Date? { @@ -102,4 +105,15 @@ class LokiBackupFilesDatabase(context: Context?, databaseHelper: SQLCipherOpenHe } } } + + fun deleteBackupFile(record: BackupFileRecord): Boolean { + return deleteBackupFile(record.id) + } + + fun deleteBackupFile(id: Long): Boolean { + if (id < 0) { + throw IllegalArgumentException("ID must be zero or a positive number.") + } + return databaseHelper.writableDatabase.delete(TABLE_NAME, "$COLUMN_ID = $id", null) > 0 + } } \ 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 234d12c3b8..938a7ebffd 100644 --- a/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java +++ b/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java @@ -1,32 +1,22 @@ package org.thoughtcrime.securesms.jobs; -import android.Manifest; import androidx.annotation.NonNull; -import org.thoughtcrime.securesms.backup.BackupPassphrase; -import org.thoughtcrime.securesms.backup.FullBackupExporter; -import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; -import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.NoExternalStorageException; +import org.thoughtcrime.securesms.database.model.BackupFileRecord; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; 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.BackupUtilOld; -import org.thoughtcrime.securesms.util.ExternalStorageUtil; +import org.thoughtcrime.securesms.util.BackupUtil; -import java.io.File; import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; +import java.util.Collections; import network.loki.messenger.R; -//TODO AC: Needs to be refactored to use Storage Access Framework or Media Store API. public class LocalBackupJob extends BaseJob { public static final String KEY = "LocalBackupJob"; @@ -59,10 +49,6 @@ public class LocalBackupJob extends BaseJob { public void onRun() throws NoExternalStorageException, IOException { Log.i(TAG, "Executing backup job..."); - if (!Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - throw new IOException("No external storage permission!"); - } - GenericForegroundService.startForegroundTask(context, context.getString(R.string.LocalBackupJob_creating_backup), NotificationChannels.BACKUPS, @@ -71,34 +57,9 @@ public class LocalBackupJob extends BaseJob { // TODO: Maybe create a new backup icon like ic_signal_backup? try { - String backupPassword = BackupPassphrase.get(context); - File backupDirectory = ExternalStorageUtil.getBackupDir(context); - String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(new Date()); - String fileName = String.format("session-%s.backup", timestamp); - File backupFile = new File(backupDirectory, fileName); + BackupFileRecord record = BackupUtil.createBackupFile(context); + BackupUtil.deleteAllBackupFiles(context, Collections.singletonList(record)); - if (backupFile.exists()) { - throw new IOException("Backup file already exists?"); - } - - if (backupPassword == null) { - throw new IOException("Backup password is null"); - } - - File tempFile = File.createTempFile("backup", "tmp", ExternalStorageUtil.getCacheDir(context)); - - FullBackupExporter.export(context, - AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(), - DatabaseFactory.getBackupDatabase(context), - tempFile, - backupPassword); - - if (!tempFile.renameTo(backupFile)) { - tempFile.delete(); - throw new IOException("Renaming temporary backup file failed!"); - } - - 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 123f3262cb..883e81740f 100644 --- a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java @@ -4,6 +4,7 @@ import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; @@ -24,7 +25,9 @@ import org.thoughtcrime.securesms.jobs.LocalBackupJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.preferences.widgets.ProgressPreference; +import org.thoughtcrime.securesms.util.BackupDirSelector; import org.thoughtcrime.securesms.util.BackupUtil; +import org.thoughtcrime.securesms.util.FragmentContextProvider; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Trimmer; @@ -38,6 +41,8 @@ import network.loki.messenger.R; public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { private static final String TAG = ChatsPreferenceFragment.class.getSimpleName(); + private BackupDirSelector backupDirSelector; + @Override public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); @@ -65,6 +70,8 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { // initializeListSummary((ListPreference) findPreference(TextSecurePreferences.MESSAGE_BODY_TEXT_SIZE_PREF)); + backupDirSelector = new BackupDirSelector(this); + EventBus.getDefault().register(this); } @@ -91,6 +98,12 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + backupDirSelector.onActivityResult(requestCode, resultCode, data); + } + @Subscribe(threadMode = ThreadMode.MAIN) public void onEvent(BackupEvent event) { ProgressPreference preference = (ProgressPreference)findPreference(TextSecurePreferences.BACKUP_NOW); @@ -143,7 +156,7 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) .onAllGranted(() -> { if (!((SwitchPreferenceCompat)preference).isChecked()) { - BackupDialog.showEnableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference); + BackupDialog.showEnableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference, backupDirSelector); } else { BackupDialog.showDisableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference); } diff --git a/src/org/thoughtcrime/securesms/profiles/AvatarHelper.java b/src/org/thoughtcrime/securesms/profiles/AvatarHelper.java index 572ae4ab6d..5a082f532b 100644 --- a/src/org/thoughtcrime/securesms/profiles/AvatarHelper.java +++ b/src/org/thoughtcrime/securesms/profiles/AvatarHelper.java @@ -52,9 +52,9 @@ public class AvatarHelper { if (data == null) { delete(context, address); } else { - FileOutputStream out = new FileOutputStream(getAvatarFile(context, address)); - out.write(data); - out.close(); + try (FileOutputStream out = new FileOutputStream(getAvatarFile(context, address))) { + out.write(data); + } } } diff --git a/src/org/thoughtcrime/securesms/util/BackupUtil.kt b/src/org/thoughtcrime/securesms/util/BackupUtil.kt index 3025c2e3db..94a5b1eaf1 100644 --- a/src/org/thoughtcrime/securesms/util/BackupUtil.kt +++ b/src/org/thoughtcrime/securesms/util/BackupUtil.kt @@ -1,14 +1,31 @@ package org.thoughtcrime.securesms.util +import android.app.Activity import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.DocumentsContract +import android.util.Log +import android.widget.Toast +import androidx.annotation.WorkerThread +import androidx.documentfile.provider.DocumentFile +import androidx.fragment.app.Fragment import network.loki.messenger.R +import org.thoughtcrime.securesms.backup.BackupPassphrase +import org.thoughtcrime.securesms.backup.FullBackupExporter +import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.BackupFileRecord import org.whispersystems.libsignal.util.ByteUtil +import java.io.IOException import java.security.SecureRandom +import java.text.SimpleDateFormat import java.util.* object BackupUtil { + private const val TAG = "BackupUtil" @JvmStatic fun getLastBackupTimeString(context: Context, locale: Locale): String { @@ -35,4 +52,195 @@ object BackupUtil { @Suppress("UNCHECKED_CAST") return result as Array } + + @JvmStatic + fun validateDirAccess(context: Context, dirUri: Uri): Boolean { + val hasWritePermission = context.contentResolver.persistedUriPermissions.any { + it.isWritePermission && it.uri == dirUri + } + if (!hasWritePermission) return false + + val document = DocumentFile.fromTreeUri(context, dirUri) + if (document == null || !document.exists()) { + return false + } + + return true + } + + @JvmStatic + fun getBackupDirUri(context: Context): Uri? { + val dirUriString = TextSecurePreferences.getBackupSaveDir(context) ?: return null + return Uri.parse(dirUriString) + } + + @JvmStatic + fun setBackupDirUri(context: Context, uriString: String?) { + TextSecurePreferences.setBackupSaveDir(context, uriString) + } + + /** + * @return The selected backup directory if it's valid (exists, is writable). + */ + @JvmStatic + fun getSelectedBackupDirIfValid(context: Context): Uri? { + val dirUri = getBackupDirUri(context) + + if (dirUri == null) { + Log.v(TAG, "The backup dir wasn't selected yet.") + return null + } + if (!validateDirAccess(context, dirUri)) { + Log.v(TAG, "Cannot validate the access to the dir $dirUri.") + return null + } + + return dirUri; + } + + @JvmStatic + @WorkerThread + @Throws(IOException::class) + fun createBackupFile(context: Context): BackupFileRecord { + val backupPassword = BackupPassphrase.get(context) + ?: throw IOException("Backup password is null") + + val dirUri = getSelectedBackupDirIfValid(context) + ?: throw IOException("Backup save directory is not selected or invalid") + + val date = Date() + val timestamp = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(date) + val fileName = String.format("session-%s.backup", timestamp) + + val fileUri = DocumentsContract.createDocument( + context.contentResolver, + DocumentFile.fromTreeUri(context, dirUri)!!.uri, + "application/x-binary", + fileName) + + if (fileUri == null) { + Toast.makeText(context, "Cannot create writable file in the dir $dirUri", Toast.LENGTH_LONG).show() + throw IOException("Cannot create writable file in the dir $dirUri") + } + + FullBackupExporter.export(context, + AttachmentSecretProvider.getInstance(context).orCreateAttachmentSecret, + DatabaseFactory.getBackupDatabase(context), + fileUri, + backupPassword) + + //TODO Use real file size. + val record = DatabaseFactory.getLokiBackupFilesDatabase(context) + .insertBackupFile(BackupFileRecord(fileUri, -1, date)) + + Log.v(TAG, "Backup file was created: $fileUri") + + return record + } + + @JvmStatic + @JvmOverloads + fun deleteAllBackupFiles(context: Context, except: Collection? = null) { + val db = DatabaseFactory.getLokiBackupFilesDatabase(context) + db.getBackupFiles().forEach { record -> + if (except != null && except.contains(record)) return@forEach + + // Try to delete the related file. The operation may fail in many cases + // (the user moved/deleted the file, revoked the write permission, etc), so that's OK. + try { + val result = DocumentsContract.deleteDocument(context.contentResolver, record.uri) + if (!result) { + Log.w(TAG, "Failed to delete backup file: ${record.uri}") + } + } catch (e: Exception) { + Log.w(TAG, "Failed to delete backup file: ${record.uri}", e) + } + + db.deleteBackupFile(record) + + Log.v(TAG, "Backup file was deleted: ${record.uri}") + } + } +} + +/** + * An utility class to help perform backup directory selection requests. + * + * An instance of this class should be created per an [Activity] or [Fragment] + * and [onActivityResult] should be called appropriately. + */ +class BackupDirSelector(private val contextProvider: ContextProvider) { + + companion object { + private const val REQUEST_CODE_SAVE_DIR = 7844 + } + + private val context: Context get() = contextProvider.getContext() + + private var listener: Listener? = null + + constructor(activity: Activity) : + this(ActivityContextProvider(activity)) + + constructor(fragment: Fragment) : + this(FragmentContextProvider(fragment)) + + /** + * Performs ACTION_OPEN_DOCUMENT_TREE intent to select backup directory URI. + * If the directory is already selected and valid, the request will be skipped. + * @param force if true, the previous selection is ignored and the user is requested to select another directory. + * @param onSelectedListener an optional action to perform once the directory is selected. + */ + fun selectBackupDir(force: Boolean, onSelectedListener: Listener? = null) { + if (!force) { + val dirUri = BackupUtil.getSelectedBackupDirIfValid(context) + if (dirUri != null && onSelectedListener != null) { + onSelectedListener.onBackupDirSelected(dirUri) + } + return + } + + // Let user pick the dir. + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + + // Request read/write permission grant for the dir. + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + + // Set the default dir. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val dirUri = BackupUtil.getBackupDirUri(context) + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, dirUri + ?: Uri.fromFile(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS))) + } + + if (onSelectedListener != null) { + this.listener = onSelectedListener + } + + contextProvider.startActivityForResult(intent, REQUEST_CODE_SAVE_DIR) + } + + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode != REQUEST_CODE_SAVE_DIR) return + + if (resultCode == Activity.RESULT_OK && data != null && data.data != null) { + // Acquire persistent access permissions for the file selected. + val persistentFlags: Int = data.flags and + (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + context.contentResolver.takePersistableUriPermission(data.data!!, persistentFlags) + + BackupUtil.setBackupDirUri(context, data.dataString) + + listener?.onBackupDirSelected(data.data!!) + } + + listener = null + } + + @FunctionalInterface + interface Listener { + fun onBackupDirSelected(uri: Uri) + } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/util/BackupUtilOld.java b/src/org/thoughtcrime/securesms/util/BackupUtilOld.java index adaecb8f4d..31aad6b8bd 100644 --- a/src/org/thoughtcrime/securesms/util/BackupUtilOld.java +++ b/src/org/thoughtcrime/securesms/util/BackupUtilOld.java @@ -4,6 +4,8 @@ package org.thoughtcrime.securesms.util; import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.database.model.BackupFileRecord; import org.thoughtcrime.securesms.logging.Log; import network.loki.messenger.R; @@ -16,125 +18,29 @@ import java.util.Arrays; import java.util.Calendar; import java.util.Locale; -//TODO AC: Needs to be refactored to use Storage Access Framework or Media Store API. +//TODO AC: Delete this class when its functionality is +// fully replaced by the BackupUtil.kt and related classes. /** @deprecated in favor of {@link BackupUtil} */ public class BackupUtilOld { private static final String TAG = BackupUtilOld.class.getSimpleName(); - public static @NonNull String getLastBackupTime(@NonNull Context context, @NonNull Locale locale) { - try { - BackupInfo backup = getLatestBackup(context); - - if (backup == null) return context.getString(R.string.BackupUtil_never); - else return DateUtils.getExtendedRelativeTimeSpanString(context, locale, backup.getTimestamp()); - } catch (NoExternalStorageException e) { - Log.w(TAG, e); - return context.getString(R.string.BackupUtil_unknown); - } - } - + /** + * @deprecated this method exists only for the backward compatibility with the legacy Signal backup code. + * Use {@link BackupUtil} if possible. + */ public static @Nullable BackupInfo getLatestBackup(Context context) throws NoExternalStorageException { - File backupDirectory = ExternalStorageUtil.getBackupDir(context); - File[] backups = backupDirectory.listFiles(); - BackupInfo latestBackup = null; + BackupFileRecord backup = BackupUtil.getLastBackup(context); + if (backup == null) return null; - for (File backup : backups) { - long backupTimestamp = getBackupTimestamp(backup); - if (latestBackup == null || (backupTimestamp != -1 && backupTimestamp > latestBackup.getTimestamp())) { - latestBackup = new BackupInfo(backupTimestamp, backup.length(), backup); - } - } - - return latestBackup; - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - public static void deleteAllBackups(Context context) { - try { - File backupDirectory = ExternalStorageUtil.getBackupDir(context); - File[] backups = backupDirectory.listFiles(); - - for (File backup : backups) { - if (backup.isFile()) backup.delete(); - } - } catch (NoExternalStorageException e) { - Log.w(TAG, e); - } - } - - public static void deleteOldBackups(Context context) { - try { - File backupDirectory = ExternalStorageUtil.getBackupDir(context); - File[] backups = backupDirectory.listFiles(); - - if (backups != null && backups.length > 2) { - Arrays.sort(backups, (left, right) -> { - long leftTimestamp = getBackupTimestamp(left); - long rightTimestamp = getBackupTimestamp(right); - - if (leftTimestamp == -1 && rightTimestamp == -1) return 0; - else if (leftTimestamp == -1) return 1; - else if (rightTimestamp == -1) return -1; - - return (int)(rightTimestamp - leftTimestamp); - }); - - for (int i=2;i Date: Wed, 16 Sep 2020 16:30:39 +1000 Subject: [PATCH 03/13] Cleanup backup permissions. --- .../preferences/ChatsPreferenceFragment.java | 30 ++++++------------- .../thoughtcrime/securesms/util/BackupUtil.kt | 11 +++---- .../securesms/util/SaveAttachmentTask.kt | 2 ++ 3 files changed, 15 insertions(+), 28 deletions(-) diff --git a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java index 883e81740f..1814e90fa9 100644 --- a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java @@ -152,17 +152,11 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { private class BackupClickListener implements Preference.OnPreferenceClickListener { @Override public boolean onPreferenceClick(Preference preference) { - Permissions.with(ChatsPreferenceFragment.this) - .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) - .onAllGranted(() -> { - if (!((SwitchPreferenceCompat)preference).isChecked()) { - BackupDialog.showEnableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference, backupDirSelector); - } else { - BackupDialog.showDisableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference); - } - }) - .withPermanentDenialDialog(getString(R.string.ChatsPreferenceFragment_signal_requires_external_storage_permission_in_order_to_create_backups)) - .execute(); + if (!((SwitchPreferenceCompat)preference).isChecked()) { + BackupDialog.showEnableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference, backupDirSelector); + } else { + BackupDialog.showDisableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference); + } return true; } @@ -172,16 +166,10 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { @SuppressLint("StaticFieldLeak") @Override public boolean onPreferenceClick(Preference preference) { - Permissions.with(ChatsPreferenceFragment.this) - .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) - .onAllGranted(() -> { - Log.i(TAG, "Queing backup..."); - ApplicationContext.getInstance(getContext()) - .getJobManager() - .add(new LocalBackupJob()); - }) - .withPermanentDenialDialog(getString(R.string.ChatsPreferenceFragment_signal_requires_external_storage_permission_in_order_to_create_backups)) - .execute(); + Log.i(TAG, "Queuing backup..."); + ApplicationContext.getInstance(getContext()) + .getJobManager() + .add(new LocalBackupJob()); return true; } diff --git a/src/org/thoughtcrime/securesms/util/BackupUtil.kt b/src/org/thoughtcrime/securesms/util/BackupUtil.kt index 94a5b1eaf1..2a70b72874 100644 --- a/src/org/thoughtcrime/securesms/util/BackupUtil.kt +++ b/src/org/thoughtcrime/securesms/util/BackupUtil.kt @@ -23,6 +23,7 @@ import java.io.IOException import java.security.SecureRandom import java.text.SimpleDateFormat import java.util.* +import kotlin.jvm.Throws object BackupUtil { private const val TAG = "BackupUtil" @@ -43,14 +44,10 @@ object BackupUtil { @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) + val random = ByteArray(30).also { SecureRandom().nextBytes(it) } + return Array(6) {i -> + String.format("%05d", ByteUtil.byteArray5ToLong(random, i * 5) % 100000) } - @Suppress("UNCHECKED_CAST") - return result as Array } @JvmStatic diff --git a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt index d4dc58eb7b..1c57af91bc 100644 --- a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt +++ b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.kt @@ -18,9 +18,11 @@ import java.io.File import java.io.IOException import java.lang.ref.WeakReference import java.text.SimpleDateFormat +import kotlin.jvm.Throws /** * Saves attachment files to an external storage using [MediaStore] API. + * Requires [android.Manifest.permission.WRITE_EXTERNAL_STORAGE] on API 28 and below. */ class SaveAttachmentTask : ProgressDialogAsyncTask> { From e0892853a79a0d52c64e124a13c0380ae083079e Mon Sep 17 00:00:00 2001 From: Anton Chekulaev Date: Wed, 16 Sep 2020 18:58:26 +1000 Subject: [PATCH 04/13] Backup dialog cleanup. --- res/values/strings.xml | 1 + .../securesms/backup/BackupDialog.java | 40 +++++++++++-------- .../preferences/ChatsPreferenceFragment.java | 1 - .../thoughtcrime/securesms/util/BackupUtil.kt | 40 +++++++++++++++++++ .../securesms/util/TextSecurePreferences.java | 2 +- 5 files changed, 66 insertions(+), 18 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 70b662aba8..d801f1dfa7 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1499,6 +1499,7 @@ Disable and delete all local backups? Delete backups Copied to clipboard + Failed to activate backups. Please try again or contact support. Session requires external storage permission in order to create backups, but it has been permanently denied. Please continue to app settings, select \"Permissions\" and enable \"Storage\". Last backup: %s In progress diff --git a/src/org/thoughtcrime/securesms/backup/BackupDialog.java b/src/org/thoughtcrime/securesms/backup/BackupDialog.java index 053aec7673..c7ca55ad35 100644 --- a/src/org/thoughtcrime/securesms/backup/BackupDialog.java +++ b/src/org/thoughtcrime/securesms/backup/BackupDialog.java @@ -4,32 +4,36 @@ package org.thoughtcrime.securesms.backup; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; - import android.widget.Button; import android.widget.CheckBox; import android.widget.TextView; import android.widget.Toast; -import network.loki.messenger.R; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; -import org.thoughtcrime.securesms.service.LocalBackupListener; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.util.BackupDirSelector; import org.thoughtcrime.securesms.util.BackupUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; +import java.io.IOException; + +import network.loki.messenger.R; + public class BackupDialog { + private static final String TAG = "BackupDialog"; public static void showEnableBackupDialog( @NonNull Context context, @NonNull SwitchPreferenceCompat preference, @NonNull BackupDirSelector backupDirSelector) { - String[] password = BackupUtil.generateBackupPassphrase(); - AlertDialog dialog = new AlertDialog.Builder(context) + String[] password = BackupUtil.generateBackupPassphrase(); + String passwordSt = Util.join(password, " "); + + AlertDialog dialog = new AlertDialog.Builder(context) .setTitle(R.string.BackupDialog_enable_local_backups) .setView(R.layout.backup_enable_dialog) .setPositiveButton(R.string.BackupDialog_enable_backups, null) @@ -42,9 +46,16 @@ public class BackupDialog { CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check); if (confirmationCheckBox.isChecked()) { backupDirSelector.selectBackupDir(true, uri -> { - BackupPassphrase.set(context, Util.join(password, " ")); - TextSecurePreferences.setBackupEnabled(context, true); - LocalBackupListener.schedule(context); + try { + BackupUtil.enableBackups(context, passwordSt); + } catch (IOException e) { + Log.e(TAG, "Failed to activate backups.", e); + Toast.makeText(context, + context.getString(R.string.BackupDialog_activation_error), + Toast.LENGTH_LONG) + .show(); + return; + } preference.setChecked(true); created.dismiss(); @@ -71,7 +82,7 @@ public class BackupDialog { textView.setOnClickListener(v -> checkBox.toggle()); dialog.findViewById(R.id.number_table).setOnClickListener(v -> { - ((ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", Util.join(password, " "))); + ((ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", passwordSt)); Toast.makeText(context, R.string.BackupDialog_copied_to_clipboard, Toast.LENGTH_LONG).show(); }); @@ -84,10 +95,7 @@ public class BackupDialog { .setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> { - BackupPassphrase.set(context, null); - TextSecurePreferences.setBackupEnabled(context, false); - BackupUtil.deleteAllBackupFiles(context); - BackupUtil.setBackupDirUri(context, null); + BackupUtil.disableBackups(context, true); preference.setChecked(false); }) .create() diff --git a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java index 1814e90fa9..5d12ab3eb9 100644 --- a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java @@ -170,7 +170,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { ApplicationContext.getInstance(getContext()) .getJobManager() .add(new LocalBackupJob()); - return true; } } diff --git a/src/org/thoughtcrime/securesms/util/BackupUtil.kt b/src/org/thoughtcrime/securesms/util/BackupUtil.kt index 2a70b72874..c23dbc772c 100644 --- a/src/org/thoughtcrime/securesms/util/BackupUtil.kt +++ b/src/org/thoughtcrime/securesms/util/BackupUtil.kt @@ -18,8 +18,10 @@ import org.thoughtcrime.securesms.backup.FullBackupExporter import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.BackupFileRecord +import org.thoughtcrime.securesms.service.LocalBackupListener import org.whispersystems.libsignal.util.ByteUtil import java.io.IOException +import java.lang.IllegalStateException import java.security.SecureRandom import java.text.SimpleDateFormat import java.util.* @@ -28,6 +30,44 @@ import kotlin.jvm.Throws object BackupUtil { private const val TAG = "BackupUtil" + /** + * Set app-wide configuration to enable the backups and schedule them. + * + * Make sure that the backup dir is selected prior activating the backup. + * Use [BackupDirSelector] or [setBackupDirUri] manually. + */ + @JvmStatic + @Throws(IOException::class) + fun enableBackups(context: Context, password: String) { + val backupDir = getBackupDirUri(context) + if (backupDir == null || validateDirAccess(context, backupDir)) { + throw IOException("Backup dir is not set or invalid.") + } + + BackupPassphrase.set(context, password) + TextSecurePreferences.setBackupEnabled(context, true) + LocalBackupListener.schedule(context) + } + + /** + * Set app-wide configuration to disable the backups. + * + * This call resets the backup dir value. + * Make sure to call [setBackupDirUri] prior next call to [enableBackups]. + * + * @param deleteBackupFiles if true, deletes all the previously created backup files + * (if the app has access to them) + */ + @JvmStatic + fun disableBackups(context: Context, deleteBackupFiles: Boolean) { + BackupPassphrase.set(context, null) + TextSecurePreferences.setBackupEnabled(context, false) + if (deleteBackupFiles) { + deleteAllBackupFiles(context) + } + setBackupDirUri(context, null) + } + @JvmStatic fun getLastBackupTimeString(context: Context, locale: Locale): String { val timestamp = DatabaseFactory.getLokiBackupFilesDatabase(context).getLastBackupFileTime() diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index 341cc25ccc..0390f5c580 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -141,7 +141,7 @@ public class TextSecurePreferences { private static final String ACTIVE_SIGNED_PRE_KEY_ID = "pref_active_signed_pre_key_id"; private static final String NEXT_SIGNED_PRE_KEY_ID = "pref_next_signed_pre_key_id"; - public static final String BACKUP_ENABLED = "pref_backup_enabled"; + public static final String BACKUP_ENABLED = "pref_backup_enabled_v2"; private static final String BACKUP_PASSPHRASE = "pref_backup_passphrase"; private static final String ENCRYPTED_BACKUP_PASSPHRASE = "pref_encrypted_backup_passphrase"; private static final String BACKUP_TIME = "pref_backup_next_time"; From bf666f0296076988bb8e8e10b099233db7b6df59 Mon Sep 17 00:00:00 2001 From: Anton Chekulaev Date: Wed, 16 Sep 2020 19:16:47 +1000 Subject: [PATCH 05/13] Backup pref related fixes. --- res/xml/preferences_chats.xml | 4 ++-- src/org/thoughtcrime/securesms/util/BackupUtil.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/xml/preferences_chats.xml b/res/xml/preferences_chats.xml index 9d0cac23f9..4b1d69e8f0 100644 --- a/res/xml/preferences_chats.xml +++ b/res/xml/preferences_chats.xml @@ -94,7 +94,7 @@ @@ -102,7 +102,7 @@ android:key="pref_backup_create" android:title="@string/preferences_chats__create_backup" android:persistent="false" - android:dependency="pref_backup_enabled" + android:dependency="pref_backup_enabled_v2" tools:summary="Last backup: 3 days ago" /> diff --git a/src/org/thoughtcrime/securesms/util/BackupUtil.kt b/src/org/thoughtcrime/securesms/util/BackupUtil.kt index c23dbc772c..894a70087d 100644 --- a/src/org/thoughtcrime/securesms/util/BackupUtil.kt +++ b/src/org/thoughtcrime/securesms/util/BackupUtil.kt @@ -40,7 +40,7 @@ object BackupUtil { @Throws(IOException::class) fun enableBackups(context: Context, password: String) { val backupDir = getBackupDirUri(context) - if (backupDir == null || validateDirAccess(context, backupDir)) { + if (backupDir == null || !validateDirAccess(context, backupDir)) { throw IOException("Backup dir is not set or invalid.") } From d2f79f902f1dd98696691abeba4a0914bd6c190e Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Thu, 17 Sep 2020 13:26:03 +1000 Subject: [PATCH 06/13] Move files & strings --- res/values/strings.xml | 3 ++- .../thoughtcrime/securesms/backup/BackupDialog.java | 3 +-- .../securesms/backup/FullBackupExporter.java | 7 +++---- .../securesms/database/DatabaseFactory.java | 1 + .../database/helpers/SQLCipherOpenHelper.java | 4 ++-- .../thoughtcrime/securesms/jobs/LocalBackupJob.java | 2 +- .../model => loki/database}/BackupFileRecord.kt | 4 ++-- .../{ => loki}/database/LokiBackupFilesDatabase.kt | 7 +++---- .../preferences/ChatsPreferenceFragment.java | 9 +++------ src/org/thoughtcrime/securesms/util/BackupUtil.kt | 3 +-- .../thoughtcrime/securesms/util/BackupUtilOld.java | 11 ++--------- .../thoughtcrime/securesms/util/ContextProvider.kt | 2 ++ 12 files changed, 23 insertions(+), 33 deletions(-) rename src/org/thoughtcrime/securesms/{database/model => loki/database}/BackupFileRecord.kt (64%) rename src/org/thoughtcrime/securesms/{ => loki}/database/LokiBackupFilesDatabase.kt (94%) diff --git a/res/values/strings.xml b/res/values/strings.xml index d801f1dfa7..98298e8f98 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1499,7 +1499,6 @@ Disable and delete all local backups? Delete backups Copied to clipboard - Failed to activate backups. Please try again or contact support. Session requires external storage permission in order to create backups, but it has been permanently denied. Please continue to app settings, select \"Permissions\" and enable \"Storage\". Last backup: %s In progress @@ -1867,4 +1866,6 @@ Voice Message Details + Failed to activate backups. Please try again or contact support. + diff --git a/src/org/thoughtcrime/securesms/backup/BackupDialog.java b/src/org/thoughtcrime/securesms/backup/BackupDialog.java index c7ca55ad35..177e75e1dd 100644 --- a/src/org/thoughtcrime/securesms/backup/BackupDialog.java +++ b/src/org/thoughtcrime/securesms/backup/BackupDialog.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.backup; - import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; @@ -51,7 +50,7 @@ public class BackupDialog { } catch (IOException e) { Log.e(TAG, "Failed to activate backups.", e); Toast.makeText(context, - context.getString(R.string.BackupDialog_activation_error), + context.getString(R.string.dialog_backup_activation_failed), Toast.LENGTH_LONG) .show(); return; diff --git a/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java b/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java index 8d361b89c9..a7f1fb1582 100644 --- a/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java +++ b/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java @@ -1,14 +1,13 @@ package org.thoughtcrime.securesms.backup; - import android.content.Context; import android.database.Cursor; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import android.net.Uri; import android.text.TextUtils; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.annimon.stream.function.Consumer; import com.annimon.stream.function.Predicate; import com.google.protobuf.ByteString; diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index de065a819b..3df26109b8 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase; +import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase; import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; import org.thoughtcrime.securesms.loki.database.LokiPreKeyBundleDatabase; import org.thoughtcrime.securesms.loki.database.LokiPreKeyRecordDatabase; diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 9e105f45a3..21ddb9f1a0 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -24,7 +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.loki.database.LokiBackupFilesDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase; import org.thoughtcrime.securesms.database.PushDatabase; @@ -92,7 +92,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV13 = 34; private static final int lokiV14_BACKUP_FILES = 35; - 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 int DATABASE_VERSION = lokiV14_BACKUP_FILES; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes private static final String DATABASE_NAME = "signal.db"; private final Context context; diff --git a/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java b/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java index 938a7ebffd..3e378a1fb5 100644 --- a/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java +++ b/src/org/thoughtcrime/securesms/jobs/LocalBackupJob.java @@ -4,7 +4,7 @@ package org.thoughtcrime.securesms.jobs; import androidx.annotation.NonNull; import org.thoughtcrime.securesms.database.NoExternalStorageException; -import org.thoughtcrime.securesms.database.model.BackupFileRecord; +import org.thoughtcrime.securesms.loki.database.BackupFileRecord; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.logging.Log; diff --git a/src/org/thoughtcrime/securesms/database/model/BackupFileRecord.kt b/src/org/thoughtcrime/securesms/loki/database/BackupFileRecord.kt similarity index 64% rename from src/org/thoughtcrime/securesms/database/model/BackupFileRecord.kt rename to src/org/thoughtcrime/securesms/loki/database/BackupFileRecord.kt index 83948d3530..0d3fc63bcf 100644 --- a/src/org/thoughtcrime/securesms/database/model/BackupFileRecord.kt +++ b/src/org/thoughtcrime/securesms/loki/database/BackupFileRecord.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.database.model +package org.thoughtcrime.securesms.loki.database import android.net.Uri import java.util.* @@ -8,5 +8,5 @@ import java.util.* */ 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) + 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/database/LokiBackupFilesDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/LokiBackupFilesDatabase.kt similarity index 94% rename from src/org/thoughtcrime/securesms/database/LokiBackupFilesDatabase.kt rename to src/org/thoughtcrime/securesms/loki/database/LokiBackupFilesDatabase.kt index d68b61a6f1..65ed0cc8bd 100644 --- a/src/org/thoughtcrime/securesms/database/LokiBackupFilesDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/LokiBackupFilesDatabase.kt @@ -1,12 +1,11 @@ -package org.thoughtcrime.securesms.database +package org.thoughtcrime.securesms.loki.database import android.content.ContentValues import android.content.Context import android.database.Cursor import android.net.Uri -import androidx.annotation.NonNull +import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper -import org.thoughtcrime.securesms.database.model.BackupFileRecord import java.lang.IllegalArgumentException import java.util.* import kotlin.collections.ArrayList @@ -15,7 +14,7 @@ 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?) +class LokiBackupFilesDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Database(context, databaseHelper) { companion object { diff --git a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java index 5d12ab3eb9..53e9b5078e 100644 --- a/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.preferences; -import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; @@ -27,7 +26,6 @@ import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.preferences.widgets.ProgressPreference; import org.thoughtcrime.securesms.util.BackupDirSelector; import org.thoughtcrime.securesms.util.BackupUtil; -import org.thoughtcrime.securesms.util.FragmentContextProvider; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Trimmer; @@ -120,9 +118,8 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { } private void setBackupSummary() { - findPreference(TextSecurePreferences.BACKUP_NOW).setSummary( - String.format(getString(R.string.ChatsPreferenceFragment_last_backup_s), - BackupUtil.getLastBackupTimeString(getContext(), Locale.getDefault()))); + findPreference(TextSecurePreferences.BACKUP_NOW) + .setSummary(String.format(getString(R.string.ChatsPreferenceFragment_last_backup_s), BackupUtil.getLastBackupTimeString(getContext(), Locale.getDefault()))); } private void setMediaDownloadSummaries() { @@ -152,7 +149,7 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { private class BackupClickListener implements Preference.OnPreferenceClickListener { @Override public boolean onPreferenceClick(Preference preference) { - if (!((SwitchPreferenceCompat)preference).isChecked()) { + if (!((SwitchPreferenceCompat) preference).isChecked()) { BackupDialog.showEnableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference, backupDirSelector); } else { BackupDialog.showDisableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference); diff --git a/src/org/thoughtcrime/securesms/util/BackupUtil.kt b/src/org/thoughtcrime/securesms/util/BackupUtil.kt index 894a70087d..0454bab3f6 100644 --- a/src/org/thoughtcrime/securesms/util/BackupUtil.kt +++ b/src/org/thoughtcrime/securesms/util/BackupUtil.kt @@ -17,11 +17,10 @@ import org.thoughtcrime.securesms.backup.BackupPassphrase import org.thoughtcrime.securesms.backup.FullBackupExporter import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.database.model.BackupFileRecord +import org.thoughtcrime.securesms.loki.database.BackupFileRecord import org.thoughtcrime.securesms.service.LocalBackupListener import org.whispersystems.libsignal.util.ByteUtil import java.io.IOException -import java.lang.IllegalStateException import java.security.SecureRandom import java.text.SimpleDateFormat import java.util.* diff --git a/src/org/thoughtcrime/securesms/util/BackupUtilOld.java b/src/org/thoughtcrime/securesms/util/BackupUtilOld.java index 31aad6b8bd..4f4cb92374 100644 --- a/src/org/thoughtcrime/securesms/util/BackupUtilOld.java +++ b/src/org/thoughtcrime/securesms/util/BackupUtilOld.java @@ -2,21 +2,14 @@ package org.thoughtcrime.securesms.util; import android.content.Context; -import androidx.annotation.NonNull; + import androidx.annotation.Nullable; -import org.thoughtcrime.securesms.database.model.BackupFileRecord; -import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.loki.database.BackupFileRecord; -import network.loki.messenger.R; import org.thoughtcrime.securesms.database.NoExternalStorageException; -import org.whispersystems.libsignal.util.ByteUtil; import java.io.File; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Locale; //TODO AC: Delete this class when its functionality is // fully replaced by the BackupUtil.kt and related classes. diff --git a/src/org/thoughtcrime/securesms/util/ContextProvider.kt b/src/org/thoughtcrime/securesms/util/ContextProvider.kt index 60b02e9a95..4bc8e104dd 100644 --- a/src/org/thoughtcrime/securesms/util/ContextProvider.kt +++ b/src/org/thoughtcrime/securesms/util/ContextProvider.kt @@ -15,6 +15,7 @@ interface ContextProvider { } class ActivityContextProvider(private val activity: Activity): ContextProvider { + override fun getContext(): Context { return activity } @@ -25,6 +26,7 @@ class ActivityContextProvider(private val activity: Activity): ContextProvider { } class FragmentContextProvider(private val fragment: Fragment): ContextProvider { + override fun getContext(): Context { return fragment.requireContext() } From 5a31a5c933e0dbe21b201c248bbb1fa060d6d30f Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Thu, 17 Sep 2020 15:30:59 +1000 Subject: [PATCH 07/13] Add reminder --- AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 883d9b4ceb..2c6f4ee9e7 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -61,6 +61,7 @@ + Date: Thu, 17 Sep 2020 17:06:39 +1000 Subject: [PATCH 08/13] Guard against tapjacking --- res/layout-sw400dp/activity_register.xml | 4 +- res/layout-sw400dp/activity_seed.xml | 6 +- .../fragment_enter_public_key.xml | 8 +- res/layout/activity_register.xml | 4 +- res/layout/activity_seed.xml | 6 +- res/layout/dialog_seed.xml | 4 +- res/layout/fragment_enter_public_key.xml | 8 +- .../conversation/ConversationItem.java | 3 +- .../activities/CreatePrivateChatActivity.kt | 8 +- .../securesms/loki/views/TapJacking.kt | 79 +++++++++++++++++++ 10 files changed, 105 insertions(+), 25 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/loki/views/TapJacking.kt diff --git a/res/layout-sw400dp/activity_register.xml b/res/layout-sw400dp/activity_register.xml index 8cc432c160..6245f922bc 100644 --- a/res/layout-sw400dp/activity_register.xml +++ b/res/layout-sw400dp/activity_register.xml @@ -30,7 +30,7 @@ android:textColor="@color/text" android:text="@string/activity_register_explanation" /> - -