1 package com.google.phonenumbers.demoapp.main; 2 3 import android.content.Context; 4 import android.util.AttributeSet; 5 import android.view.KeyEvent; 6 import android.widget.ArrayAdapter; 7 import android.widget.AutoCompleteTextView; 8 import android.widget.LinearLayout; 9 import androidx.annotation.NonNull; 10 import androidx.annotation.Nullable; 11 import com.google.android.material.textfield.TextInputLayout; 12 import com.google.i18n.phonenumbers.PhoneNumberUtil; 13 import com.google.phonenumbers.demoapp.R; 14 import java.util.ArrayList; 15 import java.util.Collections; 16 import java.util.HashMap; 17 import java.util.List; 18 import java.util.Locale; 19 import java.util.Map; 20 import java.util.Set; 21 22 /** 23 * A component containing a searchable dropdown input populated with all regions {@link 24 * PhoneNumberUtil} supports. Dropdown items are of format {@code [countryName] ([nameCode]) - 25 * +[callingCode]} (e.g. {@code Switzerland (CH) - +41}). Method provides access to the name code 26 * (e.g. {@code CH}) of the current input. Name code: <a 27 * href="https://www.iso.org/glossary-for-iso-3166.html">ISO 3166-1 alpha-2 country code</a> (e.g. 28 * {@code CH}). Calling code: <a 29 * href="https://www.itu.int/dms_pub/itu-t/opb/sp/T-SP-E.164D-2016-PDF-E.pdf">ITU-T E.164 assigned 30 * country code</a> (e.g. {@code 41}). 31 */ 32 public class CountryDropdown extends LinearLayout { 33 34 /** 35 * Map containing keys of format {@code [countryName] ([nameCode]) - +[callingCode]} (e.g. {@code 36 * Switzerland (CH) - +41}), and name codes (e.g. {@code CH}) as values. 37 */ 38 private static final Map<String, String> countryLabelMapNameCode = new HashMap<>(); 39 /** Ascending sorted list of the keys in {@link CountryDropdown#countryLabelMapNameCode}. */ 40 private static final List<String> countryLabelSorted = new ArrayList<>(); 41 42 private final TextInputLayout input; 43 private final AutoCompleteTextView inputEditText; 44 45 /** The name code of the current input. */ 46 private String nameCode; 47 CountryDropdown(@onNull Context context, @Nullable AttributeSet attrs)48 public CountryDropdown(@NonNull Context context, @Nullable AttributeSet attrs) { 49 super(context, attrs); 50 inflate(getContext(), R.layout.country_dropdown, this); 51 input = findViewById(R.id.country_dropdown_input); 52 inputEditText = findViewById(R.id.country_dropdown_input_edit_text); 53 54 inputEditText.setOnKeyListener( 55 (v, keyCode, event) -> { 56 // If the DEL key is used and the input was a valid dropdown option, clear the input 57 // completely 58 if (keyCode == KeyEvent.KEYCODE_DEL && setNameCodeForInput()) { 59 inputEditText.setText(""); 60 } 61 // Disable the error state when editing the input after the validation revealed an error 62 if (input.isErrorEnabled()) { 63 disableInputError(); 64 } 65 return false; 66 }); 67 68 populateCountryLabelMapNameCode(); 69 setAdapter(); 70 } 71 72 /** 73 * Populates {@link CountryDropdown#countryLabelMapNameCode} with all regions {@link 74 * PhoneNumberUtil} supports if not populated yet. 75 */ populateCountryLabelMapNameCode()76 private void populateCountryLabelMapNameCode() { 77 if (!countryLabelMapNameCode.isEmpty()) { 78 return; 79 } 80 81 Set<String> supportedNameCodes = PhoneNumberUtil.getInstance().getSupportedRegions(); 82 for (String nameCode : supportedNameCodes) { 83 String countryLabel = getCountryLabelForNameCode(nameCode); 84 countryLabelMapNameCode.put(countryLabel, nameCode); 85 } 86 } 87 88 /** 89 * Returns the label of format {@code [countryName] ([nameCode]) - +[callingCode]} (e.g. {@code 90 * Switzerland (CH) - +41}) for the param {@code nameCode}. 91 * 92 * @param nameCode String in format of a name code (e.g. {@code CH}) 93 * @return String label of format {@code [countryName] ([nameCode]) - +[callingCode]} (e.g. {@code 94 * Switzerland (CH) - +41}) 95 */ getCountryLabelForNameCode(String nameCode)96 private String getCountryLabelForNameCode(String nameCode) { 97 Locale locale = new Locale("en", nameCode); 98 String countryName = locale.getDisplayCountry(); 99 int callingCode = 100 PhoneNumberUtil.getInstance().getCountryCodeForRegion(nameCode.toUpperCase(Locale.ROOT)); 101 102 return countryName + " (" + nameCode.toUpperCase(Locale.ROOT) + ") - +" + callingCode; 103 } 104 105 /** 106 * Populates {@link CountryDropdown#countryLabelSorted} with the ascending sorted keys of {@link 107 * CountryDropdown#countryLabelMapNameCode} if not populated yet. Then sets an {@link 108 * ArrayAdapter} with {@link CountryDropdown#countryLabelSorted} for the dropdown to show the 109 * list. 110 */ setAdapter()111 private void setAdapter() { 112 if (countryLabelSorted.isEmpty()) { 113 countryLabelSorted.addAll(countryLabelMapNameCode.keySet()); 114 Collections.sort(countryLabelSorted); 115 } 116 117 ArrayAdapter<String> arrayAdapter = 118 new ArrayAdapter<>(getContext(), R.layout.country_dropdown_item, countryLabelSorted); 119 inputEditText.setAdapter(arrayAdapter); 120 } 121 122 /** 123 * Returns whether the current input is a valid dropdown option. Also updates the input error 124 * accordingly. 125 * 126 * @return boolean whether the current input is a valid dropdown option 127 */ validateInput()128 public boolean validateInput() { 129 if (!setNameCodeForInput()) { 130 enableInputError(); 131 return false; 132 } 133 134 disableInputError(); 135 return true; 136 } 137 138 /** 139 * Sets the {@link CountryDropdown#nameCode} to the name code of the current input if that's a 140 * valid dropdown option. Else set's it to an empty String. 141 * 142 * @return boolean whether the current input is a valid dropdown option 143 */ setNameCodeForInput()144 private boolean setNameCodeForInput() { 145 String nameCodeForInput = countryLabelMapNameCode.get(getInput()); 146 if (nameCodeForInput == null) { 147 nameCode = ""; 148 return false; 149 } 150 151 nameCode = nameCodeForInput; 152 return true; 153 } 154 155 /** Shows the error message on the input component. */ enableInputError()156 private void enableInputError() { 157 input.setErrorEnabled(true); 158 input.setError(getResources().getString(R.string.main_activity_country_dropdown_error)); 159 } 160 161 /** Hides the error message on the input component. */ disableInputError()162 private void disableInputError() { 163 input.setError(null); 164 input.setErrorEnabled(false); 165 } 166 getInput()167 private String getInput() { 168 return inputEditText.getText().toString(); 169 } 170 171 /** 172 * Returns the name code of the current input if it's a valid dropdown option, else returns an 173 * empty String. 174 * 175 * @return String name code of the current input if it's a valid dropdown option, else returns an 176 * empty String 177 */ getNameCodeForInput()178 public String getNameCodeForInput() { 179 setNameCodeForInput(); 180 return nameCode; 181 } 182 183 /** 184 * Sets the label of the country with the name code param {@code nameCode} on the input if it's 185 * valid. Else the input is not changed. 186 * 187 * @param nameCode String in format of a name code (e.g. {@code CH}) 188 */ setInputForNameCode(String nameCode)189 public void setInputForNameCode(String nameCode) { 190 String countryLabel = getCountryLabelForNameCode(nameCode); 191 if (!countryLabelSorted.contains(countryLabel)) { 192 return; 193 } 194 195 inputEditText.setText(countryLabel); 196 validateInput(); 197 } 198 199 @Override setEnabled(boolean enabled)200 public void setEnabled(boolean enabled) { 201 input.setEnabled(enabled); 202 } 203 } 204