Implement new workflow for scoped storage backup selection.

fork-5.53.8
Alex Hart 2020-10-15 16:12:53 -03:00 zatwierdzone przez Greyson Parrelli
rodzic 9a1c869efe
commit ee3d7a9a35
39 zmienionych plików z 1582 dodań i 280 usunięć

Wyświetl plik

@ -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'

Wyświetl plik

@ -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());
}

Wyświetl plik

@ -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();

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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());

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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));

Wyświetl plik

@ -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());
}
}
}
}
}

Wyświetl plik

@ -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";

Wyświetl plik

@ -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);
}
}
}
}
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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

Wyświetl plik

@ -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" />

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -0,0 +1 @@
sdk=28