Add ability to delete your Signal account from within the app.

This commit is contained in:
Alex Hart
2020-12-07 17:39:16 -04:00
committed by GitHub
parent 00b6416583
commit edb2a17bcb
21 changed files with 1019 additions and 23 deletions

View File

@@ -27,8 +27,7 @@ public abstract class FullScreenDialogFragment extends DialogFragment {
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_FRAME, ThemeUtil.isDarkTheme(requireActivity()) ? R.style.TextSecure_DarkTheme_FullScreenDialog
: R.style.TextSecure_LightTheme_FullScreenDialog);
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen);
}
@Override

View File

@@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.delete;
import androidx.annotation.NonNull;
import java.util.Objects;
final class Country {
private final String displayName;
private final int code;
private final String normalized;
Country(@NonNull String displayName, int code) {
this.displayName = displayName;
this.code = code;
this.normalized = displayName.toLowerCase();
}
int getCode() {
return code;
}
@NonNull String getDisplayName() {
return displayName;
}
public String getNormalizedDisplayName() {
return normalized;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Country country = (Country) o;
return displayName.equals(country.displayName) &&
code == country.code;
}
@Override
public int hashCode() {
return Objects.hash(displayName, code);
}
}

View File

@@ -0,0 +1,72 @@
package org.thoughtcrime.securesms.delete;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import java.util.Objects;
class DeleteAccountCountryPickerAdapter extends ListAdapter<Country, DeleteAccountCountryPickerAdapter.ViewHolder> {
private final Callback callback;
protected DeleteAccountCountryPickerAdapter(@NonNull Callback callback) {
super(new CountryDiffCallback());
this.callback = callback;
}
@Override
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.delete_account_country_adapter_item, parent, false);
return new ViewHolder(view, position -> callback.onItemSelected(getItem(position)));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.textView.setText(getItem(position).getDisplayName());
}
static class ViewHolder extends RecyclerView.ViewHolder {
private final TextView textView;
public ViewHolder(@NonNull View itemView, @NonNull Consumer<Integer> onItemClickedConsumer) {
super(itemView);
textView = itemView.findViewById(android.R.id.text1);
itemView.setOnClickListener(unused -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
onItemClickedConsumer.accept(getAdapterPosition());
}
});
}
}
private static class CountryDiffCallback extends DiffUtil.ItemCallback<Country> {
@Override
public boolean areItemsTheSame(@NonNull Country oldItem, @NonNull Country newItem) {
return Objects.equals(oldItem.getCode(), newItem.getCode());
}
@Override
public boolean areContentsTheSame(@NonNull Country oldItem, @NonNull Country newItem) {
return Objects.equals(oldItem, newItem);
}
}
interface Callback {
void onItemSelected(@NonNull Country country);
}
}

View File

@@ -0,0 +1,67 @@
package org.thoughtcrime.securesms.delete;
import android.os.Bundle;
import android.text.Editable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
public class DeleteAccountCountryPickerFragment extends DialogFragment {
private DeleteAccountViewModel viewModel;
public static void show(@NonNull FragmentManager fragmentManager) {
new DeleteAccountCountryPickerFragment().show(fragmentManager, null);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen);
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.delete_account_country_picker, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
Toolbar toolbar = view.findViewById(R.id.delete_account_country_picker_toolbar);
EditText searchFilter = view.findViewById(R.id.delete_account_country_picker_filter);
RecyclerView recycler = view.findViewById(R.id.delete_account_country_picker_recycler);
DeleteAccountCountryPickerAdapter adapter = new DeleteAccountCountryPickerAdapter(this::onCountryPicked);
recycler.setAdapter(adapter);
toolbar.setNavigationOnClickListener(unused -> dismiss());
viewModel = ViewModelProviders.of(requireActivity()).get(DeleteAccountViewModel.class);
viewModel.getFilteredCountries().observe(getViewLifecycleOwner(), adapter::submitList);
searchFilter.addTextChangedListener(new AfterTextChanged(this::onQueryChanged));
}
private void onQueryChanged(@NonNull Editable e) {
viewModel.onQueryChanged(e.toString());
}
private void onCountryPicked(@NonNull Country country) {
viewModel.onCountrySelected(country.getDisplayName(), country.getCode());
dismissAllowingStateLoss();
}
}

View File

@@ -0,0 +1,288 @@
package org.thoughtcrime.securesms.delete;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.text.Editable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import com.google.android.material.snackbar.Snackbar;
import com.google.i18n.phonenumbers.AsYouTypeFormatter;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
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;
public class DeleteAccountFragment extends Fragment {
private ArrayAdapter<String> countrySpinnerAdapter;
private LabeledEditText countryCode;
private LabeledEditText number;
private AsYouTypeFormatter countryFormatter;
private DeleteAccountViewModel viewModel;
private DialogInterface deletionProgressDialog;
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.delete_account_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
TextView bullets = view.findViewById(R.id.delete_account_fragment_bullets);
Spinner countrySpinner = view.findViewById(R.id.delete_account_fragment_country_spinner);
View confirm = view.findViewById(R.id.delete_account_fragment_delete);
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.getCountryDisplayName().observe(getViewLifecycleOwner(), this::setCountryDisplay);
viewModel.getRegionCode().observe(getViewLifecycleOwner(), this::setCountryFormatter);
viewModel.getCountryCode().observe(getViewLifecycleOwner(), this::setCountryCode);
viewModel.getEvents().observe(getViewLifecycleOwner(), this::handleEvent);
initializeNumberInput();
countryCode.getInput().addTextChangedListener(new AfterTextChanged(this::afterCountryCodeChanged));
countryCode.getInput().setImeOptions(EditorInfo.IME_ACTION_NEXT);
confirm.setOnClickListener(unused -> viewModel.submit());
bullets.setText(buildBulletsText());
initializeSpinner(countrySpinner);
}
@Override
public void onResume() {
super.onResume();
((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.preferences__delete_account);
}
private @NonNull CharSequence buildBulletsText() {
return new SpannableStringBuilder().append(SpanUtil.bullet(getString(R.string.DeleteAccountFragment__delete_your_account_info_and_profile_photo)))
.append("\n")
.append(SpanUtil.bullet(getString(R.string.DeleteAccountFragment__delete_all_your_messages)))
.append("\n")
.append(SpanUtil.bullet(getString(R.string.DeleteAccountFragment__remove_you_from_all_signal_groups)));
}
@SuppressLint("ClickableViewAccessibility")
private void initializeSpinner(@NonNull Spinner countrySpinner) {
countrySpinnerAdapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item);
countrySpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
countrySpinner.setAdapter(countrySpinnerAdapter);
countrySpinner.setOnTouchListener((view, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
pickCountry();
}
return true;
});
countrySpinner.setOnKeyListener((view, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && event.getAction() == KeyEvent.ACTION_UP) {
pickCountry();
return true;
}
return false;
});
}
private void pickCountry() {
DeleteAccountCountryPickerFragment.show(requireFragmentManager());
}
private void setCountryCode(int countryCode) {
this.countryCode.setText(String.valueOf(countryCode));
}
private void setCountryDisplay(@NonNull String regionDisplayName) {
countrySpinnerAdapter.clear();
if (TextUtils.isEmpty(regionDisplayName)) {
countrySpinnerAdapter.add(requireContext().getString(R.string.RegistrationActivity_select_your_country));
} else {
countrySpinnerAdapter.add(regionDisplayName);
}
}
private void setCountryFormatter(@Nullable String regionCode) {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
countryFormatter = regionCode != null ? util.getAsYouTypeFormatter(regionCode) : null;
reformatText(number.getText());
if (!TextUtils.isEmpty(regionCode) && !regionCode.equals("ZZ")) {
number.requestFocus();
int numberLength = number.getText().length();
number.getInput().setSelection(numberLength, numberLength);
}
}
private Long reformatText(Editable s) {
if (countryFormatter == null) {
return null;
}
if (TextUtils.isEmpty(s)) {
return null;
}
countryFormatter.clear();
String formattedNumber = null;
StringBuilder justDigits = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isDigit(c)) {
formattedNumber = countryFormatter.inputDigit(c);
justDigits.append(c);
}
}
if (formattedNumber != null && !s.toString().equals(formattedNumber)) {
s.replace(0, s.length(), formattedNumber);
}
if (justDigits.length() == 0) {
return null;
}
return Long.parseLong(justDigits.toString());
}
private void initializeNumberInput() {
EditText numberInput = number.getInput();
Long nationalNumber = viewModel.getNationalNumber();
if (nationalNumber != null) {
number.setText(String.valueOf(nationalNumber));
} else {
number.setText("");
}
numberInput.addTextChangedListener(new AfterTextChanged(this::afterNumberChanged));
numberInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
numberInput.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
ViewUtil.hideKeyboard(requireContext(), v);
viewModel.submit();
return true;
}
return false;
});
}
private void afterCountryCodeChanged(@Nullable Editable s) {
if (TextUtils.isEmpty(s) || !TextUtils.isDigitsOnly(s)) {
viewModel.onCountrySelected(null, 0);
return;
}
viewModel.onCountrySelected(null, Integer.parseInt(s.toString()));
}
private void afterNumberChanged(@Nullable Editable s) {
Long number = reformatText(s);
if (number == null) return;
viewModel.setNationalNumber(number);
}
private void handleEvent(@NonNull DeleteAccountViewModel.EventType eventType) {
switch (eventType) {
case NO_COUNTRY_CODE:
Snackbar.make(requireView(), R.string.DeleteAccountFragment__no_country_code, Snackbar.LENGTH_SHORT).show();
break;
case NO_NATIONAL_NUMBER:
Snackbar.make(requireView(), R.string.DeleteAccountFragment__no_number, Snackbar.LENGTH_SHORT).show();
break;
case NOT_A_MATCH:
new AlertDialog.Builder(requireContext())
.setMessage(R.string.DeleteAccountFragment__the_phone_number)
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.setCancelable(true)
.show();
break;
case CONFIRM_DELETION:
new AlertDialog.Builder(requireContext())
.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();
})
.setCancelable(true)
.show();
break;
case PIN_DELETION_FAILED:
case SERVER_DELETION_FAILED:
dismissDeletionProgressDialog();
showNetworkDeletionFailedDialog();
break;
case LOCAL_DATA_DELETION_FAILED:
dismissDeletionProgressDialog();
showLocalDataDeletionFailedDialog();
break;
default:
throw new IllegalStateException("Unknown error type: " + eventType);
}
}
private void dismissDeletionProgressDialog() {
if (deletionProgressDialog != null) {
deletionProgressDialog.dismiss();
deletionProgressDialog = null;
}
}
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();
}
private void showLocalDataDeletionFailedDialog() {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.DeleteAccountFragment__failed_to_delete_local_data)
.setPositiveButton(R.string.DeleteAccountFragment__launch_app_settings, (dialog, which) -> {
Intent settingsIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
settingsIntent.setData(Uri.fromParts("package", requireActivity().getPackageName(), null));
startActivity(settingsIntent);
})
.setCancelable(false)
.show();
}
}

View File

@@ -0,0 +1,86 @@
package org.thoughtcrime.securesms.delete;
import androidx.annotation.NonNull;
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.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.pin.KbsEnclaves;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException;
import java.text.Collator;
import java.util.Comparator;
import java.util.List;
class DeleteAccountRepository {
private static final String TAG = Log.tag(DeleteAccountRepository.class);
@NonNull List<Country> getAllCountries() {
return Stream.of(PhoneNumberUtil.getInstance().getSupportedRegions())
.map(DeleteAccountRepository::getCountryForRegion)
.sorted(new RegionComparator())
.toList();
}
void deleteAccount(@NonNull Runnable onFailureToRemovePin,
@NonNull Runnable onFailureToDeleteFromService,
@NonNull Runnable onFailureToDeleteLocalData)
{
SignalExecutors.BOUNDED.execute(() -> {
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();
return;
}
Log.i(TAG, "deleteAccount: successfully removed pin.");
Log.i(TAG, "deleteAccount: attempting to delete account from server...");
try {
ApplicationDependencies.getSignalServiceAccountManager().deleteAccount();
} catch (IOException e) {
Log.w(TAG, "deleteAccount: failed to delete account from signal service", e);
onFailureToDeleteFromService.run();
return;
}
Log.i(TAG, "deleteAccount: successfully removed account from server");
Log.i(TAG, "deleteAccount: attempting to delete user data and close process...");
if (!ServiceUtil.getActivityManager(ApplicationDependencies.getApplication()).clearApplicationUserData()) {
Log.w(TAG, "deleteAccount: failed to delete user data");
onFailureToDeleteLocalData.run();
}
});
}
private static @NonNull Country getCountryForRegion(@NonNull String region) {
return new Country(PhoneNumberFormatter.getRegionDisplayName(region),
PhoneNumberUtil.getInstance().getCountryCodeForRegion(region));
}
private static class RegionComparator implements Comparator<Country> {
private final Collator collator;
RegionComparator() {
collator = Collator.getInstance();
collator.setStrength(Collator.PRIMARY);
}
@Override
public int compare(Country lhs, Country rhs) {
return collator.compare(lhs.getDisplayName(), rhs.getDisplayName());
}
}
}

View File

@@ -0,0 +1,175 @@
package org.thoughtcrime.securesms.delete;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState;
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.List;
public class DeleteAccountViewModel extends ViewModel {
private final DeleteAccountRepository repository;
private final List<Country> allCountries;
private final LiveData<List<Country>> filteredCountries;
private final LiveData<String> regionCode;
private final MutableLiveData<Integer> countryCode;
private final MutableLiveData<String> countryDisplayName;
private final MutableLiveData<Long> nationalNumber;
private final MutableLiveData<String> query;
private final SingleLiveEvent<EventType> events;
private final LiveData<NumberViewState> numberViewState;
public DeleteAccountViewModel(@NonNull DeleteAccountRepository repository) {
this.repository = repository;
this.allCountries = repository.getAllCountries();
this.countryCode = new DefaultValueLiveData<>(NumberViewState.INITIAL.getCountryCode());
this.nationalNumber = new DefaultValueLiveData<>(NumberViewState.INITIAL.getNationalNumber());
this.countryDisplayName = new DefaultValueLiveData<>(NumberViewState.INITIAL.getCountryDisplayName());
this.query = new DefaultValueLiveData<>("");
this.regionCode = Transformations.map(countryCode, this::mapCountryCodeToRegionCode);
this.filteredCountries = Transformations.map(query, q -> Stream.of(allCountries).filter(country -> isMatch(q, country)).toList());
LiveData<NumberViewState> partialViewState = LiveDataUtil.combineLatest(countryCode,
countryDisplayName,
DeleteAccountViewModel::getPartialNumberViewState);
this.numberViewState = LiveDataUtil.combineLatest(partialViewState, nationalNumber, DeleteAccountViewModel::getCompleteNumberViewState);
this.events = new SingleLiveEvent<>();
}
@NonNull LiveData<List<Country>> getFilteredCountries() {
return filteredCountries;
}
@NonNull LiveData<String> getCountryDisplayName() {
return Transformations.distinctUntilChanged(Transformations.map(numberViewState, NumberViewState::getCountryDisplayName));
}
@NonNull LiveData<String> getRegionCode() {
return Transformations.distinctUntilChanged(regionCode);
}
@NonNull LiveData<Integer> getCountryCode() {
return Transformations.distinctUntilChanged(Transformations.map(numberViewState, NumberViewState::getCountryCode));
}
@NonNull SingleLiveEvent<EventType> getEvents() {
return events;
}
@Nullable Long getNationalNumber() {
Long number = nationalNumber.getValue();
if (number == null || number == NumberViewState.INITIAL.getNationalNumber()) {
return null;
} else {
return number;
}
}
void onQueryChanged(@NonNull String query) {
this.query.setValue(query.toLowerCase());
}
void deleteAccount() {
repository.deleteAccount(() -> events.postValue(EventType.PIN_DELETION_FAILED),
() -> events.postValue(EventType.SERVER_DELETION_FAILED),
() -> events.postValue(EventType.LOCAL_DATA_DELETION_FAILED));
}
void submit() {
Integer countryCode = this.countryCode.getValue();
Long nationalNumber = this.nationalNumber.getValue();
if (countryCode == null || countryCode == 0) {
events.setValue(EventType.NO_COUNTRY_CODE);
return;
}
if (nationalNumber == null) {
events.setValue(EventType.NO_NATIONAL_NUMBER);
return;
}
Phonenumber.PhoneNumber number = new Phonenumber.PhoneNumber();
number.setCountryCode(countryCode);
number.setNationalNumber(nationalNumber);
if (PhoneNumberUtil.getInstance().isNumberMatch(number, Recipient.self().requireE164()) == PhoneNumberUtil.MatchType.EXACT_MATCH) {
events.setValue(EventType.CONFIRM_DELETION);
} else {
events.setValue(EventType.NOT_A_MATCH);
}
}
void onCountrySelected(@Nullable String countryDisplayName, int countryCode) {
if (countryDisplayName != null) {
this.countryDisplayName.setValue(countryDisplayName);
}
this.countryCode.setValue(countryCode);
}
void setNationalNumber(long nationalNumber) {
this.nationalNumber.setValue(nationalNumber);
}
private @NonNull String mapCountryCodeToRegionCode(int countryCode) {
return PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode);
}
private static @NonNull NumberViewState getPartialNumberViewState(int countryCode, @Nullable String countryDisplayName) {
return new NumberViewState.Builder().countryCode(countryCode).selectedCountryDisplayName(countryDisplayName).build();
}
private static @NonNull NumberViewState getCompleteNumberViewState(@NonNull NumberViewState partial, long nationalNumber) {
return partial.toBuilder().nationalNumber(nationalNumber).build();
}
private static boolean isMatch(@NonNull String query, @NonNull Country country) {
if (TextUtils.isEmpty(query)) {
return true;
} else {
return country.getNormalizedDisplayName().contains(query.toLowerCase());
}
}
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;
public Factory(DeleteAccountRepository repository) {
this.repository = repository;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return modelClass.cast(new DeleteAccountViewModel(repository));
}
}
}

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.preferences;
import android.app.ActionBar;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
@@ -8,11 +9,14 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.preference.CheckBoxPreference;
import androidx.preference.Preference;
@@ -24,11 +28,13 @@ import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactIdentityManager;
import org.thoughtcrime.securesms.delete.DeleteAccountFragment;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
@@ -45,6 +51,7 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
private static final String SUBMIT_DEBUG_LOG_PREF = "pref_submit_debug_logs";
private static final String INTERNAL_PREF = "pref_internal";
private static final String ADVANCED_PIN_PREF = "pref_advanced_pin_settings";
private static final String DELETE_ACCOUNT = "pref_delete_account";
private static final int PICK_IDENTITY_CONTACT = 1;
@@ -60,12 +67,7 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
Preference pinSettings = this.findPreference(ADVANCED_PIN_PREF);
pinSettings.setOnPreferenceClickListener(preference -> {
requireActivity().getSupportFragmentManager()
.beginTransaction()
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
.replace(android.R.id.content, new AdvancedPinPreferenceFragment())
.addToBackStack(null)
.commit();
getApplicationPreferencesActivity().pushFragment(new AdvancedPreferenceFragment());
return false;
});
@@ -73,17 +75,32 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
internalPreference.setVisible(FeatureFlags.internalUser());
internalPreference.setOnPreferenceClickListener(preference -> {
if (FeatureFlags.internalUser()) {
requireActivity().getSupportFragmentManager()
.beginTransaction()
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
.replace(android.R.id.content, new InternalOptionsPreferenceFragment())
.addToBackStack(null)
.commit();
getApplicationPreferencesActivity().pushFragment(new InternalOptionsPreferenceFragment());
return true;
} else {
return false;
}
});
Preference deleteAccount = this.findPreference(DELETE_ACCOUNT);
deleteAccount.setOnPreferenceClickListener(preference -> {
getApplicationPreferencesActivity().pushFragment(new DeleteAccountFragment());
return false;
});
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.signal_background_tertiary));
View list = view.findViewById(R.id.recycler_view);
ViewGroup.LayoutParams params = list.getLayoutParams();
params.height = ActionBar.LayoutParams.WRAP_CONTENT;
list.setLayoutParams(params);
list.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.signal_background_primary));
}
@Override
@@ -94,7 +111,7 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
@Override
public void onResume() {
super.onResume();
((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.preferences__advanced);
getApplicationPreferencesActivity().getSupportActionBar().setTitle(R.string.preferences__advanced);
initializePushMessagingToggle();
}
@@ -109,6 +126,10 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
}
}
private @NonNull ApplicationPreferencesActivity getApplicationPreferencesActivity() {
return (ApplicationPreferencesActivity) requireActivity();
}
private void initializePushMessagingToggle() {
CheckBoxPreference preference = (CheckBoxPreference)this.findPreference(PUSH_MESSAGING_PREF);

View File

@@ -6,6 +6,7 @@ import android.os.Build;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.BulletSpan;
import android.text.style.DynamicDrawableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
@@ -50,6 +51,12 @@ public class SpanUtil {
return spannable;
}
public static @NonNull CharSequence bullet(@NonNull CharSequence sequence) {
SpannableString spannable = new SpannableString(sequence);
spannable.setSpan(new BulletSpan(), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannable;
}
public static CharSequence buildImageSpan(@NonNull Drawable drawable) {
SpannableString imageSpan = new SpannableString(" ");