Ensure user leaves groups before deleting account.

fork-5.53.8
Alex Hart 2021-11-30 13:55:24 -04:00 zatwierdzone przez Greyson Parrelli
rodzic 3242d97c75
commit 321c84583b
8 zmienionych plików z 247 dodań i 46 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -34,9 +34,9 @@ public class DeleteAccountViewModel extends ViewModel {
private final MutableLiveData<String> regionCode;
private final LiveData<String> countryDisplayName;
private final MutableLiveData<Long> nationalNumber;
private final MutableLiveData<String> query;
private final SingleLiveEvent<EventType> events;
private final LiveData<Optional<String>> walletBalance;
private final MutableLiveData<String> query;
private final SingleLiveEvent<DeleteAccountEvent> events;
private final LiveData<Optional<String>> walletBalance;
public DeleteAccountViewModel(@NonNull DeleteAccountRepository repository) {
this.repository = repository;
@ -67,7 +67,7 @@ public class DeleteAccountViewModel extends ViewModel {
return Transformations.distinctUntilChanged(regionCode);
}
@NonNull SingleLiveEvent<EventType> getEvents() {
@NonNull SingleLiveEvent<DeleteAccountEvent> 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;

Wyświetl plik

@ -0,0 +1,50 @@
<?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="wrap_content">
<TextView
android:id="@+id/delete_account_progress_dialog_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dsl_settings_gutter"
android:layout_marginTop="16dp"
android:layout_marginEnd="@dimen/dsl_settings_gutter"
android:text="@string/DeleteAccountFragment__leaving_groups"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/delete_account_progress_dialog_spinner" />
<TextView
android:id="@+id/delete_account_progress_dialog_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dsl_settings_gutter"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/dsl_settings_gutter"
android:layout_marginBottom="48dp"
android:minLines="2"
android:text="@string/DeleteAccountFragment__depending_on_the_number_of_groups"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/signal_text_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/delete_account_progress_dialog_title" />
<ProgressBar
android:id="@+id/delete_account_progress_dialog_spinner"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="@dimen/dsl_settings_gutter"
android:layout_marginTop="58dp"
android:indeterminateBehavior="cycle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -3364,6 +3364,18 @@
<string name="DeleteAccountFragment__failed_to_delete_account">Failed to delete account. Do you have a network connection?</string>
<string name="DeleteAccountFragment__failed_to_delete_local_data">Failed to delete local data. You can manually clear it in the system application settings.</string>
<string name="DeleteAccountFragment__launch_app_settings">Launch App Settings</string>
<!-- Title of progress dialog shown when a user deletes their account and the process is leaving all groups -->
<string name="DeleteAccountFragment__leaving_groups">Leaving groups…</string>
<!-- Title of progress dialog shown when a user deletes their account and the process has left all groups -->
<string name="DeleteAccountFragment__deleting_account">Deleting account…</string>
<!-- Message of progress dialog shown when a user deletes their account and the process is leaving groups -->
<string name="DeleteAccountFragment__depending_on_the_number_of_groups">Depending on the number of groups you\'re in, this might take a few minutes</string>
<!-- Message of progress dialog shown when a user deletes their account and the process has left all groups -->
<string name="DeleteAccountFragment__deleting_all_user_data_and_resetting">Deleting user data and resetting the app</string>
<!-- Title of error dialog shown when a network error occurs during account deletion -->
<string name="DeleteAccountFragment__account_not_deleted">Account Not Deleted</string>
<!-- Message of error dialog shown when a network error occurs during account deletion -->
<string name="DeleteAccountFragment__there_was_a_problem">There was a problem completing the deletion process. Check your network connection and try again.</string>
<!-- DeleteAccountCountryPickerFragment -->
<string name="DeleteAccountCountryPickerFragment__search_countries">Search Countries</string>