mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-24 10:35:19 +00:00
New backup util and backup dir selector.
This commit is contained in:
parent
5a5702302f
commit
019b47b18f
@ -6,22 +6,28 @@ import android.content.ClipboardManager;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||||
|
import org.thoughtcrime.securesms.util.BackupDirSelector;
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||||
import org.thoughtcrime.securesms.util.BackupUtilOld;
|
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
public class BackupDialog {
|
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();
|
String[] password = BackupUtil.generateBackupPassphrase();
|
||||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||||
.setTitle(R.string.BackupDialog_enable_local_backups)
|
.setTitle(R.string.BackupDialog_enable_local_backups)
|
||||||
@ -35,12 +41,14 @@ public class BackupDialog {
|
|||||||
button.setOnClickListener(v -> {
|
button.setOnClickListener(v -> {
|
||||||
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
|
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
|
||||||
if (confirmationCheckBox.isChecked()) {
|
if (confirmationCheckBox.isChecked()) {
|
||||||
|
backupDirSelector.selectBackupDir(true, uri -> {
|
||||||
BackupPassphrase.set(context, Util.join(password, " "));
|
BackupPassphrase.set(context, Util.join(password, " "));
|
||||||
TextSecurePreferences.setBackupEnabled(context, true);
|
TextSecurePreferences.setBackupEnabled(context, true);
|
||||||
LocalBackupListener.schedule(context);
|
LocalBackupListener.schedule(context);
|
||||||
|
|
||||||
preference.setChecked(true);
|
preference.setChecked(true);
|
||||||
created.dismiss();
|
created.dismiss();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show();
|
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) -> {
|
.setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
|
||||||
BackupPassphrase.set(context, null);
|
BackupPassphrase.set(context, null);
|
||||||
TextSecurePreferences.setBackupEnabled(context, false);
|
TextSecurePreferences.setBackupEnabled(context, false);
|
||||||
BackupUtilOld.deleteAllBackups(context);
|
BackupUtil.deleteAllBackupFiles(context);
|
||||||
|
BackupUtil.setBackupDirUri(context, null);
|
||||||
preference.setChecked(false);
|
preference.setChecked(false);
|
||||||
})
|
})
|
||||||
.create()
|
.create()
|
||||||
|
@ -16,7 +16,7 @@ public class BackupPassphrase {
|
|||||||
|
|
||||||
private static final String TAG = BackupPassphrase.class.getSimpleName();
|
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 passphrase = TextSecurePreferences.getBackupPassphrase(context);
|
||||||
String encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
|
String encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import android.content.Context;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import com.annimon.stream.function.Consumer;
|
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.kdf.HKDFv3;
|
||||||
import org.whispersystems.libsignal.util.ByteUtil;
|
import org.whispersystems.libsignal.util.ByteUtil;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.Flushable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@ -64,11 +67,16 @@ public class FullBackupExporter extends FullBackupBase {
|
|||||||
public static void export(@NonNull Context context,
|
public static void export(@NonNull Context context,
|
||||||
@NonNull AttachmentSecret attachmentSecret,
|
@NonNull AttachmentSecret attachmentSecret,
|
||||||
@NonNull SQLiteDatabase input,
|
@NonNull SQLiteDatabase input,
|
||||||
@NonNull File output,
|
@NonNull Uri fileUri,
|
||||||
@NonNull String passphrase)
|
@NonNull String passphrase)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(output, passphrase);
|
OutputStream baseOutputStream = context.getContentResolver().openOutputStream(fileUri);
|
||||||
|
if (baseOutputStream == null) {
|
||||||
|
throw new IOException("Cannot open an output stream for the file URI: " + fileUri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
try (BackupFrameOutputStream outputStream = new BackupFrameOutputStream(baseOutputStream, passphrase)) {
|
||||||
outputStream.writeDatabaseVersion(input.getVersion());
|
outputStream.writeDatabaseVersion(input.getVersion());
|
||||||
|
|
||||||
List<String> tables = exportSchema(input, outputStream);
|
List<String> tables = exportSchema(input, outputStream);
|
||||||
@ -105,9 +113,9 @@ public class FullBackupExporter extends FullBackupBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
outputStream.writeEnd();
|
outputStream.writeEnd();
|
||||||
outputStream.close();
|
|
||||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count));
|
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static List<String> exportSchema(@NonNull SQLiteDatabase input, @NonNull BackupFrameOutputStream outputStream)
|
private static List<String> exportSchema(@NonNull SQLiteDatabase input, @NonNull BackupFrameOutputStream outputStream)
|
||||||
throws IOException
|
throws IOException
|
||||||
@ -228,9 +236,10 @@ public class FullBackupExporter extends FullBackupBase {
|
|||||||
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM));
|
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM));
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(data) && size > 0) {
|
if (!TextUtils.isEmpty(data) && size > 0) {
|
||||||
InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
|
try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) {
|
||||||
outputStream.writeSticker(rowId, inputStream, size);
|
outputStream.writeSticker(rowId, inputStream, size);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, 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 OutputStream outputStream;
|
||||||
private final Cipher cipher;
|
private final Cipher cipher;
|
||||||
@ -280,7 +289,7 @@ public class FullBackupExporter extends FullBackupBase {
|
|||||||
private byte[] iv;
|
private byte[] iv;
|
||||||
private int counter;
|
private int counter;
|
||||||
|
|
||||||
private BackupFrameOutputStream(@NonNull File output, @NonNull String passphrase) throws IOException {
|
private BackupFrameOutputStream(@NonNull OutputStream outputStream, @NonNull String passphrase) throws IOException {
|
||||||
try {
|
try {
|
||||||
byte[] salt = Util.getSecretBytes(32);
|
byte[] salt = Util.getSecretBytes(32);
|
||||||
byte[] key = getBackupKey(passphrase, salt);
|
byte[] key = getBackupKey(passphrase, salt);
|
||||||
@ -292,7 +301,7 @@ public class FullBackupExporter extends FullBackupBase {
|
|||||||
|
|
||||||
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||||
this.mac = Mac.getInstance("HmacSHA256");
|
this.mac = Mac.getInstance("HmacSHA256");
|
||||||
this.outputStream = new FileOutputStream(output);
|
this.outputStream = outputStream;
|
||||||
this.iv = Util.getSecretBytes(16);
|
this.iv = Util.getSecretBytes(16);
|
||||||
this.counter = Conversions.byteArrayToInt(iv);
|
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 {
|
public void close() throws IOException {
|
||||||
outputStream.close();
|
outputStream.close();
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,10 @@ import android.content.ContentValues
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.annotation.NonNull
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
import org.thoughtcrime.securesms.database.model.BackupFileRecord
|
import org.thoughtcrime.securesms.database.model.BackupFileRecord
|
||||||
|
import java.lang.IllegalArgumentException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
@ -56,7 +58,7 @@ class LokiBackupFilesDatabase(context: Context?, databaseHelper: SQLCipherOpenHe
|
|||||||
fun getBackupFiles(): List<BackupFileRecord> {
|
fun getBackupFiles(): List<BackupFileRecord> {
|
||||||
databaseHelper.readableDatabase.query(TABLE_NAME, allColumns, null, null, null, null, null).use {
|
databaseHelper.readableDatabase.query(TABLE_NAME, allColumns, null, null, null, null, null).use {
|
||||||
val records = ArrayList<BackupFileRecord>()
|
val records = ArrayList<BackupFileRecord>()
|
||||||
while (it != null && it.moveToFirst()) {
|
while (it != null && it.moveToNext()) {
|
||||||
val record = mapCursorToRecord(it)
|
val record = mapCursorToRecord(it)
|
||||||
records.add(record)
|
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)
|
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? {
|
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
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,32 +1,22 @@
|
|||||||
package org.thoughtcrime.securesms.jobs;
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import androidx.annotation.NonNull;
|
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.NoExternalStorageException;
|
||||||
|
import org.thoughtcrime.securesms.database.model.BackupFileRecord;
|
||||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
|
||||||
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
||||||
import org.thoughtcrime.securesms.util.BackupUtilOld;
|
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||||
import org.thoughtcrime.securesms.util.ExternalStorageUtil;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
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 class LocalBackupJob extends BaseJob {
|
||||||
|
|
||||||
public static final String KEY = "LocalBackupJob";
|
public static final String KEY = "LocalBackupJob";
|
||||||
@ -59,10 +49,6 @@ public class LocalBackupJob extends BaseJob {
|
|||||||
public void onRun() throws NoExternalStorageException, IOException {
|
public void onRun() throws NoExternalStorageException, IOException {
|
||||||
Log.i(TAG, "Executing backup job...");
|
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,
|
GenericForegroundService.startForegroundTask(context,
|
||||||
context.getString(R.string.LocalBackupJob_creating_backup),
|
context.getString(R.string.LocalBackupJob_creating_backup),
|
||||||
NotificationChannels.BACKUPS,
|
NotificationChannels.BACKUPS,
|
||||||
@ -71,34 +57,9 @@ public class LocalBackupJob extends BaseJob {
|
|||||||
// TODO: Maybe create a new backup icon like ic_signal_backup?
|
// TODO: Maybe create a new backup icon like ic_signal_backup?
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String backupPassword = BackupPassphrase.get(context);
|
BackupFileRecord record = BackupUtil.createBackupFile(context);
|
||||||
File backupDirectory = ExternalStorageUtil.getBackupDir(context);
|
BackupUtil.deleteAllBackupFiles(context, Collections.singletonList(record));
|
||||||
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);
|
|
||||||
|
|
||||||
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 {
|
} finally {
|
||||||
GenericForegroundService.stopForegroundTask(context);
|
GenericForegroundService.stopForegroundTask(context);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import android.Manifest;
|
|||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
@ -24,7 +25,9 @@ import org.thoughtcrime.securesms.jobs.LocalBackupJob;
|
|||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.preferences.widgets.ProgressPreference;
|
import org.thoughtcrime.securesms.preferences.widgets.ProgressPreference;
|
||||||
|
import org.thoughtcrime.securesms.util.BackupDirSelector;
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.FragmentContextProvider;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Trimmer;
|
import org.thoughtcrime.securesms.util.Trimmer;
|
||||||
|
|
||||||
@ -38,6 +41,8 @@ import network.loki.messenger.R;
|
|||||||
public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||||
private static final String TAG = ChatsPreferenceFragment.class.getSimpleName();
|
private static final String TAG = ChatsPreferenceFragment.class.getSimpleName();
|
||||||
|
|
||||||
|
private BackupDirSelector backupDirSelector;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle paramBundle) {
|
public void onCreate(Bundle paramBundle) {
|
||||||
super.onCreate(paramBundle);
|
super.onCreate(paramBundle);
|
||||||
@ -65,6 +70,8 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||||||
|
|
||||||
// initializeListSummary((ListPreference) findPreference(TextSecurePreferences.MESSAGE_BODY_TEXT_SIZE_PREF));
|
// initializeListSummary((ListPreference) findPreference(TextSecurePreferences.MESSAGE_BODY_TEXT_SIZE_PREF));
|
||||||
|
|
||||||
|
backupDirSelector = new BackupDirSelector(this);
|
||||||
|
|
||||||
EventBus.getDefault().register(this);
|
EventBus.getDefault().register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,6 +98,12 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
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)
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
public void onEvent(BackupEvent event) {
|
public void onEvent(BackupEvent event) {
|
||||||
ProgressPreference preference = (ProgressPreference)findPreference(TextSecurePreferences.BACKUP_NOW);
|
ProgressPreference preference = (ProgressPreference)findPreference(TextSecurePreferences.BACKUP_NOW);
|
||||||
@ -143,7 +156,7 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
if (!((SwitchPreferenceCompat)preference).isChecked()) {
|
if (!((SwitchPreferenceCompat)preference).isChecked()) {
|
||||||
BackupDialog.showEnableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference);
|
BackupDialog.showEnableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference, backupDirSelector);
|
||||||
} else {
|
} else {
|
||||||
BackupDialog.showDisableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference);
|
BackupDialog.showDisableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference);
|
||||||
}
|
}
|
||||||
|
@ -52,9 +52,9 @@ public class AvatarHelper {
|
|||||||
if (data == null) {
|
if (data == null) {
|
||||||
delete(context, address);
|
delete(context, address);
|
||||||
} else {
|
} else {
|
||||||
FileOutputStream out = new FileOutputStream(getAvatarFile(context, address));
|
try (FileOutputStream out = new FileOutputStream(getAvatarFile(context, address))) {
|
||||||
out.write(data);
|
out.write(data);
|
||||||
out.close();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,31 @@
|
|||||||
package org.thoughtcrime.securesms.util
|
package org.thoughtcrime.securesms.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
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 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.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.database.model.BackupFileRecord
|
import org.thoughtcrime.securesms.database.model.BackupFileRecord
|
||||||
import org.whispersystems.libsignal.util.ByteUtil
|
import org.whispersystems.libsignal.util.ByteUtil
|
||||||
|
import java.io.IOException
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object BackupUtil {
|
object BackupUtil {
|
||||||
|
private const val TAG = "BackupUtil"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getLastBackupTimeString(context: Context, locale: Locale): String {
|
fun getLastBackupTimeString(context: Context, locale: Locale): String {
|
||||||
@ -35,4 +52,195 @@ object BackupUtil {
|
|||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return result as Array<String>
|
return result as Array<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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<BackupFileRecord>? = 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)
|
||||||
|
}
|
||||||
}
|
}
|
@ -4,6 +4,8 @@ package org.thoughtcrime.securesms.util;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.database.model.BackupFileRecord;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
@ -16,125 +18,29 @@ import java.util.Arrays;
|
|||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Locale;
|
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} */
|
/** @deprecated in favor of {@link BackupUtil} */
|
||||||
public class BackupUtilOld {
|
public class BackupUtilOld {
|
||||||
|
|
||||||
private static final String TAG = BackupUtilOld.class.getSimpleName();
|
private static final String TAG = BackupUtilOld.class.getSimpleName();
|
||||||
|
|
||||||
public static @NonNull String getLastBackupTime(@NonNull Context context, @NonNull Locale locale) {
|
/**
|
||||||
try {
|
* @deprecated this method exists only for the backward compatibility with the legacy Signal backup code.
|
||||||
BackupInfo backup = getLatestBackup(context);
|
* Use {@link BackupUtil} if possible.
|
||||||
|
*/
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static @Nullable BackupInfo getLatestBackup(Context context) throws NoExternalStorageException {
|
public static @Nullable BackupInfo getLatestBackup(Context context) throws NoExternalStorageException {
|
||||||
File backupDirectory = ExternalStorageUtil.getBackupDir(context);
|
BackupFileRecord backup = BackupUtil.getLastBackup(context);
|
||||||
File[] backups = backupDirectory.listFiles();
|
if (backup == null) return null;
|
||||||
BackupInfo latestBackup = null;
|
|
||||||
|
|
||||||
for (File backup : backups) {
|
|
||||||
long backupTimestamp = getBackupTimestamp(backup);
|
|
||||||
|
|
||||||
if (latestBackup == null || (backupTimestamp != -1 && backupTimestamp > latestBackup.getTimestamp())) {
|
return new BackupInfo(
|
||||||
latestBackup = new BackupInfo(backupTimestamp, backup.length(), backup);
|
backup.getTimestamp().getTime(),
|
||||||
}
|
backup.getFileSize(),
|
||||||
}
|
new File(backup.getUri().getPath()));
|
||||||
|
|
||||||
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<backups.length;i++) {
|
|
||||||
Log.i(TAG, "Deleting: " + backups[i].getAbsolutePath());
|
|
||||||
|
|
||||||
if (!backups[i].delete()) {
|
|
||||||
Log.w(TAG, "Delete failed: " + backups[i].getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (NoExternalStorageException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static @NonNull String[] generateBackupPassphrase() {
|
|
||||||
String[] result = new String[6];
|
|
||||||
byte[] random = new byte[30];
|
|
||||||
|
|
||||||
new SecureRandom().nextBytes(random);
|
|
||||||
|
|
||||||
for (int i=0;i<30;i+=5) {
|
|
||||||
result[i/5] = String.format("%05d", ByteUtil.byteArray5ToLong(random, i) % 100000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long getBackupTimestamp(File backup) {
|
|
||||||
String name = backup.getName();
|
|
||||||
String[] prefixSuffix = name.split("[.]");
|
|
||||||
|
|
||||||
if (prefixSuffix.length == 2) {
|
|
||||||
String[] parts = prefixSuffix[0].split("\\-");
|
|
||||||
|
|
||||||
if (parts.length == 7) {
|
|
||||||
try {
|
|
||||||
Calendar calendar = Calendar.getInstance();
|
|
||||||
calendar.set(Calendar.YEAR, Integer.parseInt(parts[1]));
|
|
||||||
calendar.set(Calendar.MONTH, Integer.parseInt(parts[2]) - 1);
|
|
||||||
calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(parts[3]));
|
|
||||||
calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(parts[4]));
|
|
||||||
calendar.set(Calendar.MINUTE, Integer.parseInt(parts[5]));
|
|
||||||
calendar.set(Calendar.SECOND, Integer.parseInt(parts[6]));
|
|
||||||
calendar.set(Calendar.MILLISECOND, 0);
|
|
||||||
|
|
||||||
return calendar.getTimeInMillis();
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public static class BackupInfo {
|
public static class BackupInfo {
|
||||||
|
|
||||||
private final long timestamp;
|
private final long timestamp;
|
||||||
|
35
src/org/thoughtcrime/securesms/util/ContextProvider.kt
Normal file
35
src/org/thoughtcrime/securesms/util/ContextProvider.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package org.thoughtcrime.securesms.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simplified version of [android.content.ContextWrapper],
|
||||||
|
* but properly supports [startActivityForResult] for the implementations.
|
||||||
|
*/
|
||||||
|
interface ContextProvider {
|
||||||
|
fun getContext(): Context
|
||||||
|
fun startActivityForResult(intent: Intent, requestCode: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActivityContextProvider(private val activity: Activity): ContextProvider {
|
||||||
|
override fun getContext(): Context {
|
||||||
|
return activity
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startActivityForResult(intent: Intent, requestCode: Int) {
|
||||||
|
activity.startActivityForResult(intent, requestCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FragmentContextProvider(private val fragment: Fragment): ContextProvider {
|
||||||
|
override fun getContext(): Context {
|
||||||
|
return fragment.requireContext()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startActivityForResult(intent: Intent, requestCode: Int) {
|
||||||
|
fragment.startActivityForResult(intent, requestCode)
|
||||||
|
}
|
||||||
|
}
|
@ -146,6 +146,7 @@ public class TextSecurePreferences {
|
|||||||
private static final String ENCRYPTED_BACKUP_PASSPHRASE = "pref_encrypted_backup_passphrase";
|
private static final String ENCRYPTED_BACKUP_PASSPHRASE = "pref_encrypted_backup_passphrase";
|
||||||
private static final String BACKUP_TIME = "pref_backup_next_time";
|
private static final String BACKUP_TIME = "pref_backup_next_time";
|
||||||
public static final String BACKUP_NOW = "pref_backup_create";
|
public static final String BACKUP_NOW = "pref_backup_create";
|
||||||
|
private static final String BACKUP_SAVE_DIR = "pref_save_dir";
|
||||||
|
|
||||||
public static final String SCREEN_LOCK = "pref_android_screen_lock";
|
public static final String SCREEN_LOCK = "pref_android_screen_lock";
|
||||||
public static final String SCREEN_LOCK_TIMEOUT = "pref_android_screen_lock_timeout";
|
public static final String SCREEN_LOCK_TIMEOUT = "pref_android_screen_lock_timeout";
|
||||||
@ -296,6 +297,14 @@ public class TextSecurePreferences {
|
|||||||
return getLongPreference(context, BACKUP_TIME, -1);
|
return getLongPreference(context, BACKUP_TIME, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setBackupSaveDir(@NonNull Context context, String dirUri) {
|
||||||
|
setStringPreference(context, BACKUP_SAVE_DIR, dirUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getBackupSaveDir(@NonNull Context context) {
|
||||||
|
return getStringPreference(context, BACKUP_SAVE_DIR, null);
|
||||||
|
}
|
||||||
|
|
||||||
public static int getNextPreKeyId(@NonNull Context context) {
|
public static int getNextPreKeyId(@NonNull Context context) {
|
||||||
return getIntegerPreference(context, NEXT_PRE_KEY_ID, new SecureRandom().nextInt(Medium.MAX_VALUE));
|
return getIntegerPreference(context, NEXT_PRE_KEY_ID, new SecureRandom().nextInt(Medium.MAX_VALUE));
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user