Implement new workflow for scoped storage backup selection.
This commit is contained in:
parent
9a1c869efe
commit
ee3d7a9a35
39 changed files with 1582 additions and 280 deletions
|
@ -386,10 +386,11 @@ dependencies {
|
|||
testImplementation 'org.powermock:powermock-classloading-xstream:1.7.4'
|
||||
|
||||
testImplementation 'androidx.test:core:1.2.0'
|
||||
testImplementation ('org.robolectric:robolectric:4.2') {
|
||||
testImplementation ('org.robolectric:robolectric:4.4') {
|
||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||
}
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
testImplementation 'org.hamcrest:hamcrest:2.2'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
|||
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
|
||||
|
@ -64,6 +65,8 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
|
|||
public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener
|
||||
{
|
||||
public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
|
||||
|
||||
|
@ -96,6 +99,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
|||
|
||||
if (getIntent() != null && getIntent().getCategories() != null && getIntent().getCategories().contains("android.intent.category.NOTIFICATION_PREFERENCES")) {
|
||||
initFragment(android.R.id.content, new NotificationsPreferenceFragment());
|
||||
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_BACKUPS_FRAGMENT, false)) {
|
||||
initFragment(android.R.id.content, new BackupsPreferenceFragment());
|
||||
} else if (icicle == null) {
|
||||
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ package org.thoughtcrime.securesms.backup;
|
|||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
@ -13,10 +15,15 @@ import android.widget.TextView;
|
|||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
|
@ -24,28 +31,53 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class BackupDialog {
|
||||
|
||||
public static void showEnableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
|
||||
private static final String TAG = Log.tag(BackupDialog.class);
|
||||
|
||||
public static void showEnableBackupDialog(@NonNull Context context,
|
||||
@Nullable Intent backupDirectorySelectionIntent,
|
||||
@Nullable String backupDirectoryDisplayName,
|
||||
@NonNull Runnable onBackupsEnabled)
|
||||
{
|
||||
String[] password = BackupUtil.generateBackupPassphrase();
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.BackupDialog_enable_local_backups)
|
||||
.setView(R.layout.backup_enable_dialog)
|
||||
.setView(backupDirectorySelectionIntent != null ? R.layout.backup_enable_dialog_v29 : R.layout.backup_enable_dialog)
|
||||
.setPositiveButton(R.string.BackupDialog_enable_backups, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(created -> {
|
||||
if (backupDirectoryDisplayName != null) {
|
||||
TextView folderName = dialog.findViewById(R.id.backup_enable_dialog_folder_name);
|
||||
if (folderName != null) {
|
||||
folderName.setText(backupDirectoryDisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setOnClickListener(v -> {
|
||||
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
|
||||
if (confirmationCheckBox.isChecked()) {
|
||||
if (backupDirectorySelectionIntent != null && backupDirectorySelectionIntent.getData() != null) {
|
||||
Uri backupDirectoryUri = backupDirectorySelectionIntent.getData();
|
||||
int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
|
||||
SignalStore.settings().setSignalBackupDirectory(backupDirectoryUri);
|
||||
context.getContentResolver()
|
||||
.takePersistableUriPermission(backupDirectoryUri, takeFlags);
|
||||
}
|
||||
|
||||
BackupPassphrase.set(context, Util.join(password, " "));
|
||||
TextSecurePreferences.setNextBackupTime(context, 0);
|
||||
TextSecurePreferences.setBackupEnabled(context, true);
|
||||
LocalBackupListener.schedule(context);
|
||||
|
||||
preference.setChecked(true);
|
||||
onBackupsEnabled.run();
|
||||
created.dismiss();
|
||||
} else {
|
||||
Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show();
|
||||
|
@ -76,16 +108,38 @@ public class BackupDialog {
|
|||
|
||||
}
|
||||
|
||||
public static void showDisableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
|
||||
@RequiresApi(29)
|
||||
public static void showChooseBackupLocationDialog(@NonNull Fragment fragment, int requestCode) {
|
||||
new AlertDialog.Builder(fragment.requireContext())
|
||||
.setView(R.layout.backup_choose_location_dialog)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setPositiveButton(R.string.BackupDialog_choose_folder, ((dialog, which) -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
fragment.startActivityForResult(intent, requestCode);
|
||||
|
||||
dialog.dismiss();
|
||||
}))
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void showDisableBackupDialog(@NonNull Context context, @NonNull Runnable onBackupsDisabled) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.BackupDialog_delete_backups)
|
||||
.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.deleteAllBackups();
|
||||
preference.setChecked(false);
|
||||
BackupUtil.disableBackups(context);
|
||||
|
||||
onBackupsDisabled.run();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
|
|
|
@ -3,9 +3,12 @@ package org.thoughtcrime.securesms.backup;
|
|||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import com.annimon.stream.function.Consumer;
|
||||
import com.annimon.stream.function.Predicate;
|
||||
|
@ -50,6 +53,7 @@ import java.security.InvalidKeyException;
|
|||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
|
@ -84,7 +88,32 @@ public class FullBackupExporter extends FullBackupBase {
|
|||
@NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(output, passphrase);
|
||||
try (OutputStream outputStream = new FileOutputStream(output)) {
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
public static void export(@NonNull Context context,
|
||||
@NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull DocumentFile output,
|
||||
@NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
try (OutputStream outputStream = Objects.requireNonNull(context.getContentResolver().openOutputStream(output.getUri()))) {
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase);
|
||||
}
|
||||
}
|
||||
|
||||
private static void internalExport(@NonNull Context context,
|
||||
@NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull OutputStream fileOutputStream,
|
||||
@NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase);
|
||||
int count = 0;
|
||||
|
||||
try {
|
||||
|
@ -322,7 +351,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 output, @NonNull String passphrase) throws IOException {
|
||||
try {
|
||||
byte[] salt = Util.getSecretBytes(32);
|
||||
byte[] key = getBackupKey(passphrase, salt);
|
||||
|
@ -334,7 +363,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 = output;
|
||||
this.iv = Util.getSecretBytes(16);
|
||||
this.counter = Conversions.byteArrayToInt(iv);
|
||||
|
||||
|
|
|
@ -6,9 +6,11 @@ import android.content.ContentValues;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.net.Uri;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
@ -25,8 +27,8 @@ import org.thoughtcrime.securesms.database.SearchDatabase;
|
|||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
@ -36,7 +38,6 @@ import org.whispersystems.libsignal.util.ByteUtil;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
@ -46,6 +47,7 @@ import java.security.MessageDigest;
|
|||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
|
@ -61,13 +63,14 @@ public class FullBackupImporter extends FullBackupBase {
|
|||
private static final String TAG = FullBackupImporter.class.getSimpleName();
|
||||
|
||||
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase db, @NonNull File file, @NonNull String passphrase)
|
||||
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
BackupRecordInputStream inputStream = new BackupRecordInputStream(file, passphrase);
|
||||
int count = 0;
|
||||
int count = 0;
|
||||
|
||||
try (InputStream is = getInputStream(context, uri)) {
|
||||
BackupRecordInputStream inputStream = new BackupRecordInputStream(is, passphrase);
|
||||
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
dropAllTables(db);
|
||||
|
@ -93,6 +96,14 @@ public class FullBackupImporter extends FullBackupBase {
|
|||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count));
|
||||
}
|
||||
|
||||
private static @NonNull InputStream getInputStream(@NonNull Context context, @NonNull Uri uri) throws IOException{
|
||||
if (BackupUtil.isUserSelectionRequired(context)) {
|
||||
return Objects.requireNonNull(context.getContentResolver().openInputStream(uri));
|
||||
} else {
|
||||
return new FileInputStream(new File(Objects.requireNonNull(uri.getPath())));
|
||||
}
|
||||
}
|
||||
|
||||
private static void processVersion(@NonNull SQLiteDatabase db, DatabaseVersion version) throws IOException {
|
||||
if (version.getVersion() > db.getVersion()) {
|
||||
throw new DatabaseDowngradeException(db.getVersion(), version.getVersion());
|
||||
|
@ -221,9 +232,9 @@ public class FullBackupImporter extends FullBackupBase {
|
|||
private byte[] iv;
|
||||
private int counter;
|
||||
|
||||
private BackupRecordInputStream(@NonNull File file, @NonNull String passphrase) throws IOException {
|
||||
private BackupRecordInputStream(@NonNull InputStream in, @NonNull String passphrase) throws IOException {
|
||||
try {
|
||||
this.in = new FileInputStream(file);
|
||||
this.in = in;
|
||||
|
||||
byte[] headerLengthBytes = new byte[4];
|
||||
Util.readFully(in, headerLengthBytes);
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
|||
import org.thoughtcrime.securesms.jobs.FailingJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob;
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJobApi29;
|
||||
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||
|
@ -60,6 +61,7 @@ public class WorkManagerFactoryMappings {
|
|||
put("DirectoryRefreshJob", DirectoryRefreshJob.KEY);
|
||||
put("FcmRefreshJob", FcmRefreshJob.KEY);
|
||||
put("LocalBackupJob", LocalBackupJob.KEY);
|
||||
put("LocalBackupJobApi29", LocalBackupJobApi29.KEY);
|
||||
put("MmsDownloadJob", MmsDownloadJob.KEY);
|
||||
put("MmsReceiveJob", MmsReceiveJob.KEY);
|
||||
put("MmsSendJob", MmsSendJob.KEY);
|
||||
|
|
|
@ -71,6 +71,7 @@ public final class JobManagerFactories {
|
|||
put(KbsEnclaveMigrationWorkerJob.KEY, new KbsEnclaveMigrationWorkerJob.Factory());
|
||||
put(LeaveGroupJob.KEY, new LeaveGroupJob.Factory());
|
||||
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
||||
put(LocalBackupJobApi29.KEY, new LocalBackupJobApi29.Factory());
|
||||
put(MmsDownloadJob.KEY, new MmsDownloadJob.Factory());
|
||||
put(MmsReceiveJob.KEY, new MmsReceiveJob.Factory());
|
||||
put(MmsSendJob.KEY, new MmsSendJob.Factory());
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.jobs;
|
|||
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
@ -53,7 +54,11 @@ public final class LocalBackupJob extends BaseJob {
|
|||
parameters.addConstraint(ChargingConstraint.KEY);
|
||||
}
|
||||
|
||||
jobManager.add(new LocalBackupJob(parameters.build()));
|
||||
if (BackupUtil.isUserSelectionRequired(ApplicationDependencies.getApplication())) {
|
||||
jobManager.add(new LocalBackupJobApi29(parameters.build()));
|
||||
} else {
|
||||
jobManager.add(new LocalBackupJob(parameters.build()));
|
||||
}
|
||||
}
|
||||
|
||||
private LocalBackupJob(@NonNull Job.Parameters parameters) {
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.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.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
||||
import org.thoughtcrime.securesms.service.NotificationController;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Backup Job for installs requiring Scoped Storage.
|
||||
*
|
||||
* @see LocalBackupJob#enqueue(boolean)
|
||||
*/
|
||||
public final class LocalBackupJobApi29 extends BaseJob {
|
||||
|
||||
public static final String KEY = "LocalBackupJobApi29";
|
||||
|
||||
private static final String TAG = Log.tag(LocalBackupJobApi29.class);
|
||||
|
||||
private static final short BACKUP_FAILED_ID = 31321;
|
||||
|
||||
public static final String TEMP_BACKUP_FILE_PREFIX = ".backup";
|
||||
public static final String TEMP_BACKUP_FILE_SUFFIX = ".tmp";
|
||||
|
||||
LocalBackupJobApi29(@NonNull Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return Data.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException {
|
||||
Log.i(TAG, "Executing backup job...");
|
||||
|
||||
NotificationManagerCompat.from(context).cancel(BACKUP_FAILED_ID);
|
||||
|
||||
if (!BackupUtil.isUserSelectionRequired(context)) {
|
||||
throw new IOException("Wrong backup job!");
|
||||
}
|
||||
|
||||
Uri backupDirectoryUri = SignalStore.settings().getSignalBackupDirectory();
|
||||
if (backupDirectoryUri == null || backupDirectoryUri.getPath() == null) {
|
||||
throw new IOException("Backup Directory has not been selected!");
|
||||
}
|
||||
|
||||
try (NotificationController notification = GenericForegroundService.startForegroundTask(context,
|
||||
context.getString(R.string.LocalBackupJob_creating_backup),
|
||||
NotificationChannels.BACKUPS,
|
||||
R.drawable.ic_signal_backup))
|
||||
{
|
||||
notification.setIndeterminateProgress();
|
||||
|
||||
String backupPassword = BackupPassphrase.get(context);
|
||||
DocumentFile backupDirectory = DocumentFile.fromTreeUri(context, backupDirectoryUri);
|
||||
String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(new Date());
|
||||
String fileName = String.format("signal-%s.backup", timestamp);
|
||||
|
||||
if (backupDirectory == null || !backupDirectory.canWrite()) {
|
||||
BackupUtil.disableBackups(context);
|
||||
postBackupsDisabledNotification();
|
||||
throw new IOException("Cannot write to backup directory location.");
|
||||
}
|
||||
|
||||
deleteOldTemporaryBackups(backupDirectory);
|
||||
|
||||
if (backupDirectory.findFile(fileName) != null) {
|
||||
throw new IOException("Backup file already exists!");
|
||||
}
|
||||
|
||||
String temporaryName = String.format(Locale.US, "%s%s%s", TEMP_BACKUP_FILE_PREFIX, UUID.randomUUID(), TEMP_BACKUP_FILE_SUFFIX);
|
||||
DocumentFile temporaryFile = backupDirectory.createFile("application/octet-stream", temporaryName);
|
||||
|
||||
if (temporaryFile == null) {
|
||||
throw new IOException("Failed to create temporary backup file.");
|
||||
}
|
||||
|
||||
if (backupPassword == null) {
|
||||
throw new IOException("Backup password is null");
|
||||
}
|
||||
|
||||
try {
|
||||
FullBackupExporter.export(context,
|
||||
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
||||
DatabaseFactory.getBackupDatabase(context),
|
||||
temporaryFile,
|
||||
backupPassword);
|
||||
|
||||
if (!temporaryFile.renameTo(fileName)) {
|
||||
Log.w(TAG, "Failed to rename temp file");
|
||||
throw new IOException("Renaming temporary backup file failed!");
|
||||
}
|
||||
} finally {
|
||||
DocumentFile fileToCleanUp = backupDirectory.findFile(temporaryName);
|
||||
if (fileToCleanUp != null) {
|
||||
if (fileToCleanUp.delete()) {
|
||||
Log.w(TAG, "Backup failed. Deleted temp file");
|
||||
} else {
|
||||
Log.w(TAG, "Backup failed. Failed to delete temp file " + temporaryName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BackupUtil.deleteOldBackups();
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteOldTemporaryBackups(@NonNull DocumentFile backupDirectory) {
|
||||
for (DocumentFile file : backupDirectory.listFiles()) {
|
||||
if (file.isFile()) {
|
||||
String name = file.getName();
|
||||
if (name != null && name.startsWith(TEMP_BACKUP_FILE_PREFIX) && name.endsWith(TEMP_BACKUP_FILE_SUFFIX)) {
|
||||
if (file.delete()) {
|
||||
Log.w(TAG, "Deleted old temporary backup file");
|
||||
} else {
|
||||
Log.w(TAG, "Could not delete old temporary backup file");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
}
|
||||
|
||||
private void postBackupsDisabledNotification() {
|
||||
Intent intent = new Intent(context, ApplicationPreferencesActivity.class);
|
||||
|
||||
intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_BACKUPS_FRAGMENT, true);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, intent, 0);
|
||||
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.BACKUPS)
|
||||
.setSmallIcon(R.drawable.ic_signal_backup)
|
||||
.setContentTitle(context.getString(R.string.LocalBackupJobApi29_backups_disabled))
|
||||
.setContentText(context.getString(R.string.LocalBackupJobApi29_your_backup_directory_has_been_deleted_or_moved))
|
||||
.setContentIntent(pendingIntent)
|
||||
.build();
|
||||
|
||||
NotificationManagerCompat.from(context)
|
||||
.notify(BACKUP_FAILED_ID, backupFailedNotification);
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<LocalBackupJobApi29> {
|
||||
@Override
|
||||
public @NonNull
|
||||
LocalBackupJobApi29 create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new LocalBackupJobApi29(parameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public final class MiscellaneousValues extends SignalStoreValues {
|
||||
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public final class SettingsValues extends SignalStoreValues {
|
||||
|
||||
public static final String LINK_PREVIEWS = "settings.link_previews";
|
||||
public static final String KEEP_MESSAGES_DURATION = "settings.keep_messages_duration";
|
||||
|
||||
private static final String SIGNAL_BACKUP_DIRECTORY = "settings.signal.backup.directory";
|
||||
|
||||
public static final String THREAD_TRIM_LENGTH = "pref_trim_length";
|
||||
public static final String THREAD_TRIM_ENABLED = "pref_trim_threads";
|
||||
|
||||
|
@ -53,4 +59,22 @@ public final class SettingsValues extends SignalStoreValues {
|
|||
putInteger(THREAD_TRIM_LENGTH, length);
|
||||
}
|
||||
|
||||
public void setSignalBackupDirectory(@NonNull Uri uri) {
|
||||
putString(SIGNAL_BACKUP_DIRECTORY, uri.toString());
|
||||
}
|
||||
|
||||
public @Nullable
|
||||
Uri getSignalBackupDirectory() {
|
||||
String uri = getString(SIGNAL_BACKUP_DIRECTORY, "");
|
||||
|
||||
if (TextUtils.isEmpty(uri)) {
|
||||
return null;
|
||||
} else {
|
||||
return Uri.parse(uri);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearSignalBackupDirectory() {
|
||||
putString(SIGNAL_BACKUP_DIRECTORY, null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.backup.BackupDialog;
|
||||
import org.thoughtcrime.securesms.backup.FullBackupBase;
|
||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
public class BackupsPreferenceFragment extends Fragment {
|
||||
|
||||
private static final String TAG = Log.tag(BackupsPreferenceFragment.class);
|
||||
|
||||
private static final short CHOOSE_BACKUPS_LOCATION_REQUEST_CODE = 26212;
|
||||
|
||||
private View create;
|
||||
private View folder;
|
||||
private View verify;
|
||||
private TextView toggle;
|
||||
private TextView info;
|
||||
private TextView summary;
|
||||
private TextView folderName;
|
||||
private ProgressBar progress;
|
||||
private TextView progressSummary;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_backups, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
create = view.findViewById(R.id.fragment_backup_create);
|
||||
folder = view.findViewById(R.id.fragment_backup_folder);
|
||||
verify = view.findViewById(R.id.fragment_backup_verify);
|
||||
toggle = view.findViewById(R.id.fragment_backup_toggle);
|
||||
info = view.findViewById(R.id.fragment_backup_info);
|
||||
summary = view.findViewById(R.id.fragment_backup_create_summary);
|
||||
folderName = view.findViewById(R.id.fragment_backup_folder_name);
|
||||
progress = view.findViewById(R.id.fragment_backup_progress);
|
||||
progressSummary = view.findViewById(R.id.fragment_backup_progress_summary);
|
||||
|
||||
toggle.setOnClickListener(unused -> onToggleClicked());
|
||||
create.setOnClickListener(unused -> onCreateClicked());
|
||||
verify.setOnClickListener(unused -> BackupDialog.showVerifyBackupPassphraseDialog(requireContext()));
|
||||
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.BackupsPreferenceFragment__chat_backups);
|
||||
|
||||
setBackupStatus();
|
||||
setBackupSummary();
|
||||
setInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == CHOOSE_BACKUPS_LOCATION_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
|
||||
|
||||
DocumentFile backupDirectory = DocumentFile.fromTreeUri(requireContext(), data.getData());
|
||||
if (backupDirectory == null || !backupDirectory.isDirectory()) {
|
||||
Log.w(TAG, "Could not open backup directory.");
|
||||
return;
|
||||
}
|
||||
|
||||
BackupDialog.showEnableBackupDialog(requireContext(),
|
||||
data,
|
||||
backupDirectory.getName(),
|
||||
this::setBackupsEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(FullBackupBase.BackupEvent event) {
|
||||
if (event.getType() == FullBackupBase.BackupEvent.Type.PROGRESS) {
|
||||
create.setEnabled(false);
|
||||
summary.setText(getString(R.string.BackupsPreferenceFragment__in_progress));
|
||||
progress.setVisibility(View.VISIBLE);
|
||||
progressSummary.setVisibility(View.VISIBLE);
|
||||
progressSummary.setText(getString(R.string.BackupsPreferenceFragment__d_so_far, event.getCount()));
|
||||
} else if (event.getType() == FullBackupBase.BackupEvent.Type.FINISHED) {
|
||||
create.setEnabled(true);
|
||||
progress.setVisibility(View.GONE);
|
||||
progressSummary.setVisibility(View.GONE);
|
||||
setBackupSummary();
|
||||
}
|
||||
}
|
||||
|
||||
private void setBackupStatus() {
|
||||
if (TextSecurePreferences.isBackupEnabled(requireContext())) {
|
||||
if (BackupUtil.canUserAccessBackupDirectory(requireContext())) {
|
||||
setBackupsEnabled();
|
||||
} else {
|
||||
Log.w(TAG, "Cannot access backup directory. Disabling backups.");
|
||||
|
||||
BackupUtil.disableBackups(requireContext());
|
||||
setBackupsDisabled();
|
||||
}
|
||||
} else {
|
||||
setBackupsDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
private void setBackupSummary() {
|
||||
summary.setText(getString(R.string.BackupsPreferenceFragment__last_backup, BackupUtil.getLastBackupTime(requireContext(), Locale.getDefault())));
|
||||
}
|
||||
|
||||
private void setBackupFolderName() {
|
||||
folder.setVisibility(View.GONE);
|
||||
|
||||
if (BackupUtil.canUserAccessBackupDirectory(requireContext())) {
|
||||
if (BackupUtil.isUserSelectionRequired(requireContext()) &&
|
||||
BackupUtil.canUserAccessBackupDirectory(requireContext()))
|
||||
{
|
||||
Uri backupUri = Objects.requireNonNull(SignalStore.settings().getSignalBackupDirectory());
|
||||
DocumentFile backupFile = Objects.requireNonNull(DocumentFile.fromTreeUri(requireContext(), backupUri));
|
||||
|
||||
if (backupFile.getName() != null) {
|
||||
folder.setVisibility(View.VISIBLE);
|
||||
folderName.setText(backupFile.getName());
|
||||
}
|
||||
} else if (StorageUtil.canWriteInSignalStorageDir()) {
|
||||
try {
|
||||
folder.setVisibility(View.VISIBLE);
|
||||
folderName.setText(StorageUtil.getBackupDirectory().getPath());
|
||||
} catch (NoExternalStorageException e) {
|
||||
Log.w(TAG, "Could not display folder name.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setInfo() {
|
||||
String link = String.format("<a href=\"%s\">%s</a>", getString(R.string.backup_support_url), getString(R.string.BackupsPreferenceFragment__learn_more));
|
||||
String infoText = getString(R.string.BackupsPreferenceFragment__to_restore_a_backup, link);
|
||||
|
||||
info.setText(HtmlCompat.fromHtml(infoText, 0));
|
||||
info.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
private void onToggleClicked() {
|
||||
if (BackupUtil.isUserSelectionRequired(requireContext())) {
|
||||
onToggleClickedApi29();
|
||||
} else {
|
||||
onToggleClickedLegacy();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
private void onToggleClickedApi29() {
|
||||
if (!TextSecurePreferences.isBackupEnabled(requireContext())) {
|
||||
BackupDialog.showChooseBackupLocationDialog(this, CHOOSE_BACKUPS_LOCATION_REQUEST_CODE);
|
||||
} else {
|
||||
BackupDialog.showDisableBackupDialog(requireContext(), this::setBackupsDisabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void onToggleClickedLegacy() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
if (!TextSecurePreferences.isBackupEnabled(requireContext())) {
|
||||
BackupDialog.showEnableBackupDialog(requireContext(), null, null, this::setBackupsEnabled);
|
||||
} else {
|
||||
BackupDialog.showDisableBackupDialog(requireContext(), this::setBackupsDisabled);
|
||||
}
|
||||
})
|
||||
.withPermanentDenialDialog(getString(R.string.BackupsPreferenceFragment_signal_requires_external_storage_permission_in_order_to_create_backups))
|
||||
.execute();
|
||||
}
|
||||
|
||||
private void onCreateClicked() {
|
||||
if (BackupUtil.isUserSelectionRequired(requireContext())) {
|
||||
onCreateClickedApi29();
|
||||
} else {
|
||||
onCreateClickedLegacy();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
private void onCreateClickedApi29() {
|
||||
Log.i(TAG, "Queing backup...");
|
||||
LocalBackupJob.enqueue(true);
|
||||
}
|
||||
|
||||
private void onCreateClickedLegacy() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
Log.i(TAG, "Queuing backup...");
|
||||
LocalBackupJob.enqueue(true);
|
||||
})
|
||||
.withPermanentDenialDialog(getString(R.string.BackupsPreferenceFragment_signal_requires_external_storage_permission_in_order_to_create_backups))
|
||||
.execute();
|
||||
}
|
||||
|
||||
private void setBackupsEnabled() {
|
||||
toggle.setText(R.string.BackupsPreferenceFragment__turn_off);
|
||||
create.setVisibility(View.VISIBLE);
|
||||
verify.setVisibility(View.VISIBLE);
|
||||
setBackupFolderName();
|
||||
}
|
||||
|
||||
private void setBackupsDisabled() {
|
||||
toggle.setText(R.string.BackupsPreferenceFragment__turn_on);
|
||||
create.setVisibility(View.GONE);
|
||||
folder.setVisibility(View.GONE);
|
||||
verify.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
|
@ -1,33 +1,25 @@
|
|||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.backup.BackupDialog;
|
||||
import org.thoughtcrime.securesms.backup.FullBackupBase.BackupEvent;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
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.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
|
@ -46,16 +38,12 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||
findPreference(TextSecurePreferences.MESSAGE_BODY_TEXT_SIZE_PREF)
|
||||
.setOnPreferenceChangeListener(new ListSummaryListener());
|
||||
|
||||
findPreference(TextSecurePreferences.BACKUP_ENABLED)
|
||||
.setOnPreferenceClickListener(new BackupClickListener());
|
||||
findPreference(TextSecurePreferences.BACKUP_NOW)
|
||||
.setOnPreferenceClickListener(new BackupCreateListener());
|
||||
findPreference(TextSecurePreferences.BACKUP_PASSPHRASE_VERIFY)
|
||||
.setOnPreferenceClickListener(new BackupVerifyListener());
|
||||
findPreference(TextSecurePreferences.BACKUP).setOnPreferenceClickListener(unused -> {
|
||||
goToBackupsPreferenceFragment();
|
||||
return true;
|
||||
});
|
||||
|
||||
initializeListSummary((ListPreference) findPreference(TextSecurePreferences.MESSAGE_BODY_TEXT_SIZE_PREF));
|
||||
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -68,7 +56,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||
super.onResume();
|
||||
((ApplicationPreferencesActivity)getActivity()).getSupportActionBar().setTitle(R.string.preferences__chats);
|
||||
setMediaDownloadSummaries();
|
||||
setBackupSummary();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -82,24 +69,8 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(BackupEvent event) {
|
||||
ProgressPreference preference = (ProgressPreference)findPreference(TextSecurePreferences.BACKUP_NOW);
|
||||
|
||||
if (event.getType() == BackupEvent.Type.PROGRESS) {
|
||||
preference.setEnabled(false);
|
||||
preference.setSummary(getString(R.string.ChatsPreferenceFragment_in_progress));
|
||||
preference.setProgress(event.getCount());
|
||||
} else if (event.getType() == BackupEvent.Type.FINISHED) {
|
||||
preference.setEnabled(true);
|
||||
preference.setProgressVisible(false);
|
||||
setBackupSummary();
|
||||
}
|
||||
}
|
||||
|
||||
private void setBackupSummary() {
|
||||
findPreference(TextSecurePreferences.BACKUP_NOW)
|
||||
.setSummary(String.format(getString(R.string.ChatsPreferenceFragment_last_backup_s), BackupUtil.getLastBackupTime(getContext(), Locale.getDefault())));
|
||||
private void goToBackupsPreferenceFragment() {
|
||||
((ApplicationPreferencesActivity) requireActivity()).pushFragment(new BackupsPreferenceFragment());
|
||||
}
|
||||
|
||||
private void setMediaDownloadSummaries() {
|
||||
|
@ -124,51 +95,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||
: TextUtils.join(", ", outValues);
|
||||
}
|
||||
|
||||
private class BackupClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Permissions.with(ChatsPreferenceFragment.this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
if (!((SwitchPreferenceCompat)preference).isChecked()) {
|
||||
BackupDialog.showEnableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference);
|
||||
} else {
|
||||
BackupDialog.showDisableBackupDialog(getActivity(), (SwitchPreferenceCompat)preference);
|
||||
}
|
||||
})
|
||||
.withPermanentDenialDialog(getString(R.string.ChatsPreferenceFragment_signal_requires_external_storage_permission_in_order_to_create_backups))
|
||||
.execute();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class BackupCreateListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Permissions.with(ChatsPreferenceFragment.this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
Log.i(TAG, "Starting backup from user");
|
||||
LocalBackupJob.enqueue(true);
|
||||
})
|
||||
.withPermanentDenialDialog(getString(R.string.ChatsPreferenceFragment_signal_requires_external_storage_permission_in_order_to_create_backups))
|
||||
.execute();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class BackupVerifyListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
BackupDialog.showVerifyBackupPassphraseDialog(requireContext());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class MediaDownloadChangeListener implements Preference.OnPreferenceChangeListener {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ChooseBackupFragment extends BaseRegistrationFragment {
|
||||
|
||||
private static final String TAG = Log.tag(ChooseBackupFragment.class);
|
||||
|
||||
private static final short OPEN_FILE_REQUEST_CODE = 3862;
|
||||
|
||||
private View chooseBackupButton;
|
||||
private TextView learnMore;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
return inflater.inflate(R.layout.fragment_registration_choose_backup, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
if (BackupUtil.isUserSelectionRequired(requireContext())) {
|
||||
chooseBackupButton = view.findViewById(R.id.choose_backup_fragment_button);
|
||||
chooseBackupButton.setOnClickListener(this::onChooseBackupSelected);
|
||||
|
||||
learnMore = view.findViewById(R.id.choose_backup_fragment_learn_more);
|
||||
learnMore.setText(HtmlCompat.fromHtml(String.format("<a href=\"%s\">%s</a>", getString(R.string.backup_support_url), getString(R.string.ChooseBackupFragment__learn_more)), 0));
|
||||
learnMore.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
} else {
|
||||
Log.i(TAG, "User Selection is not required. Skipping.");
|
||||
Navigation.findNavController(requireView()).navigate(ChooseBackupFragmentDirections.actionSkip());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == OPEN_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) {
|
||||
ChooseBackupFragmentDirections.ActionRestore restore = ChooseBackupFragmentDirections.actionRestore();
|
||||
|
||||
restore.setUri(data.getData());
|
||||
|
||||
Navigation.findNavController(requireView()).navigate(restore);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(21)
|
||||
private void onChooseBackupSelected(@NonNull View view) {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
|
||||
intent.setType("application/octet-stream");
|
||||
|
||||
startActivityForResult(intent, OPEN_FILE_REQUEST_CODE);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,14 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
|
@ -22,7 +25,9 @@ import android.widget.Toast;
|
|||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
|
@ -40,21 +45,24 @@ import org.thoughtcrime.securesms.backup.FullBackupImporter;
|
|||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
||||
|
||||
private static final String TAG = Log.tag(RestoreBackupFragment.class);
|
||||
private static final String TAG = Log.tag(RestoreBackupFragment.class);
|
||||
private static final short OPEN_DOCUMENT_TREE_RESULT_CODE = 13782;
|
||||
|
||||
private TextView restoreBackupSize;
|
||||
private TextView restoreBackupTime;
|
||||
|
@ -102,35 +110,68 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!Permissions.hasAll(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
RestoreBackupFragmentArgs args = RestoreBackupFragmentArgs.fromBundle(requireArguments());
|
||||
if (BackupUtil.isUserSelectionRequired(requireContext()) && args.getUri() != null) {
|
||||
Log.i(TAG, "Restoring backup from passed uri");
|
||||
initializeBackupForUri(view, args.getUri());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (BackupUtil.canUserAccessBackupDirectory(requireContext())) {
|
||||
initializeBackupDetection(view);
|
||||
} else {
|
||||
Log.i(TAG, "Skipping backup detection. We don't have the permission.");
|
||||
Navigation.findNavController(view)
|
||||
.navigate(RestoreBackupFragmentDirections.actionSkipNoReturn());
|
||||
} else {
|
||||
initializeBackupDetection(view);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == OPEN_DOCUMENT_TREE_RESULT_CODE && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
|
||||
Uri backupDirectoryUri = data.getData();
|
||||
int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
|
||||
SignalStore.settings().setSignalBackupDirectory(backupDirectoryUri);
|
||||
requireContext().getContentResolver()
|
||||
.takePersistableUriPermission(backupDirectoryUri, takeFlags);
|
||||
|
||||
enableBackups(requireContext());
|
||||
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(RestoreBackupFragmentDirections.actionBackupRestored());
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
private void initializeBackupForUri(@NonNull View view, @NonNull Uri uri) {
|
||||
getFromUri(requireContext(), uri, backup -> handleBackupInfo(view, backup));
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void initializeBackupDetection(@NonNull View view) {
|
||||
searchForBackup(backup -> {
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
Log.i(TAG, "No context on fragment, must have navigated away.");
|
||||
return;
|
||||
}
|
||||
searchForBackup(backup -> handleBackupInfo(view, backup));
|
||||
}
|
||||
|
||||
if (backup == null) {
|
||||
Log.i(TAG, "Skipping backup detection. No backup found, or permission revoked since.");
|
||||
Navigation.findNavController(view)
|
||||
.navigate(RestoreBackupFragmentDirections.actionNoBackupFound());
|
||||
} else {
|
||||
restoreBackupSize.setText(getString(R.string.RegistrationActivity_backup_size_s, Util.getPrettyFileSize(backup.getSize())));
|
||||
restoreBackupTime.setText(getString(R.string.RegistrationActivity_backup_timestamp_s, DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), backup.getTimestamp())));
|
||||
private void handleBackupInfo(@NonNull View view, @Nullable BackupUtil.BackupInfo backup) {
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
Log.i(TAG, "No context on fragment, must have navigated away.");
|
||||
return;
|
||||
}
|
||||
|
||||
restoreButton.setOnClickListener((v) -> handleRestore(v.getContext(), backup));
|
||||
}
|
||||
});
|
||||
if (backup == null) {
|
||||
Log.i(TAG, "Skipping backup detection. No backup found, or permission revoked since.");
|
||||
Navigation.findNavController(view)
|
||||
.navigate(RestoreBackupFragmentDirections.actionNoBackupFound());
|
||||
} else {
|
||||
restoreBackupSize.setText(getString(R.string.RegistrationActivity_backup_size_s, Util.getPrettyFileSize(backup.getSize())));
|
||||
restoreBackupTime.setText(getString(R.string.RegistrationActivity_backup_timestamp_s, DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), backup.getTimestamp())));
|
||||
|
||||
restoreButton.setOnClickListener((v) -> handleRestore(v.getContext(), backup));
|
||||
}
|
||||
}
|
||||
|
||||
interface OnBackupSearchResultListener {
|
||||
|
@ -159,6 +200,15 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
|||
}.execute();
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
static void getFromUri(@NonNull Context context,
|
||||
@NonNull Uri backupUri,
|
||||
@NonNull OnBackupSearchResultListener listener)
|
||||
{
|
||||
SimpleTask.run(() -> BackupUtil.getBackupInfoForUri(context, backupUri),
|
||||
listener::run);
|
||||
}
|
||||
|
||||
private void handleRestore(@NonNull Context context, @NonNull BackupUtil.BackupInfo backup) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
|
||||
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
|
||||
|
@ -198,19 +248,18 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
|||
|
||||
SQLiteDatabase database = DatabaseFactory.getBackupDatabase(context);
|
||||
|
||||
BackupPassphrase.set(context, passphrase);
|
||||
FullBackupImporter.importFile(context,
|
||||
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
||||
database,
|
||||
backup.getFile(),
|
||||
backup.getUri(),
|
||||
passphrase);
|
||||
|
||||
DatabaseFactory.upgradeRestored(context, database);
|
||||
NotificationChannels.restoreContactNotificationChannels(context);
|
||||
|
||||
LocalBackupListener.setNextBackupTimeToIntervalFromNow(context);
|
||||
BackupPassphrase.set(context, passphrase);
|
||||
TextSecurePreferences.setBackupEnabled(context, true);
|
||||
LocalBackupListener.schedule(context);
|
||||
enableBackups(context);
|
||||
|
||||
AppInitialization.onPostBackupRestore(context);
|
||||
|
||||
Log.i(TAG, "Backup restore complete.");
|
||||
|
@ -272,11 +321,48 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
|||
skipRestoreButton.setVisibility(View.INVISIBLE);
|
||||
|
||||
if (event.getType() == FullBackupBase.BackupEvent.Type.FINISHED) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(RestoreBackupFragmentDirections.actionBackupRestored());
|
||||
if (BackupUtil.isUserSelectionRequired(requireContext()) && !BackupUtil.canUserAccessBackupDirectory(requireContext())) {
|
||||
displayConfirmationDialog(requireContext());
|
||||
} else {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(RestoreBackupFragmentDirections.actionBackupRestored());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void enableBackups(@NonNull Context context) {
|
||||
if (BackupUtil.canUserAccessBackupDirectory(context)) {
|
||||
LocalBackupListener.setNextBackupTimeToIntervalFromNow(context);
|
||||
TextSecurePreferences.setBackupEnabled(context, true);
|
||||
LocalBackupListener.schedule(context);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
private void displayConfirmationDialog(@NonNull Context context) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.RestoreBackupFragment__re_enable_backups)
|
||||
.setMessage(R.string.RestoreBackupFragment__to_continue_using)
|
||||
.setPositiveButton(R.string.RestoreBackupFragment__choose_folder, (dialog, which) -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
startActivityForResult(intent, OPEN_DOCUMENT_TREE_RESULT_CODE);
|
||||
})
|
||||
.setNegativeButton(R.string.RestoreBackupFragment__keep_disabled, (dialog, which) -> {
|
||||
BackupPassphrase.set(context, null);
|
||||
dialog.dismiss();
|
||||
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(RestoreBackupFragmentDirections.actionBackupRestored());
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
|
||||
private enum BackupImportResult {
|
||||
SUCCESS,
|
||||
FAILURE_VERSION_DOWNGRADE,
|
||||
|
|
|
@ -8,9 +8,12 @@ import android.text.TextUtils;
|
|||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.navigation.ActivityNavigator;
|
||||
import androidx.navigation.Navigation;
|
||||
|
@ -23,6 +26,7 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
@ -32,7 +36,21 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
|||
|
||||
private static final String TAG = Log.tag(WelcomeFragment.class);
|
||||
|
||||
private static final String[] PERMISSIONS = { Manifest.permission.WRITE_CONTACTS,
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_PHONE_STATE };
|
||||
private static final String[] PERMISSIONS_API_29 = { Manifest.permission.WRITE_CONTACTS,
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.READ_PHONE_STATE };
|
||||
private static final @StringRes int RATIONALE = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends;
|
||||
private static final @StringRes int RATIONALE_API_29 = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_in_order_to_connect_with_friends;
|
||||
private static final int[] HEADERS = { R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp };
|
||||
private static final int[] HEADERS_API_29 = { R.drawable.ic_contacts_white_48dp };
|
||||
|
||||
private CircularProgressButton continueButton;
|
||||
private View restoreFromBackup;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
|
@ -75,7 +93,16 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
|||
continueButton = view.findViewById(R.id.welcome_continue_button);
|
||||
continueButton.setOnClickListener(this::continueClicked);
|
||||
|
||||
view.findViewById(R.id.welcome_terms_button).setOnClickListener(v -> onTermsClicked());
|
||||
restoreFromBackup = view.findViewById(R.id.welcome_restore_backup);
|
||||
restoreFromBackup.setOnClickListener(this::restoreFromBackupClicked);
|
||||
|
||||
TextView welcomeTermsButton = view.findViewById(R.id.welcome_terms_button);
|
||||
welcomeTermsButton.setOnClickListener(v -> onTermsClicked());
|
||||
|
||||
if (canUserSelectBackup()) {
|
||||
restoreFromBackup.setVisibility(View.VISIBLE);
|
||||
welcomeTermsButton.setTextColor(ContextCompat.getColor(requireActivity(), R.color.core_grey_60));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,18 +112,24 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
|||
}
|
||||
|
||||
private void continueClicked(@NonNull View view) {
|
||||
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext());
|
||||
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_CONTACTS,
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_PHONE_STATE)
|
||||
.request(getContinuePermissions(isUserSelectionRequired))
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends),
|
||||
R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp)
|
||||
.onAnyResult(() -> {
|
||||
gatherInformationAndContinue(continueButton);
|
||||
})
|
||||
.withRationaleDialog(getString(getContinueRationale(isUserSelectionRequired)), getContinueHeaders(isUserSelectionRequired))
|
||||
.onAnyResult(() -> gatherInformationAndContinue(continueButton))
|
||||
.execute();
|
||||
}
|
||||
|
||||
private void restoreFromBackupClicked(@NonNull View view) {
|
||||
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext());
|
||||
|
||||
Permissions.with(this)
|
||||
.request(getContinuePermissions(isUserSelectionRequired))
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(getContinueRationale(isUserSelectionRequired)), getContinueHeaders(isUserSelectionRequired))
|
||||
.onAnyResult(() -> gatherInformationAndChooseBackup(continueButton))
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
@ -127,6 +160,15 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
|||
});
|
||||
}
|
||||
|
||||
private void gatherInformationAndChooseBackup(@NonNull View view) {
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true);
|
||||
|
||||
initializeNumber();
|
||||
|
||||
Navigation.findNavController(view)
|
||||
.navigate(WelcomeFragmentDirections.actionChooseBackup());
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private void initializeNumber() {
|
||||
Optional<Phonenumber.PhoneNumber> localNumber = Optional.absent();
|
||||
|
@ -149,4 +191,22 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
|||
private void onTermsClicked() {
|
||||
CommunicationActions.openBrowserLink(requireContext(), RegistrationConstants.TERMS_AND_CONDITIONS_URL);
|
||||
}
|
||||
|
||||
private boolean canUserSelectBackup() {
|
||||
return BackupUtil.isUserSelectionRequired(requireContext()) &&
|
||||
!isReregister() &&
|
||||
!TextSecurePreferences.isBackupEnabled(requireContext());
|
||||
}
|
||||
|
||||
private static String[] getContinuePermissions(boolean isUserSelectionRequired) {
|
||||
return isUserSelectionRequired ? PERMISSIONS_API_29 : PERMISSIONS;
|
||||
}
|
||||
|
||||
private static @StringRes int getContinueRationale(boolean isUserSelectionRequired) {
|
||||
return isUserSelectionRequired ? RATIONALE_API_29 : RATIONALE;
|
||||
}
|
||||
|
||||
private static int[] getContinueHeaders(boolean isUserSelectionRequired) {
|
||||
return isUserSelectionRequired ? HEADERS_API_29 : HEADERS;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,6 @@ import org.thoughtcrime.securesms.util.TelephonyUtil;
|
|||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
|
||||
import org.thoughtcrime.securesms.webrtc.IncomingPstnCallReceiver;
|
||||
import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.BluetoothStateManager;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger;
|
||||
|
@ -193,7 +192,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
|||
private WiredHeadsetStateReceiver wiredHeadsetStateReceiver;
|
||||
private PowerButtonReceiver powerButtonReceiver;
|
||||
private LockManager lockManager;
|
||||
private IncomingPstnCallReceiver callReceiver;
|
||||
private UncaughtExceptionHandlerManager uncaughtExceptionHandlerManager;
|
||||
|
||||
@Nullable private CallManager callManager;
|
||||
|
@ -220,7 +218,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
|||
|
||||
initializeResources();
|
||||
|
||||
registerIncomingPstnCallReceiver();
|
||||
registerUncaughtExceptionHandler();
|
||||
registerWiredHeadsetStateReceiver();
|
||||
|
||||
|
@ -298,10 +295,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
|||
callManager = null;
|
||||
}
|
||||
|
||||
if (callReceiver != null) {
|
||||
unregisterReceiver(callReceiver);
|
||||
}
|
||||
|
||||
if (uncaughtExceptionHandlerManager != null) {
|
||||
uncaughtExceptionHandlerManager.unregister();
|
||||
}
|
||||
|
@ -363,11 +356,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
|||
}
|
||||
}
|
||||
|
||||
private void registerIncomingPstnCallReceiver() {
|
||||
callReceiver = new IncomingPstnCallReceiver();
|
||||
registerReceiver(callReceiver, new IntentFilter("android.intent.action.PHONE_STATE"));
|
||||
}
|
||||
|
||||
private void registerUncaughtExceptionHandler() {
|
||||
uncaughtExceptionHandlerManager = new UncaughtExceptionHandlerManager();
|
||||
uncaughtExceptionHandlerManager.registerHandler(new ProximityLockRelease(lockManager));
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.backup.BackupPassphrase;
|
||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -18,6 +28,7 @@ import java.util.Calendar;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
public class BackupUtil {
|
||||
|
||||
|
@ -37,6 +48,24 @@ public class BackupUtil {
|
|||
}
|
||||
}
|
||||
|
||||
public static boolean isUserSelectionRequired(@NonNull Context context) {
|
||||
return Build.VERSION.SDK_INT >= 29 && !Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
}
|
||||
|
||||
public static boolean canUserAccessBackupDirectory(@NonNull Context context) {
|
||||
if (isUserSelectionRequired(context)) {
|
||||
Uri backupDirectoryUri = SignalStore.settings().getSignalBackupDirectory();
|
||||
if (backupDirectoryUri == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DocumentFile backupDirectory = DocumentFile.fromTreeUri(context, backupDirectoryUri);
|
||||
return backupDirectory != null && backupDirectory.exists() && backupDirectory.canRead() && backupDirectory.canWrite();
|
||||
} else {
|
||||
return Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable BackupInfo getLatestBackup() throws NoExternalStorageException {
|
||||
List<BackupInfo> backups = getAllBackupsNewestFirst();
|
||||
|
||||
|
@ -71,17 +100,96 @@ public class BackupUtil {
|
|||
}
|
||||
}
|
||||
|
||||
public static void disableBackups(@NonNull Context context) {
|
||||
BackupPassphrase.set(context, null);
|
||||
TextSecurePreferences.setBackupEnabled(context, false);
|
||||
BackupUtil.deleteAllBackups();
|
||||
|
||||
if (BackupUtil.isUserSelectionRequired(context)) {
|
||||
Uri backupLocationUri = SignalStore.settings().getSignalBackupDirectory();
|
||||
|
||||
if (backupLocationUri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
SignalStore.settings().clearSignalBackupDirectory();
|
||||
|
||||
try {
|
||||
context.getContentResolver()
|
||||
.releasePersistableUriPermission(Objects.requireNonNull(backupLocationUri),
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
} catch (SecurityException e) {
|
||||
Log.w(TAG, "Could not release permissions", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<BackupInfo> getAllBackupsNewestFirst() throws NoExternalStorageException {
|
||||
if (isUserSelectionRequired(ApplicationDependencies.getApplication())) {
|
||||
return getAllBackupsNewestFirstApi29();
|
||||
} else {
|
||||
return getAllBackupsNewestFirstLegacy();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
private static List<BackupInfo> getAllBackupsNewestFirstApi29() {
|
||||
Uri backupDirectoryUri = SignalStore.settings().getSignalBackupDirectory();
|
||||
if (backupDirectoryUri == null) {
|
||||
Log.i(TAG, "Backup directory is not set. Returning an empty list.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
DocumentFile backupDirectory = DocumentFile.fromTreeUri(ApplicationDependencies.getApplication(), backupDirectoryUri);
|
||||
if (backupDirectory == null || !backupDirectory.exists() || !backupDirectory.canRead()) {
|
||||
Log.w(TAG, "Backup directory is inaccessible. Returning an empty list.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
DocumentFile[] files = backupDirectory.listFiles();
|
||||
List<BackupInfo> backups = new ArrayList<>(files.length);
|
||||
|
||||
for (DocumentFile file : files) {
|
||||
if (file.isFile() && file.getName() != null && file.getName().endsWith(".backup")) {
|
||||
long backupTimestamp = getBackupTimestamp(file.getName());
|
||||
|
||||
if (backupTimestamp != -1) {
|
||||
backups.add(new BackupInfo(backupTimestamp, file.length(), file.getUri()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(backups, (a, b) -> Long.compare(b.timestamp, a.timestamp));
|
||||
|
||||
return backups;
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
public static @Nullable BackupInfo getBackupInfoForUri(@NonNull Context context, @NonNull Uri uri) {
|
||||
DocumentFile documentFile = DocumentFile.fromSingleUri(context, uri);
|
||||
|
||||
if (documentFile != null && documentFile.exists() && documentFile.canRead() && documentFile.canWrite() && documentFile.getName().endsWith(".backup")) {
|
||||
long backupTimestamp = getBackupTimestamp(documentFile.getName());
|
||||
|
||||
return new BackupInfo(backupTimestamp, documentFile.length(), documentFile.getUri());
|
||||
} else {
|
||||
Log.w(TAG, "Could not load backup info.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<BackupInfo> getAllBackupsNewestFirstLegacy() throws NoExternalStorageException {
|
||||
File backupDirectory = StorageUtil.getBackupDirectory();
|
||||
File[] files = backupDirectory.listFiles();
|
||||
List<BackupInfo> backups = new ArrayList<>(files.length);
|
||||
|
||||
for (File file : files) {
|
||||
if (file.isFile() && file.getAbsolutePath().endsWith(".backup")) {
|
||||
long backupTimestamp = getBackupTimestamp(file);
|
||||
long backupTimestamp = getBackupTimestamp(file.getName());
|
||||
|
||||
if (backupTimestamp != -1) {
|
||||
backups.add(new BackupInfo(backupTimestamp, file.length(), file));
|
||||
backups.add(new BackupInfo(backupTimestamp, file.length(), Uri.fromFile(file)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -104,9 +212,8 @@ public class BackupUtil {
|
|||
return result;
|
||||
}
|
||||
|
||||
private static long getBackupTimestamp(File backup) {
|
||||
String name = backup.getName();
|
||||
String[] prefixSuffix = name.split("[.]");
|
||||
private static long getBackupTimestamp(@NonNull String backupName) {
|
||||
String[] prefixSuffix = backupName.split("[.]");
|
||||
|
||||
if (prefixSuffix.length == 2) {
|
||||
String[] parts = prefixSuffix[0].split("\\-");
|
||||
|
@ -136,12 +243,12 @@ public class BackupUtil {
|
|||
|
||||
private final long timestamp;
|
||||
private final long size;
|
||||
private final File file;
|
||||
private final Uri uri;
|
||||
|
||||
BackupInfo(long timestamp, long size, File file) {
|
||||
BackupInfo(long timestamp, long size, Uri uri) {
|
||||
this.timestamp = timestamp;
|
||||
this.size = size;
|
||||
this.file = file;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
|
@ -152,16 +259,27 @@ public class BackupUtil {
|
|||
return size;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
private void delete() {
|
||||
Log.i(TAG, "Deleting: " + file.getAbsolutePath());
|
||||
DocumentFile document = DocumentFile.fromSingleUri(ApplicationDependencies.getApplication(), uri);
|
||||
if (document != null && document.exists()) {
|
||||
Log.i(TAG, "Deleting: " + uri);
|
||||
|
||||
if (!file.delete()) {
|
||||
Log.w(TAG, "Delete failed: " + file.getAbsolutePath());
|
||||
if (!document.delete()) {
|
||||
Log.w(TAG, "Delete failed: " + uri);
|
||||
}
|
||||
} else {
|
||||
File file = new File(uri.toString());
|
||||
Log.i(TAG, "Deleting: " + file.getAbsolutePath());
|
||||
|
||||
if (!file.delete()) {
|
||||
Log.w(TAG, "Delete failed: " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -141,12 +141,11 @@ 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 = "pref_backup";
|
||||
public static final String BACKUP_ENABLED = "pref_backup_enabled";
|
||||
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";
|
||||
public static final String BACKUP_NOW = "pref_backup_create";
|
||||
public static final String BACKUP_PASSPHRASE_VERIFY = "pref_backup_passphrase_verify";
|
||||
|
||||
public static final String SCREEN_LOCK = "pref_android_screen_lock";
|
||||
public static final String SCREEN_LOCK_TIMEOUT = "pref_android_screen_lock_timeout";
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
package org.thoughtcrime.securesms.webrtc;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.ResultReceiver;
|
||||
import android.telephony.TelephonyManager;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Listens for incoming PSTN calls and rejects them if a RedPhone call is already in progress.
|
||||
*
|
||||
* Unstable use of reflection employed to gain access to ITelephony.
|
||||
*
|
||||
*/
|
||||
public class IncomingPstnCallReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = IncomingPstnCallReceiver.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "Checking incoming call...");
|
||||
|
||||
if (intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) == null) {
|
||||
Log.w(TAG, "Telephony event does not contain number...");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!intent.getStringExtra(TelephonyManager.EXTRA_STATE).equals(TelephonyManager.EXTRA_STATE_RINGING)) {
|
||||
Log.w(TAG, "Telephony event is not state ringing...");
|
||||
return;
|
||||
}
|
||||
|
||||
InCallListener listener = new InCallListener(context, new Handler());
|
||||
|
||||
WebRtcCallService.isCallActive(context, listener);
|
||||
}
|
||||
|
||||
private static class InCallListener extends ResultReceiver {
|
||||
|
||||
private final Context context;
|
||||
|
||||
InCallListener(Context context, Handler handler) {
|
||||
super(handler);
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||
if (resultCode == 1) {
|
||||
Log.i(TAG, "Attempting to deny incoming PSTN call.");
|
||||
|
||||
TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
|
||||
try {
|
||||
Method getTelephony = tm.getClass().getDeclaredMethod("getITelephony");
|
||||
getTelephony.setAccessible(true);
|
||||
Object telephonyService = getTelephony.invoke(tm);
|
||||
Method endCall = telephonyService.getClass().getDeclaredMethod("endCall");
|
||||
endCall.invoke(telephonyService);
|
||||
Log.i(TAG, "Denied Incoming Call.");
|
||||
} catch (NoSuchMethodException e) {
|
||||
Log.w(TAG, "Unable to access ITelephony API", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
Log.w(TAG, "Unable to access ITelephony API", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
Log.w(TAG, "Unable to access ITelephony API", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/core_ultramarine">
|
||||
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="8dp" />
|
||||
<solid android:color="?colorAccent" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item android:id="@android:id/background">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="8dp" />
|
||||
<solid android:color="?colorAccent" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
</ripple>
|
10
app/src/main/res/drawable/ic_backup_outline_60.xml
Normal file
10
app/src/main/res/drawable/ic_backup_outline_60.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="60dp"
|
||||
android:height="60dp"
|
||||
android:viewportWidth="60"
|
||||
android:viewportHeight="60">
|
||||
<path
|
||||
android:pathData="M55.5,30C55.5,44.0833 44.0833,55.5 30,55.5C28.2887,55.5 26.6168,55.3314 25,55.01V57.5549C26.6224,57.8473 28.2934,58 30,58C45.464,58 58,45.464 58,30C58,14.536 45.464,2 30,2C14.536,2 2,14.536 2,30C2,39.2194 6.4557,47.398 13.3312,52.5H6V55H15.5H18V52.5V43H15.5V50.979C8.8535,46.3764 4.5,38.6966 4.5,30C4.5,15.9167 15.9167,4.5 30,4.5C44.0833,4.5 55.5,15.9167 55.5,30ZM27.896,34.5785C27.3911,34.5785 26.9171,34.3724 26.5564,34.022C26.1958,33.6614 26,33.177 26,32.6721L27.1747,15H28.6379L29.6993,30.8689L48.0103,31.9508V33.4037L27.896,34.5785Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_folder_outline_24.xml
Normal file
10
app/src/main/res/drawable/ic_folder_outline_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M2.1529,5.8928L2.1428,18.1071C2.1428,19.2268 3.0492,20.1428 4.1571,20.1428H20.2714C21.3792,20.1428 22.2857,19.2268 22.2857,18.1071V7.9285C22.2857,6.8089 21.3792,5.8928 20.2714,5.8928H12.2143L10.2,3.8571H4.1571C3.0492,3.8571 2.1529,4.7732 2.1529,5.8928ZM9.6634,5.1428H4.1571C3.7805,5.1428 3.4386,5.462 3.4386,5.8928L3.4285,18.1071C3.4285,18.1073 3.4285,18.107 3.4285,18.1071C3.4288,18.5293 3.7722,18.8571 4.1571,18.8571H20.2714C20.6564,18.8571 21,18.5295 21,18.1071V7.9285C21,7.5061 20.6564,7.1785 20.2714,7.1785H11.6777L9.6634,5.1428Z"
|
||||
android:fillColor="?colorAccent"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_folder_solid_24.xml
Normal file
9
app/src/main/res/drawable/ic_folder_solid_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M2.1428,18.1071L2.1529,5.8928C2.1529,4.7732 3.0492,3.8571 4.1571,3.8571H10.2L12.2143,5.8928H20.2714C21.3792,5.8928 22.2857,6.8089 22.2857,7.9285V18.1071C22.2857,19.2268 21.3792,20.1428 20.2714,20.1428H4.1571C3.0492,20.1428 2.1428,19.2268 2.1428,18.1071Z"
|
||||
android:fillColor="?colorAccent"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
||||
<solid android:color="?colorAccent"/>
|
||||
<corners android:radius="8dp"/>
|
||||
</shape>
|
23
app/src/main/res/layout/backup_choose_location_dialog.xml
Normal file
23
app/src/main/res/layout/backup_choose_location_dialog.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="36dp"
|
||||
android:layout_marginBottom="36dp"
|
||||
app:srcCompat="?attr/folder_icon" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:text="@string/BackupDialog_to_enable_backups_choose_a_folder"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
</LinearLayout>
|
126
app/src/main/res/layout/backup_enable_dialog_v29.xml
Normal file
126
app/src/main/res/layout/backup_enable_dialog_v29.xml
Normal file
|
@ -0,0 +1,126 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="23dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="23dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/backup_enable_dialog__folder"
|
||||
android:textAppearance="@style/Signal.Text.Caption"
|
||||
android:textColor="?attr/backup_enable_subhead_color"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/backup_enable_dialog_folder_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="22dp"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
tools:text="Documents" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:background="?attr/backup_enable_dialog_divider_background" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/backup_enable_dialog__you_must_have_this_passphrase"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
|
||||
<TableLayout
|
||||
android:id="@+id/number_table"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TableRow
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/code_first"
|
||||
style="@style/BackupPassphrase"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="22934" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/code_second"
|
||||
style="@style/BackupPassphrase"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
tools:text="56944" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/code_third"
|
||||
style="@style/BackupPassphrase"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
tools:text="42738" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/code_fourth"
|
||||
style="@style/BackupPassphrase"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="34431" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/code_fifth"
|
||||
style="@style/BackupPassphrase"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
tools:text="24922" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/code_sixth"
|
||||
style="@style/BackupPassphrase"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
tools:text="58594" />
|
||||
</TableRow>
|
||||
</TableLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/confirmation_check"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/confirmation_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/backup_enable_dialog__i_have_written_down_this_passphrase"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
180
app/src/main/res/layout/fragment_backups.xml
Normal file
180
app/src/main/res/layout/fragment_backups.xml
Normal file
|
@ -0,0 +1,180 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/BackupsPreferenceFragment__backups_are_encrypted_with_a_passphrase"
|
||||
android:textAppearance="@style/Signal.Text.Preview"
|
||||
android:textColor="?attr/title_text_color_secondary" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/fragment_backup_create"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:minHeight="?attr/listPreferredItemHeightLarge"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fragment_backup_create_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/BackupsPreferenceFragment__create_backup"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="?attr/title_text_color_primary"
|
||||
app:layout_constraintBottom_toTopOf="@id/fragment_backup_create_summary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fragment_backup_create_summary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/BackupsPreferenceFragment__last_backup"
|
||||
android:textAppearance="@style/Signal.Text.Preview"
|
||||
android:textColor="?attr/title_text_color_secondary"
|
||||
app:layout_constraintBottom_toTopOf="@id/fragment_backup_progress"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/fragment_backup_create_title" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/fragment_backup_progress"
|
||||
style="?android:progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@id/fragment_backup_progress_summary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/fragment_backup_create_summary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fragment_backup_progress_summary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textColor="?attr/title_text_color_secondary"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/fragment_backup_progress"
|
||||
tools:text="10000 so far..."
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/fragment_backup_folder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightLarge"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/BackupsPreferenceFragment__backup_folder"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="?attr/title_text_color_primary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fragment_backup_folder_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAppearance="@style/Signal.Text.Preview"
|
||||
android:textColor="?attr/title_text_color_secondary" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/fragment_backup_verify"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightLarge"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/BackupsPreferenceFragment__verify_backup_passphrase"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="?attr/title_text_color_primary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/BackupsPreferenceFragment__test_your_backup_passphrase"
|
||||
android:textAppearance="@style/Signal.Text.Preview"
|
||||
android:textColor="?attr/title_text_color_secondary" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/fragment_backup_toggle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:background="@drawable/primary_action_button_background"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:text="@string/BackupsPreferenceFragment__turn_on"
|
||||
android:textColor="@color/core_white" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/icon_tint_dark" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fragment_backup_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/BackupsPreferenceFragment__to_restore_a_backup"
|
||||
android:textAppearance="@style/Signal.Text.Preview"
|
||||
android:textColor="?attr/title_text_color_secondary" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
|
@ -0,0 +1,71 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/choose_backup_fragment_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="64dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/ChooseBackupFragment__restore_from_backup"
|
||||
android:textAppearance="@style/Signal.Text.Headline.Registration"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/choose_backup_fragment_message"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/ChooseBackupFragment__restore_your_messages_and_media"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/choose_backup_fragment_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/choose_backup_fragment_learn_more"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/ChooseBackupFragment__learn_more"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/choose_backup_fragment_message" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/choose_backup_fragment_icon"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:background="@drawable/circle_tintable"
|
||||
android:contentDescription="@string/ChooseBackupFragment__icon_content_description"
|
||||
android:padding="30dp"
|
||||
app:backgroundTint="@color/core_grey_02"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_backup_outline_60" />
|
||||
|
||||
<com.dd.CircularProgressButton
|
||||
android:id="@+id/choose_backup_fragment_button"
|
||||
style="@style/Button.Registration"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:cpb_textIdle="@string/ChooseBackupFragment__choose_backup"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -47,10 +48,29 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="@dimen/registration_button_bottom_margin"
|
||||
android:layout_marginBottom="17dp"
|
||||
app:cpb_textIdle="@string/RegistrationActivity_continue"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/welcome_restore_backup"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_goneMarginBottom="@dimen/registration_button_bottom_margin" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcome_restore_backup"
|
||||
style="@style/Signal.Text.Body.Registration"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/registration_activity__restore_backup"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/welcome_continue_button"
|
||||
app:layout_constraintStart_toStartOf="@+id/welcome_continue_button"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -27,6 +27,40 @@
|
|||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_choose_backup"
|
||||
app:destination="@id/chooseBackupFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/chooseBackupFragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.ChooseBackupFragment"
|
||||
android:label="fragment_choose_backup"
|
||||
tools:layout="@layout/fragment_registration_choose_backup">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_restore"
|
||||
app:destination="@id/restoreBackupFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||
app:popUpTo="@id/chooseBackupFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_skip"
|
||||
app:destination="@id/enterPhoneNumberFragment"
|
||||
app:enterAnim="@anim/slide_from_end"
|
||||
app:exitAnim="@anim/slide_to_start"
|
||||
app:popEnterAnim="@anim/slide_from_start"
|
||||
app:popExitAnim="@anim/slide_to_end" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
@ -220,6 +254,11 @@
|
|||
app:popUpTo="@+id/restoreBackupFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<argument app:nullable="true"
|
||||
app:argType="android.net.Uri"
|
||||
android:defaultValue="@null"
|
||||
android:name="uri" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
<attr name="title_text_color_secondary" format="color"/>
|
||||
<attr name="title_text_color_disabled" format="color"/>
|
||||
|
||||
<attr name="folder_icon" format="reference" />
|
||||
<attr name="backup_enable_subhead_color" format="color" />
|
||||
<attr name="backup_enable_dialog_divider_background" format="color" />
|
||||
|
||||
<attr name="attachment_type_selector_background" format="color"/>
|
||||
<attr name="attachment_document_icon_small" format="reference" />
|
||||
<attr name="attachment_document_icon_large" format="reference" />
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
<string name="install_url" translatable="false">https://signal.org/install</string>
|
||||
<string name="donate_url" translatable="false">https://signal.org/donate</string>
|
||||
<string name="backup_support_url" translatable="false">https://support.signal.org/hc/articles/360007059752</string>
|
||||
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
|
@ -385,6 +386,36 @@
|
|||
<string name="CreateProfileActivity_signal_profiles_are_end_to_end_encrypted">Your profile is end-to-end encrypted. Your profile and changes to it will be visible to your contacts, when you initiate or accept new conversations, and when you join new groups.</string>
|
||||
<string name="CreateProfileActivity_set_avatar_description">Set avatar</string>
|
||||
|
||||
<!-- ChooseBackupFragment -->
|
||||
<string name="ChooseBackupFragment__restore_from_backup">Restore from Backup?</string>
|
||||
<string name="ChooseBackupFragment__restore_your_messages_and_media">Restore your messages and media from a local backup. If you don\'t restore now, you won\'t be able to restore later.</string>
|
||||
<string name="ChooseBackupFragment__icon_content_description">Restore from backup icon</string>
|
||||
<string name="ChooseBackupFragment__choose_backup">Choose backup</string>
|
||||
<string name="ChooseBackupFragment__learn_more">Learn more</string>
|
||||
|
||||
<!-- RestoreBackupFragment -->
|
||||
<string name="RestoreBackupFragment__re_enable_backups">Re-enable Backups?</string>
|
||||
<string name="RestoreBackupFragment__to_continue_using">To continue using backups, please choose where they should be saved."</string>
|
||||
<string name="RestoreBackupFragment__choose_folder">Choose folder</string>
|
||||
<string name="RestoreBackupFragment__keep_disabled">Keep disabled</string>
|
||||
|
||||
<!-- BackupsPreferenceFragment -->
|
||||
<string name="BackupsPreferenceFragment__chat_backups">Chat backups</string>
|
||||
<string name="BackupsPreferenceFragment__backups_are_encrypted_with_a_passphrase">Backups are encrypted with a passphrase and stored on your device</string>
|
||||
<string name="BackupsPreferenceFragment__create_backup">Create backup</string>
|
||||
<string name="BackupsPreferenceFragment__last_backup">Last backup: %1$s</string>
|
||||
<string name="BackupsPreferenceFragment__backup_folder">Backup folder</string>
|
||||
<string name="BackupsPreferenceFragment__verify_backup_passphrase">Verify backup passphrase</string>
|
||||
<string name="BackupsPreferenceFragment__test_your_backup_passphrase">Test your backup passphrase and verify that it matches</string>
|
||||
<string name="BackupsPreferenceFragment__turn_on">Turn on</string>
|
||||
<string name="BackupsPreferenceFragment__turn_off">Turn off</string>
|
||||
<string name="BackupsPreferenceFragment__to_restore_a_backup">To restore a backup, install a new copy of Signal. Open the app and tap "Restore backup", then locate a backup folder. %1$s</string>
|
||||
<string name="BackupsPreferenceFragment__learn_more">Learn more</string>
|
||||
<string name="BackupsPreferenceFragment__in_progress">In progress…</string>
|
||||
<string name="BackupsPreferenceFragment__d_so_far">%1$d so far…</string>
|
||||
<string name="BackupsPreferenceFragment_signal_requires_external_storage_permission_in_order_to_create_backups">Signal 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\".</string>
|
||||
|
||||
|
||||
<!-- CustomDefaultPreference -->
|
||||
<string name="CustomDefaultPreference_using_custom">Using custom: %s</string>
|
||||
<string name="CustomDefaultPreference_using_default">Using default: %s</string>
|
||||
|
@ -1267,6 +1298,7 @@
|
|||
<string name="RegistrationActivity_more_information">More information</string>
|
||||
<string name="RegistrationActivity_less_information">Less information</string>
|
||||
<string name="RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends">Signal needs access to your contacts and media in order to connect with friends, exchange messages, and make secure calls</string>
|
||||
<string name="RegistrationActivity_signal_needs_access_to_your_contacts_in_order_to_connect_with_friends">Signal needs access to your contacts in order to connect with friends, exchange messages, and make secure calls</string>
|
||||
<string name="RegistrationActivity_rate_limited_to_service">You\'ve made too many attempts to register this number. Please try again later.</string>
|
||||
<string name="RegistrationActivity_unable_to_connect_to_service">Unable to connect to service. Please check network connection and try again.</string>
|
||||
<string name="RegistrationActivity_to_easily_verify_your_phone_number_signal_can_automatically_detect_your_verification_code">To easily verify your phone number, Signal can automatically detect your verification code if you allow Signal to view SMS messages.</string>
|
||||
|
@ -2549,6 +2581,8 @@
|
|||
<string name="PushDecryptJob_unlock_to_view_pending_messages">Unlock to view pending messages</string>
|
||||
<string name="enter_backup_passphrase_dialog__backup_passphrase">Backup passphrase</string>
|
||||
<string name="backup_enable_dialog__backups_will_be_saved_to_external_storage_and_encrypted_with_the_passphrase_below_you_must_have_this_passphrase_in_order_to_restore_a_backup">Backups will be saved to external storage and encrypted with the passphrase below. You must have this passphrase in order to restore a backup.</string>
|
||||
<string name="backup_enable_dialog__you_must_have_this_passphrase">You must have this passphrase in order to restore a backup.</string>
|
||||
<string name="backup_enable_dialog__folder">Folder</string>
|
||||
<string name="backup_enable_dialog__i_have_written_down_this_passphrase">I have written down this passphrase. Without it, I will be unable to restore a backup.</string>
|
||||
<string name="registration_activity__restore_backup">Restore backup</string>
|
||||
<string name="registration_activity__skip">Skip</string>
|
||||
|
@ -2574,6 +2608,8 @@
|
|||
<string name="BackupDialog_delete_backups">Delete backups?</string>
|
||||
<string name="BackupDialog_disable_and_delete_all_local_backups">Disable and delete all local backups?</string>
|
||||
<string name="BackupDialog_delete_backups_statement">Delete backups</string>
|
||||
<string name="BackupDialog_to_enable_backups_choose_a_folder">To enable backups, choose a folder. Backups will be saved to this location.</string>
|
||||
<string name="BackupDialog_choose_folder">Choose folder</string>
|
||||
<string name="BackupDialog_copied_to_clipboard">Copied to clipboard</string>
|
||||
<string name="BackupDialog_enter_backup_passphrase_to_verify">Enter your backup passphrase to verify</string>
|
||||
<string name="BackupDialog_verify">Verify</string>
|
||||
|
@ -2583,6 +2619,8 @@
|
|||
<string name="ChatsPreferenceFragment_last_backup_s">Last backup: %s</string>
|
||||
<string name="ChatsPreferenceFragment_in_progress">In progress</string>
|
||||
<string name="LocalBackupJob_creating_backup">Creating backup…</string>
|
||||
<string name="LocalBackupJobApi29_backups_disabled">Backups disabled.</string>
|
||||
<string name="LocalBackupJobApi29_your_backup_directory_has_been_deleted_or_moved">Your backup directory has been deleted or moved.</string>
|
||||
<string name="ProgressPreference_d_messages_so_far">%d messages so far</string>
|
||||
<string name="RegistrationActivity_please_enter_the_verification_code_sent_to_s">Please enter the verification code sent to %s.</string>
|
||||
<string name="RegistrationActivity_wrong_number">Wrong number</string>
|
||||
|
|
|
@ -163,6 +163,10 @@
|
|||
<item name="icon_tint">@color/core_grey_75</item>
|
||||
<item name="icon_tint_dark">@color/core_grey_15</item>
|
||||
|
||||
<item name="folder_icon">@drawable/ic_folder_outline_24</item>
|
||||
<item name="backup_enable_dialog_divider_background">@color/core_grey_20</item>
|
||||
<item name="backup_enable_subhead_color">@color/core_grey_65</item>
|
||||
|
||||
<item name="insight_modal_background">@drawable/insights_modal_background</item>
|
||||
<item name="insight_modal_button_background">@color/core_grey_10</item>
|
||||
<item name="insight_title">@color/core_grey_90</item>
|
||||
|
@ -494,6 +498,10 @@
|
|||
<item name="icon_tint">@color/core_grey_15</item>
|
||||
<item name="icon_tint_dark">?icon_tint</item>
|
||||
|
||||
<item name="folder_icon">@drawable/ic_folder_solid_24</item>
|
||||
<item name="backup_enable_dialog_divider_background">@color/core_grey_60</item>
|
||||
<item name="backup_enable_subhead_color">@color/core_grey_25</item>
|
||||
|
||||
<item name="insight_modal_background">@drawable/insights_modal_background_dark</item>
|
||||
<item name="insight_modal_button_background">@color/core_grey_60</item>
|
||||
<item name="insight_title">@color/core_grey_25</item>
|
||||
|
|
|
@ -62,26 +62,11 @@
|
|||
<PreferenceCategory android:layout="@layout/preference_divider"/>
|
||||
|
||||
<PreferenceCategory android:key="backup_category" android:title="@string/preferences_chats__backups">
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="pref_backup_enabled"
|
||||
android:title="@string/preferences_chats__chat_backups"
|
||||
android:summary="@string/preferences_chats__backup_chats_to_external_storage" />
|
||||
|
||||
<org.thoughtcrime.securesms.preferences.widgets.ProgressPreference
|
||||
android:key="pref_backup_create"
|
||||
android:title="@string/preferences_chats__create_backup"
|
||||
android:persistent="false"
|
||||
android:dependency="pref_backup_enabled"
|
||||
tools:summary="Last backup: 3 days ago"/>
|
||||
|
||||
<androidx.preference.Preference
|
||||
android:key="pref_backup_passphrase_verify"
|
||||
android:title="@string/preferences_chats__verify_backup_passphrase"
|
||||
android:key="pref_backup"
|
||||
android:summary="@string/preferences_chats__backup_chats_to_external_storage"
|
||||
android:persistent="false"
|
||||
android:dependency="pref_backup_enabled"
|
||||
android:summary="@string/preferences_chats__test_your_backup_passphrase_and_verify_that_it_matches"/>
|
||||
|
||||
android:title="@string/preferences_chats__chat_backups" />
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
|
|
@ -6,15 +6,12 @@ import org.thoughtcrime.securesms.contacts.sync.FuzzyPhoneNumberHelper.OutputRes
|
|||
import org.thoughtcrime.securesms.contacts.sync.FuzzyPhoneNumberHelper.OutputResultV2;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import edu.emory.mathcs.backport.java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.thoughtcrime.securesms.testutil.TestHelpers.mapOf;
|
||||
|
|
|
@ -6,10 +6,9 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.testutil.LogRecorder;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
import edu.emory.mathcs.backport.java.util.Collections;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
|
|
@ -9,8 +9,7 @@ import org.robolectric.RobolectricTestRunner;
|
|||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import edu.emory.mathcs.backport.java.util.Collections;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
|
1
app/src/test/resources/robolectric.properties
Normal file
1
app/src/test/resources/robolectric.properties
Normal file
|
@ -0,0 +1 @@
|
|||
sdk=28
|
Loading…
Add table
Reference in a new issue