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