From 321c84583b4c85131befefedb1a41320841b0675 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 30 Nov 2021 13:55:24 -0400 Subject: [PATCH] Ensure user leaves groups before deleting account. --- .../securesms/database/GroupDatabase.java | 8 +++ .../securesms/delete/DeleteAccountEvent.kt | 51 ++++++++++++++++++ .../delete/DeleteAccountFragment.java | 53 ++++++++++++------- .../delete/DeleteAccountProgressDialog.kt | 49 +++++++++++++++++ .../delete/DeleteAccountRepository.java | 40 +++++++++++--- .../delete/DeleteAccountViewModel.java | 30 ++++------- .../layout/delete_account_progress_dialog.xml | 50 +++++++++++++++++ app/src/main/res/values/strings.xml | 12 +++++ 8 files changed, 247 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountEvent.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountProgressDialog.kt create mode 100644 app/src/main/res/layout/delete_account_progress_dialog.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 121d62df1..a6ce6978f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -958,6 +958,14 @@ private static final String[] GROUP_PROJECTION = { return getCurrent(); } + public int getCount() { + if (cursor == null) { + return 0; + } else { + return cursor.getCount(); + } + } + public @Nullable GroupRecord getCurrent() { if (cursor == null || cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)) == null || cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID)) == 0) { return null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountEvent.kt new file mode 100644 index 000000000..4f641a3f4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountEvent.kt @@ -0,0 +1,51 @@ +package org.thoughtcrime.securesms.delete + +/** + * Account deletion event. + * + * @param type Specifies what type of event this is. Each type maps to a single class. This exists in order to facilitate + * legacy Java switch statement. + */ +sealed class DeleteAccountEvent(val type: Type) { + object NoCountryCode : DeleteAccountEvent(Type.NO_COUNTRY_CODE) + + object NoNationalNumber : DeleteAccountEvent(Type.NO_NATIONAL_NUMBER) + + object NotAMatch : DeleteAccountEvent(Type.NOT_A_MATCH) + + object ConfirmDeletion : DeleteAccountEvent(Type.CONFIRM_DELETION) + + object PinDeletionFailed : DeleteAccountEvent(Type.PIN_DELETION_FAILED) + + object LeaveGroupsFailed : DeleteAccountEvent(Type.LEAVE_GROUPS_FAILED) + + object ServerDeletionFailed : DeleteAccountEvent(Type.SERVER_DELETION_FAILED) + + object LocalDataDeletionFailed : DeleteAccountEvent(Type.LOCAL_DATA_DELETION_FAILED) + + object LeaveGroupsFinished : DeleteAccountEvent(Type.LEAVE_GROUPS_FINISHED) + + /** + * Progress update for leaving groups + * + * @param totalCount The total number of groups we are attempting to leave + * @param leaveCount The number of groups we have left so far + */ + data class LeaveGroupsProgress( + val totalCount: Int, + val leaveCount: Int + ) : DeleteAccountEvent(Type.LEAVE_GROUPS_PROGRESS) + + enum class Type { + NO_COUNTRY_CODE, + NO_NATIONAL_NUMBER, + NOT_A_MATCH, + CONFIRM_DELETION, + LEAVE_GROUPS_FAILED, + PIN_DELETION_FAILED, + SERVER_DELETION_FAILED, + LOCAL_DATA_DELETION_FAILED, + LEAVE_GROUPS_PROGRESS, + LEAVE_GROUPS_FINISHED + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountFragment.java b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountFragment.java index 6edb07e6e..561ac17a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountFragment.java @@ -25,8 +25,9 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProviders; +import androidx.lifecycle.ViewModelProvider; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.google.i18n.phonenumbers.AsYouTypeFormatter; import com.google.i18n.phonenumbers.PhoneNumberUtil; @@ -36,7 +37,6 @@ import org.thoughtcrime.securesms.components.LabeledEditText; import org.thoughtcrime.securesms.util.SpanUtil; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.text.AfterTextChanged; -import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.whispersystems.libsignal.util.guava.Optional; public class DeleteAccountFragment extends Fragment { @@ -47,7 +47,7 @@ public class DeleteAccountFragment extends Fragment { private LabeledEditText number; private AsYouTypeFormatter countryFormatter; private DeleteAccountViewModel viewModel; - private DialogInterface deletionProgressDialog; + private DeleteAccountProgressDialog deletionProgressDialog; @Override public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -63,8 +63,7 @@ public class DeleteAccountFragment extends Fragment { countryCode = view.findViewById(R.id.delete_account_fragment_country_code); number = view.findViewById(R.id.delete_account_fragment_number); - viewModel = ViewModelProviders.of(requireActivity(), new DeleteAccountViewModel.Factory(new DeleteAccountRepository())) - .get(DeleteAccountViewModel.class); + viewModel = new ViewModelProvider(requireActivity(), new DeleteAccountViewModel.Factory(new DeleteAccountRepository())).get(DeleteAccountViewModel.class); viewModel.getCountryDisplayName().observe(getViewLifecycleOwner(), this::setCountryDisplay); viewModel.getRegionCode().observe(getViewLifecycleOwner(), this::handleRegionUpdated); viewModel.getEvents().observe(getViewLifecycleOwner(), this::handleEvent); @@ -220,8 +219,8 @@ public class DeleteAccountFragment extends Fragment { viewModel.setNationalNumber(number); } - private void handleEvent(@NonNull DeleteAccountViewModel.EventType eventType) { - switch (eventType) { + private void handleEvent(@NonNull DeleteAccountEvent deleteAccountEvent) { + switch (deleteAccountEvent.getType()) { case NO_COUNTRY_CODE: Snackbar.make(requireView(), R.string.DeleteAccountFragment__no_country_code, Snackbar.LENGTH_SHORT).setTextColor(Color.WHITE).show(); break; @@ -240,14 +239,11 @@ public class DeleteAccountFragment extends Fragment { .setTitle(R.string.DeleteAccountFragment__are_you_sure) .setMessage(R.string.DeleteAccountFragment__this_will_delete_your_signal_account) .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()) - .setPositiveButton(R.string.DeleteAccountFragment__delete_account, (dialog, which) -> { - dialog.dismiss(); - deletionProgressDialog = SimpleProgressDialog.show(requireContext()); - viewModel.deleteAccount(); - }) + .setPositiveButton(R.string.DeleteAccountFragment__delete_account, this::handleDeleteAccountConfirmation) .setCancelable(true) .show(); break; + case LEAVE_GROUPS_FAILED: case PIN_DELETION_FAILED: case SERVER_DELETION_FAILED: dismissDeletionProgressDialog(); @@ -257,8 +253,16 @@ public class DeleteAccountFragment extends Fragment { dismissDeletionProgressDialog(); showLocalDataDeletionFailedDialog(); break; + case LEAVE_GROUPS_PROGRESS: + ensureDeletionProgressDialog(); + deletionProgressDialog.presentLeavingGroups((DeleteAccountEvent.LeaveGroupsProgress) deleteAccountEvent); + break; + case LEAVE_GROUPS_FINISHED: + ensureDeletionProgressDialog(); + deletionProgressDialog.presentDeletingAccount(); + break; default: - throw new IllegalStateException("Unknown error type: " + eventType); + throw new IllegalStateException("Unknown error type: " + deleteAccountEvent); } } @@ -270,11 +274,12 @@ public class DeleteAccountFragment extends Fragment { } private void showNetworkDeletionFailedDialog() { - new AlertDialog.Builder(requireContext()) - .setMessage(R.string.DeleteAccountFragment__failed_to_delete_account) - .setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()) - .setCancelable(true) - .show(); + new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.DeleteAccountFragment__account_not_deleted) + .setMessage(R.string.DeleteAccountFragment__there_was_a_problem) + .setPositiveButton(android.R.string.ok, this::handleDeleteAccountConfirmation) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()) + .setCancelable(true) + .show(); } private void showLocalDataDeletionFailedDialog() { @@ -288,4 +293,16 @@ public class DeleteAccountFragment extends Fragment { .setCancelable(false) .show(); } + + private void handleDeleteAccountConfirmation(DialogInterface dialog, int which) { + dialog.dismiss(); + ensureDeletionProgressDialog(); + viewModel.deleteAccount(); + } + + private void ensureDeletionProgressDialog() { + if (deletionProgressDialog != null) { + deletionProgressDialog = DeleteAccountProgressDialog.show(requireContext()); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountProgressDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountProgressDialog.kt new file mode 100644 index 000000000..ffb8729b9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountProgressDialog.kt @@ -0,0 +1,49 @@ +package org.thoughtcrime.securesms.delete + +import android.content.Context +import android.widget.ProgressBar +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.thoughtcrime.securesms.R + +/** + * Dialog which shows one of two states: + * + * 1. A "Leaving Groups" state with a determinate progress bar which updates as we leave groups + * 1. A "Deleting Account" state with an indeterminate progress bar + */ +class DeleteAccountProgressDialog private constructor(private val alertDialog: AlertDialog) { + + val title: TextView = alertDialog.findViewById(R.id.delete_account_progress_dialog_title)!! + val message: TextView = alertDialog.findViewById(R.id.delete_account_progress_dialog_message)!! + val progressBar: ProgressBar = alertDialog.findViewById(R.id.delete_account_progress_dialog_spinner)!! + + fun presentLeavingGroups(leaveGroupsProgress: DeleteAccountEvent.LeaveGroupsProgress) { + title.setText(R.string.DeleteAccountFragment__leaving_groups) + message.setText(R.string.DeleteAccountFragment__depending_on_the_number_of_groups) + progressBar.max = leaveGroupsProgress.totalCount + progressBar.progress = leaveGroupsProgress.leaveCount + } + + fun presentDeletingAccount() { + title.setText(R.string.DeleteAccountFragment__deleting_account) + message.setText(R.string.DeleteAccountFragment__deleting_all_user_data_and_resetting) + progressBar.isIndeterminate = true + } + + fun dismiss() { + alertDialog.dismiss() + } + + companion object { + @JvmStatic + fun show(context: Context): DeleteAccountProgressDialog { + return DeleteAccountProgressDialog( + MaterialAlertDialogBuilder(context) + .setView(R.layout.delete_account_progress_dialog) + .show() + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java index 663dbfb1e..3617b6d91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java @@ -1,13 +1,17 @@ package org.thoughtcrime.securesms.delete; import androidx.annotation.NonNull; +import androidx.core.util.Consumer; import com.annimon.stream.Stream; import com.google.i18n.phonenumbers.PhoneNumberUtil; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.groups.GroupManager; import org.thoughtcrime.securesms.pin.KbsEnclaves; import org.thoughtcrime.securesms.util.ServiceUtil; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -36,18 +40,40 @@ class DeleteAccountRepository { return PhoneNumberUtil.getInstance().getCountryCodeForRegion(region); } - void deleteAccount(@NonNull Runnable onFailureToRemovePin, - @NonNull Runnable onFailureToDeleteFromService, - @NonNull Runnable onFailureToDeleteLocalData) - { + void deleteAccount(@NonNull Consumer onDeleteAccountEvent) { SignalExecutors.BOUNDED.execute(() -> { + Log.i(TAG, "deleteAccount: attempting to leave groups..."); + + int groupsLeft = 0; + try (GroupDatabase.Reader groups = SignalDatabase.groups().getGroups()) { + GroupDatabase.GroupRecord groupRecord = groups.getNext(); + onDeleteAccountEvent.accept(new DeleteAccountEvent.LeaveGroupsProgress(groups.getCount(), 0)); + Log.i(TAG, "deleteAccount: found " + groups.getCount() + " groups to leave."); + + while (groupRecord != null) { + if (groupRecord.getId().isPush() && groupRecord.isActive()) { + GroupManager.leaveGroup(ApplicationDependencies.getApplication(), groupRecord.getId().requirePush()); + onDeleteAccountEvent.accept(new DeleteAccountEvent.LeaveGroupsProgress(groups.getCount(), ++groupsLeft)); + } + + groupRecord = groups.getNext(); + } + + onDeleteAccountEvent.accept(DeleteAccountEvent.LeaveGroupsFinished.INSTANCE); + } catch (Exception e) { + Log.w(TAG, "deleteAccount: failed to leave groups", e); + onDeleteAccountEvent.accept(DeleteAccountEvent.LeaveGroupsFailed.INSTANCE); + return; + } + + Log.i(TAG, "deleteAccount: successfully left all groups."); Log.i(TAG, "deleteAccount: attempting to remove pin..."); try { ApplicationDependencies.getKeyBackupService(KbsEnclaves.current()).newPinChangeSession().removePin(); } catch (UnauthenticatedResponseException | IOException e) { Log.w(TAG, "deleteAccount: failed to remove PIN", e); - onFailureToRemovePin.run(); + onDeleteAccountEvent.accept(DeleteAccountEvent.PinDeletionFailed.INSTANCE); return; } @@ -58,7 +84,7 @@ class DeleteAccountRepository { ApplicationDependencies.getSignalServiceAccountManager().deleteAccount(); } catch (IOException e) { Log.w(TAG, "deleteAccount: failed to delete account from signal service", e); - onFailureToDeleteFromService.run(); + onDeleteAccountEvent.accept(DeleteAccountEvent.ServerDeletionFailed.INSTANCE); return; } @@ -67,7 +93,7 @@ class DeleteAccountRepository { if (!ServiceUtil.getActivityManager(ApplicationDependencies.getApplication()).clearApplicationUserData()) { Log.w(TAG, "deleteAccount: failed to delete user data"); - onFailureToDeleteLocalData.run(); + onDeleteAccountEvent.accept(DeleteAccountEvent.LocalDataDeletionFailed.INSTANCE); } }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountViewModel.java index 214e2639e..d4a1291d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountViewModel.java @@ -34,9 +34,9 @@ public class DeleteAccountViewModel extends ViewModel { private final MutableLiveData regionCode; private final LiveData countryDisplayName; private final MutableLiveData nationalNumber; - private final MutableLiveData query; - private final SingleLiveEvent events; - private final LiveData> walletBalance; + private final MutableLiveData query; + private final SingleLiveEvent events; + private final LiveData> walletBalance; public DeleteAccountViewModel(@NonNull DeleteAccountRepository repository) { this.repository = repository; @@ -67,7 +67,7 @@ public class DeleteAccountViewModel extends ViewModel { return Transformations.distinctUntilChanged(regionCode); } - @NonNull SingleLiveEvent getEvents() { + @NonNull SingleLiveEvent getEvents() { return events; } @@ -80,9 +80,7 @@ public class DeleteAccountViewModel extends ViewModel { } void deleteAccount() { - repository.deleteAccount(() -> events.postValue(EventType.PIN_DELETION_FAILED), - () -> events.postValue(EventType.SERVER_DELETION_FAILED), - () -> events.postValue(EventType.LOCAL_DATA_DELETION_FAILED)); + repository.deleteAccount(events::postValue); } void submit() { @@ -91,12 +89,12 @@ public class DeleteAccountViewModel extends ViewModel { Long nationalNumber = this.nationalNumber.getValue(); if (countryCode == null || countryCode == 0) { - events.setValue(EventType.NO_COUNTRY_CODE); + events.setValue(DeleteAccountEvent.NoCountryCode.INSTANCE); return; } if (nationalNumber == null) { - events.setValue(EventType.NO_NATIONAL_NUMBER); + events.setValue(DeleteAccountEvent.NoNationalNumber.INSTANCE); return; } @@ -105,9 +103,9 @@ public class DeleteAccountViewModel extends ViewModel { number.setNationalNumber(nationalNumber); if (PhoneNumberUtil.getInstance().isNumberMatch(number, Recipient.self().requireE164()) == PhoneNumberUtil.MatchType.EXACT_MATCH) { - events.setValue(EventType.CONFIRM_DELETION); + events.setValue(DeleteAccountEvent.ConfirmDeletion.INSTANCE); } else { - events.setValue(EventType.NOT_A_MATCH); + events.setValue(DeleteAccountEvent.NotAMatch.INSTANCE); } } @@ -155,16 +153,6 @@ public class DeleteAccountViewModel extends ViewModel { } } - enum EventType { - NO_COUNTRY_CODE, - NO_NATIONAL_NUMBER, - NOT_A_MATCH, - CONFIRM_DELETION, - PIN_DELETION_FAILED, - SERVER_DELETION_FAILED, - LOCAL_DATA_DELETION_FAILED - } - public static final class Factory implements ViewModelProvider.Factory { private final DeleteAccountRepository repository; diff --git a/app/src/main/res/layout/delete_account_progress_dialog.xml b/app/src/main/res/layout/delete_account_progress_dialog.xml new file mode 100644 index 000000000..bfb707c58 --- /dev/null +++ b/app/src/main/res/layout/delete_account_progress_dialog.xml @@ -0,0 +1,50 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 443af1ee9..c63632a4a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3364,6 +3364,18 @@ Failed to delete account. Do you have a network connection? Failed to delete local data. You can manually clear it in the system application settings. Launch App Settings + + Leaving groups… + + Deleting account… + + Depending on the number of groups you\'re in, this might take a few minutes + + Deleting user data and resetting the app + + Account Not Deleted + + There was a problem completing the deletion process. Check your network connection and try again. Search Countries