Implement new workflow for scoped storage backup selection.

This commit is contained in:
Alex Hart
2020-10-15 16:12:53 -03:00
committed by Greyson Parrelli
parent 9a1c869efe
commit ee3d7a9a35
39 changed files with 1582 additions and 280 deletions

View File

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

View File

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

View File

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