Use updated country picker when deleting account.

This commit is contained in:
Michelle Tang
2025-12-23 12:16:28 -05:00
committed by jeffrey-signal
parent 6f213158ed
commit c38fafe9fd
9 changed files with 88 additions and 286 deletions

View File

@@ -1,49 +0,0 @@
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;
private final String region;
Country(@NonNull String displayName, int code, @NonNull String region) {
this.displayName = displayName;
this.code = code;
this.normalized = displayName.toLowerCase();
this.region = region;
}
int getCode() {
return code;
}
@NonNull String getDisplayName() {
return displayName;
}
public String getNormalizedDisplayName() {
return normalized;
}
@NonNull String getRegion() {
return region;
}
@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,50 @@
package org.thoughtcrime.securesms.delete
import android.os.Bundle
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeSelectScreen
import org.thoughtcrime.securesms.registration.ui.countrycode.CountryCodeViewModel
/**
* Country code picker specific to deleting an account.
*/
class DeleteAccountCountryCodeFragment : ComposeFragment() {
companion object {
const val RESULT_KEY = "result_key"
const val RESULT_COUNTRY = "result_country"
}
private val viewModel: CountryCodeViewModel by viewModels()
@Composable
override fun FragmentContent() {
val state by viewModel.state.collectAsStateWithLifecycle()
CountryCodeSelectScreen(
state = state,
title = stringResource(R.string.CountryCodeFragment__your_country),
onSearch = { search -> viewModel.filterCountries(search) },
onDismissed = { findNavController().popBackStack() },
onClick = { country ->
setFragmentResult(RESULT_KEY, bundleOf(RESULT_COUNTRY to country))
findNavController().popBackStack()
}
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.loadCountries()
}
}

View File

@@ -1,72 +0,0 @@
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

@@ -1,66 +0,0 @@
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.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
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 = new ViewModelProvider(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.onRegionSelected(country.getRegion());
dismissAllowingStateLoss();
}
}

View File

@@ -1,25 +1,18 @@
package org.thoughtcrime.securesms.delete;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Color;
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;
@@ -28,6 +21,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
@@ -36,8 +30,10 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.LabeledEditText;
import org.thoughtcrime.securesms.registration.ui.countrycode.Country;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import java.util.Optional;
@@ -45,7 +41,7 @@ import java.util.Optional;
public class DeleteAccountFragment extends Fragment {
private ArrayAdapter<String> countrySpinnerAdapter;
private TextView countryPicker;
private TextView bullets;
private LabeledEditText countryCode;
private LabeledEditText number;
@@ -60,13 +56,13 @@ public class DeleteAccountFragment extends Fragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
Spinner countrySpinner = view.findViewById(R.id.delete_account_fragment_country_spinner);
View confirm = view.findViewById(R.id.delete_account_fragment_delete);
Toolbar toolbar = view.findViewById(R.id.toolbar);
bullets = view.findViewById(R.id.delete_account_fragment_bullets);
countryCode = view.findViewById(R.id.delete_account_fragment_country_code);
number = view.findViewById(R.id.delete_account_fragment_number);
bullets = view.findViewById(R.id.delete_account_fragment_bullets);
countryCode = view.findViewById(R.id.delete_account_fragment_country_code);
number = view.findViewById(R.id.delete_account_fragment_number);
countryPicker = view.findViewById(R.id.delete_account_fragment_country_picker);
viewModel = new ViewModelProvider(requireActivity(), new DeleteAccountViewModel.Factory(new DeleteAccountRepository())).get(DeleteAccountViewModel.class);
viewModel.getCountryDisplayName().observe(getViewLifecycleOwner(), this::setCountryDisplay);
@@ -80,8 +76,14 @@ public class DeleteAccountFragment extends Fragment {
countryCode.getInput().setImeOptions(EditorInfo.IME_ACTION_NEXT);
confirm.setOnClickListener(unused -> viewModel.submit());
toolbar.setNavigationOnClickListener(v -> Navigation.findNavController(v).popBackStack());
countryPicker.setOnClickListener(v -> SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_deleteAccountFragment_to_deleteAccountCountryFragment));
initializeSpinner(countrySpinner);
getParentFragmentManager().setFragmentResultListener(DeleteAccountCountryCodeFragment.RESULT_KEY, this, (key, bundle) -> {
Country country = bundle.getParcelable(DeleteAccountCountryCodeFragment.RESULT_COUNTRY);
if (country != null) {
viewModel.onRegionSelected(country.getRegionCode());
}
});
}
private void updateBullets(@NonNull Optional<String> formattedBalance) {
@@ -101,38 +103,11 @@ public class DeleteAccountFragment extends Fragment {
return builder;
}
@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() {
countryCode.clearFocus();
DeleteAccountCountryPickerFragment.show(requireFragmentManager());
}
private void setCountryDisplay(@NonNull String regionDisplayName) {
countrySpinnerAdapter.clear();
if (TextUtils.isEmpty(regionDisplayName)) {
countrySpinnerAdapter.add(requireContext().getString(R.string.RegistrationActivity_select_your_country));
countryPicker.setText(requireContext().getString(R.string.RegistrationActivity_select_your_country));
} else {
countrySpinnerAdapter.add(regionDisplayName);
countryPicker.setText(regionDisplayName);
}
}

View File

@@ -24,20 +24,10 @@ import org.whispersystems.signalservice.internal.EmptyResponse;
import org.whispersystems.signalservice.internal.ServiceResponse;
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();
}
@NonNull String getRegionDisplayName(@NonNull String region) {
return E164Util.getRegionDisplayName(region).orElse("");
}
@@ -126,25 +116,4 @@ class DeleteAccountRepository {
}
});
}
private static @NonNull Country getCountryForRegion(@NonNull String region) {
return new Country(E164Util.getRegionDisplayName(region).orElse(""),
PhoneNumberUtil.getInstance().getCountryCodeForRegion(region),
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

@@ -29,23 +29,17 @@ import java.util.Optional;
public class DeleteAccountViewModel extends ViewModel {
private final DeleteAccountRepository repository;
private final List<Country> allCountries;
private final LiveData<List<Country>> filteredCountries;
private final MutableLiveData<String> regionCode;
private final LiveData<String> countryDisplayName;
private final MutableLiveData<Long> nationalNumber;
private final MutableLiveData<String> query;
private final SingleLiveEvent<DeleteAccountEvent> events;
private final LiveData<Optional<String>> walletBalance;
public DeleteAccountViewModel(@NonNull DeleteAccountRepository repository) {
this.repository = repository;
this.allCountries = repository.getAllCountries();
this.regionCode = new DefaultValueLiveData<>("ZZ"); // PhoneNumberUtil private static final String UNKNOWN_REGION = "ZZ";
this.nationalNumber = new MutableLiveData<>();
this.query = new DefaultValueLiveData<>("");
this.countryDisplayName = Transformations.map(regionCode, repository::getRegionDisplayName);
this.filteredCountries = Transformations.map(query, q -> Stream.of(allCountries).filter(country -> isMatch(q, country)).toList());
this.events = new SingleLiveEvent<>();
this.walletBalance = Transformations.map(SignalStore.payments().liveMobileCoinBalance(),
DeleteAccountViewModel::getFormattedWalletBalance);
@@ -55,10 +49,6 @@ public class DeleteAccountViewModel extends ViewModel {
return walletBalance;
}
@NonNull LiveData<List<Country>> getFilteredCountries() {
return filteredCountries;
}
@NonNull LiveData<String> getCountryDisplayName() {
return Transformations.distinctUntilChanged(countryDisplayName);
}
@@ -75,10 +65,6 @@ public class DeleteAccountViewModel extends ViewModel {
return nationalNumber.getValue();
}
void onQueryChanged(@NonNull String query) {
this.query.setValue(query.toLowerCase());
}
void deleteAccount() {
repository.deleteAccount(events::postValue);
}
@@ -146,14 +132,6 @@ public class DeleteAccountViewModel extends ViewModel {
}
}
private static boolean isMatch(@NonNull String query, @NonNull Country country) {
if (TextUtils.isEmpty(query)) {
return true;
} else {
return country.getNormalizedDisplayName().contains(query.toLowerCase());
}
}
public static final class Factory implements ViewModelProvider.Factory {
private final DeleteAccountRepository repository;

View File

@@ -83,11 +83,14 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/delete_account_fragment_enter_phone_number">
<Spinner
android:id="@+id/delete_account_fragment_country_spinner"
android:layout_width="fill_parent"
<TextView
android:id="@+id/delete_account_fragment_country_picker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:drawableEnd="@drawable/ic_chevron_16"
android:drawableTint="@color/signal_colorOnSurface"
android:textAlignment="viewStart" />
</FrameLayout>

View File

@@ -223,7 +223,21 @@
android:id="@+id/deleteAccountFragment"
android:name="org.thoughtcrime.securesms.delete.DeleteAccountFragment"
android:label="delete_account_fragment"
tools:layout="@layout/delete_account_fragment" />
tools:layout="@layout/delete_account_fragment">
<action
android:id="@+id/action_deleteAccountFragment_to_deleteAccountCountryFragment"
app:destination="@id/deleteAccountCountryFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
</fragment>
<fragment
android:id="@+id/deleteAccountCountryFragment"
android:name="org.thoughtcrime.securesms.delete.DeleteAccountCountryCodeFragment" />
<fragment
android:id="@+id/exportAccountDataFragment"