mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 00:59:49 +01:00
Implement new workflow for scoped storage backup selection.
This commit is contained in:
committed by
Greyson Parrelli
parent
9a1c869efe
commit
ee3d7a9a35
@@ -0,0 +1,76 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ChooseBackupFragment extends BaseRegistrationFragment {
|
||||
|
||||
private static final String TAG = Log.tag(ChooseBackupFragment.class);
|
||||
|
||||
private static final short OPEN_FILE_REQUEST_CODE = 3862;
|
||||
|
||||
private View chooseBackupButton;
|
||||
private TextView learnMore;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
return inflater.inflate(R.layout.fragment_registration_choose_backup, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
if (BackupUtil.isUserSelectionRequired(requireContext())) {
|
||||
chooseBackupButton = view.findViewById(R.id.choose_backup_fragment_button);
|
||||
chooseBackupButton.setOnClickListener(this::onChooseBackupSelected);
|
||||
|
||||
learnMore = view.findViewById(R.id.choose_backup_fragment_learn_more);
|
||||
learnMore.setText(HtmlCompat.fromHtml(String.format("<a href=\"%s\">%s</a>", getString(R.string.backup_support_url), getString(R.string.ChooseBackupFragment__learn_more)), 0));
|
||||
learnMore.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
} else {
|
||||
Log.i(TAG, "User Selection is not required. Skipping.");
|
||||
Navigation.findNavController(requireView()).navigate(ChooseBackupFragmentDirections.actionSkip());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == OPEN_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) {
|
||||
ChooseBackupFragmentDirections.ActionRestore restore = ChooseBackupFragmentDirections.actionRestore();
|
||||
|
||||
restore.setUri(data.getData());
|
||||
|
||||
Navigation.findNavController(requireView()).navigate(restore);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(21)
|
||||
private void onChooseBackupSelected(@NonNull View view) {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
|
||||
intent.setType("application/octet-stream");
|
||||
|
||||
startActivityForResult(intent, OPEN_FILE_REQUEST_CODE);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
@@ -22,7 +25,9 @@ import android.widget.Toast;
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
@@ -40,21 +45,24 @@ import org.thoughtcrime.securesms.backup.FullBackupImporter;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
||||
|
||||
private static final String TAG = Log.tag(RestoreBackupFragment.class);
|
||||
private static final String TAG = Log.tag(RestoreBackupFragment.class);
|
||||
private static final short OPEN_DOCUMENT_TREE_RESULT_CODE = 13782;
|
||||
|
||||
private TextView restoreBackupSize;
|
||||
private TextView restoreBackupTime;
|
||||
@@ -102,35 +110,68 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Permissions.hasAll(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
RestoreBackupFragmentArgs args = RestoreBackupFragmentArgs.fromBundle(requireArguments());
|
||||
if (BackupUtil.isUserSelectionRequired(requireContext()) && args.getUri() != null) {
|
||||
Log.i(TAG, "Restoring backup from passed uri");
|
||||
initializeBackupForUri(view, args.getUri());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (BackupUtil.canUserAccessBackupDirectory(requireContext())) {
|
||||
initializeBackupDetection(view);
|
||||
} else {
|
||||
Log.i(TAG, "Skipping backup detection. We don't have the permission.");
|
||||
Navigation.findNavController(view)
|
||||
.navigate(RestoreBackupFragmentDirections.actionSkipNoReturn());
|
||||
} else {
|
||||
initializeBackupDetection(view);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == OPEN_DOCUMENT_TREE_RESULT_CODE && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
|
||||
Uri backupDirectoryUri = data.getData();
|
||||
int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
|
||||
SignalStore.settings().setSignalBackupDirectory(backupDirectoryUri);
|
||||
requireContext().getContentResolver()
|
||||
.takePersistableUriPermission(backupDirectoryUri, takeFlags);
|
||||
|
||||
enableBackups(requireContext());
|
||||
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(RestoreBackupFragmentDirections.actionBackupRestored());
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
private void initializeBackupForUri(@NonNull View view, @NonNull Uri uri) {
|
||||
getFromUri(requireContext(), uri, backup -> handleBackupInfo(view, backup));
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void initializeBackupDetection(@NonNull View view) {
|
||||
searchForBackup(backup -> {
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
Log.i(TAG, "No context on fragment, must have navigated away.");
|
||||
return;
|
||||
}
|
||||
searchForBackup(backup -> handleBackupInfo(view, backup));
|
||||
}
|
||||
|
||||
if (backup == null) {
|
||||
Log.i(TAG, "Skipping backup detection. No backup found, or permission revoked since.");
|
||||
Navigation.findNavController(view)
|
||||
.navigate(RestoreBackupFragmentDirections.actionNoBackupFound());
|
||||
} else {
|
||||
restoreBackupSize.setText(getString(R.string.RegistrationActivity_backup_size_s, Util.getPrettyFileSize(backup.getSize())));
|
||||
restoreBackupTime.setText(getString(R.string.RegistrationActivity_backup_timestamp_s, DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), backup.getTimestamp())));
|
||||
private void handleBackupInfo(@NonNull View view, @Nullable BackupUtil.BackupInfo backup) {
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
Log.i(TAG, "No context on fragment, must have navigated away.");
|
||||
return;
|
||||
}
|
||||
|
||||
restoreButton.setOnClickListener((v) -> handleRestore(v.getContext(), backup));
|
||||
}
|
||||
});
|
||||
if (backup == null) {
|
||||
Log.i(TAG, "Skipping backup detection. No backup found, or permission revoked since.");
|
||||
Navigation.findNavController(view)
|
||||
.navigate(RestoreBackupFragmentDirections.actionNoBackupFound());
|
||||
} else {
|
||||
restoreBackupSize.setText(getString(R.string.RegistrationActivity_backup_size_s, Util.getPrettyFileSize(backup.getSize())));
|
||||
restoreBackupTime.setText(getString(R.string.RegistrationActivity_backup_timestamp_s, DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), backup.getTimestamp())));
|
||||
|
||||
restoreButton.setOnClickListener((v) -> handleRestore(v.getContext(), backup));
|
||||
}
|
||||
}
|
||||
|
||||
interface OnBackupSearchResultListener {
|
||||
@@ -159,6 +200,15 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
||||
}.execute();
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
static void getFromUri(@NonNull Context context,
|
||||
@NonNull Uri backupUri,
|
||||
@NonNull OnBackupSearchResultListener listener)
|
||||
{
|
||||
SimpleTask.run(() -> BackupUtil.getBackupInfoForUri(context, backupUri),
|
||||
listener::run);
|
||||
}
|
||||
|
||||
private void handleRestore(@NonNull Context context, @NonNull BackupUtil.BackupInfo backup) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
|
||||
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
|
||||
@@ -198,19 +248,18 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
||||
|
||||
SQLiteDatabase database = DatabaseFactory.getBackupDatabase(context);
|
||||
|
||||
BackupPassphrase.set(context, passphrase);
|
||||
FullBackupImporter.importFile(context,
|
||||
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
||||
database,
|
||||
backup.getFile(),
|
||||
backup.getUri(),
|
||||
passphrase);
|
||||
|
||||
DatabaseFactory.upgradeRestored(context, database);
|
||||
NotificationChannels.restoreContactNotificationChannels(context);
|
||||
|
||||
LocalBackupListener.setNextBackupTimeToIntervalFromNow(context);
|
||||
BackupPassphrase.set(context, passphrase);
|
||||
TextSecurePreferences.setBackupEnabled(context, true);
|
||||
LocalBackupListener.schedule(context);
|
||||
enableBackups(context);
|
||||
|
||||
AppInitialization.onPostBackupRestore(context);
|
||||
|
||||
Log.i(TAG, "Backup restore complete.");
|
||||
@@ -272,11 +321,48 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
||||
skipRestoreButton.setVisibility(View.INVISIBLE);
|
||||
|
||||
if (event.getType() == FullBackupBase.BackupEvent.Type.FINISHED) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(RestoreBackupFragmentDirections.actionBackupRestored());
|
||||
if (BackupUtil.isUserSelectionRequired(requireContext()) && !BackupUtil.canUserAccessBackupDirectory(requireContext())) {
|
||||
displayConfirmationDialog(requireContext());
|
||||
} else {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(RestoreBackupFragmentDirections.actionBackupRestored());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void enableBackups(@NonNull Context context) {
|
||||
if (BackupUtil.canUserAccessBackupDirectory(context)) {
|
||||
LocalBackupListener.setNextBackupTimeToIntervalFromNow(context);
|
||||
TextSecurePreferences.setBackupEnabled(context, true);
|
||||
LocalBackupListener.schedule(context);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
private void displayConfirmationDialog(@NonNull Context context) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.RestoreBackupFragment__re_enable_backups)
|
||||
.setMessage(R.string.RestoreBackupFragment__to_continue_using)
|
||||
.setPositiveButton(R.string.RestoreBackupFragment__choose_folder, (dialog, which) -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
startActivityForResult(intent, OPEN_DOCUMENT_TREE_RESULT_CODE);
|
||||
})
|
||||
.setNegativeButton(R.string.RestoreBackupFragment__keep_disabled, (dialog, which) -> {
|
||||
BackupPassphrase.set(context, null);
|
||||
dialog.dismiss();
|
||||
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(RestoreBackupFragmentDirections.actionBackupRestored());
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
|
||||
private enum BackupImportResult {
|
||||
SUCCESS,
|
||||
FAILURE_VERSION_DOWNGRADE,
|
||||
|
||||
@@ -8,9 +8,12 @@ import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.navigation.ActivityNavigator;
|
||||
import androidx.navigation.Navigation;
|
||||
@@ -23,6 +26,7 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -32,7 +36,21 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
||||
|
||||
private static final String TAG = Log.tag(WelcomeFragment.class);
|
||||
|
||||
private static final String[] PERMISSIONS = { Manifest.permission.WRITE_CONTACTS,
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_PHONE_STATE };
|
||||
private static final String[] PERMISSIONS_API_29 = { Manifest.permission.WRITE_CONTACTS,
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.READ_PHONE_STATE };
|
||||
private static final @StringRes int RATIONALE = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends;
|
||||
private static final @StringRes int RATIONALE_API_29 = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_in_order_to_connect_with_friends;
|
||||
private static final int[] HEADERS = { R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp };
|
||||
private static final int[] HEADERS_API_29 = { R.drawable.ic_contacts_white_48dp };
|
||||
|
||||
private CircularProgressButton continueButton;
|
||||
private View restoreFromBackup;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
@@ -75,7 +93,16 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
||||
continueButton = view.findViewById(R.id.welcome_continue_button);
|
||||
continueButton.setOnClickListener(this::continueClicked);
|
||||
|
||||
view.findViewById(R.id.welcome_terms_button).setOnClickListener(v -> onTermsClicked());
|
||||
restoreFromBackup = view.findViewById(R.id.welcome_restore_backup);
|
||||
restoreFromBackup.setOnClickListener(this::restoreFromBackupClicked);
|
||||
|
||||
TextView welcomeTermsButton = view.findViewById(R.id.welcome_terms_button);
|
||||
welcomeTermsButton.setOnClickListener(v -> onTermsClicked());
|
||||
|
||||
if (canUserSelectBackup()) {
|
||||
restoreFromBackup.setVisibility(View.VISIBLE);
|
||||
welcomeTermsButton.setTextColor(ContextCompat.getColor(requireActivity(), R.color.core_grey_60));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,18 +112,24 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
||||
}
|
||||
|
||||
private void continueClicked(@NonNull View view) {
|
||||
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext());
|
||||
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_CONTACTS,
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_PHONE_STATE)
|
||||
.request(getContinuePermissions(isUserSelectionRequired))
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends),
|
||||
R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp)
|
||||
.onAnyResult(() -> {
|
||||
gatherInformationAndContinue(continueButton);
|
||||
})
|
||||
.withRationaleDialog(getString(getContinueRationale(isUserSelectionRequired)), getContinueHeaders(isUserSelectionRequired))
|
||||
.onAnyResult(() -> gatherInformationAndContinue(continueButton))
|
||||
.execute();
|
||||
}
|
||||
|
||||
private void restoreFromBackupClicked(@NonNull View view) {
|
||||
boolean isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext());
|
||||
|
||||
Permissions.with(this)
|
||||
.request(getContinuePermissions(isUserSelectionRequired))
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(getContinueRationale(isUserSelectionRequired)), getContinueHeaders(isUserSelectionRequired))
|
||||
.onAnyResult(() -> gatherInformationAndChooseBackup(continueButton))
|
||||
.execute();
|
||||
}
|
||||
|
||||
@@ -127,6 +160,15 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
||||
});
|
||||
}
|
||||
|
||||
private void gatherInformationAndChooseBackup(@NonNull View view) {
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true);
|
||||
|
||||
initializeNumber();
|
||||
|
||||
Navigation.findNavController(view)
|
||||
.navigate(WelcomeFragmentDirections.actionChooseBackup());
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private void initializeNumber() {
|
||||
Optional<Phonenumber.PhoneNumber> localNumber = Optional.absent();
|
||||
@@ -149,4 +191,22 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
|
||||
private void onTermsClicked() {
|
||||
CommunicationActions.openBrowserLink(requireContext(), RegistrationConstants.TERMS_AND_CONDITIONS_URL);
|
||||
}
|
||||
|
||||
private boolean canUserSelectBackup() {
|
||||
return BackupUtil.isUserSelectionRequired(requireContext()) &&
|
||||
!isReregister() &&
|
||||
!TextSecurePreferences.isBackupEnabled(requireContext());
|
||||
}
|
||||
|
||||
private static String[] getContinuePermissions(boolean isUserSelectionRequired) {
|
||||
return isUserSelectionRequired ? PERMISSIONS_API_29 : PERMISSIONS;
|
||||
}
|
||||
|
||||
private static @StringRes int getContinueRationale(boolean isUserSelectionRequired) {
|
||||
return isUserSelectionRequired ? RATIONALE_API_29 : RATIONALE;
|
||||
}
|
||||
|
||||
private static int[] getContinueHeaders(boolean isUserSelectionRequired) {
|
||||
return isUserSelectionRequired ? HEADERS_API_29 : HEADERS;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user