1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.telephony;
18 
19 import android.annotation.WorkerThread;
20 import android.compat.annotation.UnsupportedAppUsage;
21 import android.os.Build;
22 import android.text.Editable;
23 import android.text.Selection;
24 import android.text.TextWatcher;
25 import android.text.style.TtsSpan;
26 
27 import com.android.i18n.phonenumbers.AsYouTypeFormatter;
28 import com.android.i18n.phonenumbers.PhoneNumberUtil;
29 
30 import java.util.Locale;
31 
32 /**
33  * Watches a {@link android.widget.TextView} and if a phone number is entered
34  * will format it.
35  * <p>
36  * Stop formatting when the user
37  * <ul>
38  * <li>Inputs non-dialable characters</li>
39  * <li>Removes the separator in the middle of string.</li>
40  * </ul>
41  * <p>
42  * The formatting will be restarted once the text is cleared.
43  *
44  * @deprecated This is a thin wrapper on a `libphonenumber` `AsYouTypeFormatter`; it is recommended
45  * to use that instead.
46  */
47 public class PhoneNumberFormattingTextWatcher implements TextWatcher {
48 
49     /**
50      * Indicates the change was caused by ourselves.
51      */
52     private boolean mSelfChange = false;
53 
54     /**
55      * Indicates the formatting has been stopped.
56      */
57     private boolean mStopFormatting;
58 
59     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
60     private AsYouTypeFormatter mFormatter;
61 
62     /**
63      * The formatting is based on the current system locale and future locale changes
64      * may not take effect on this instance.
65      */
PhoneNumberFormattingTextWatcher()66     public PhoneNumberFormattingTextWatcher() {
67         this(Locale.getDefault().getCountry());
68     }
69 
70     /**
71      * The formatting is based on the given <code>countryCode</code>.
72      *
73      * @param countryCode the ISO 3166-1 two-letter country code that indicates the country/region
74      * where the phone number is being entered.
75      */
76     @WorkerThread
PhoneNumberFormattingTextWatcher(String countryCode)77     public PhoneNumberFormattingTextWatcher(String countryCode) {
78         if (countryCode == null) throw new IllegalArgumentException();
79         mFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(countryCode);
80     }
81 
82     @Override
beforeTextChanged(CharSequence s, int start, int count, int after)83     public void beforeTextChanged(CharSequence s, int start, int count,
84             int after) {
85         if (mSelfChange || mStopFormatting) {
86             return;
87         }
88         // If the user manually deleted any non-dialable characters, stop formatting
89         if (count > 0 && hasSeparator(s, start, count)) {
90             stopFormatting();
91         }
92     }
93 
94     @Override
onTextChanged(CharSequence s, int start, int before, int count)95     public void onTextChanged(CharSequence s, int start, int before, int count) {
96         if (mSelfChange || mStopFormatting) {
97             return;
98         }
99         // If the user inserted any non-dialable characters, stop formatting
100         if (count > 0 && hasSeparator(s, start, count)) {
101             stopFormatting();
102         }
103     }
104 
105     @Override
afterTextChanged(Editable s)106     public synchronized void afterTextChanged(Editable s) {
107         if (mStopFormatting) {
108             // Restart the formatting when all texts were clear.
109             mStopFormatting = !(s.length() == 0);
110             return;
111         }
112         if (mSelfChange) {
113             // Ignore the change caused by s.replace().
114             return;
115         }
116         String formatted = reformat(s, Selection.getSelectionEnd(s));
117         if (formatted != null) {
118             int rememberedPos = mFormatter.getRememberedPosition();
119             mSelfChange = true;
120             s.replace(0, s.length(), formatted, 0, formatted.length());
121             // The text could be changed by other TextWatcher after we changed it. If we found the
122             // text is not the one we were expecting, just give up calling setSelection().
123             if (formatted.equals(s.toString())) {
124                 Selection.setSelection(s, rememberedPos);
125             }
126             mSelfChange = false;
127         }
128 
129         //remove previous TTS spans
130         TtsSpan[] ttsSpans = s.getSpans(0, s.length(), TtsSpan.class);
131         for (TtsSpan ttsSpan : ttsSpans) {
132             s.removeSpan(ttsSpan);
133         }
134 
135         PhoneNumberUtils.ttsSpanAsPhoneNumber(s, 0, s.length());
136     }
137 
138     /**
139      * Generate the formatted number by ignoring all non-dialable chars and stick the cursor to the
140      * nearest dialable char to the left. For instance, if the number is  (650) 123-45678 and '4' is
141      * removed then the cursor should be behind '3' instead of '-'.
142      */
reformat(CharSequence s, int cursor)143     private String reformat(CharSequence s, int cursor) {
144         // The index of char to the leftward of the cursor.
145         int curIndex = cursor - 1;
146         String formatted = null;
147         mFormatter.clear();
148         char lastNonSeparator = 0;
149         boolean hasCursor = false;
150         int len = s.length();
151         for (int i = 0; i < len; i++) {
152             char c = s.charAt(i);
153             if (PhoneNumberUtils.isNonSeparator(c)) {
154                 if (lastNonSeparator != 0) {
155                     formatted = getFormattedNumber(lastNonSeparator, hasCursor);
156                     hasCursor = false;
157                 }
158                 lastNonSeparator = c;
159             }
160             if (i == curIndex) {
161                 hasCursor = true;
162             }
163         }
164         if (lastNonSeparator != 0) {
165             formatted = getFormattedNumber(lastNonSeparator, hasCursor);
166         }
167         return formatted;
168     }
169 
getFormattedNumber(char lastNonSeparator, boolean hasCursor)170     private String getFormattedNumber(char lastNonSeparator, boolean hasCursor) {
171         return hasCursor ? mFormatter.inputDigitAndRememberPosition(lastNonSeparator)
172                 : mFormatter.inputDigit(lastNonSeparator);
173     }
174 
stopFormatting()175     private void stopFormatting() {
176         mStopFormatting = true;
177         mFormatter.clear();
178     }
179 
hasSeparator(final CharSequence s, final int start, final int count)180     private boolean hasSeparator(final CharSequence s, final int start, final int count) {
181         for (int i = start; i < start + count; i++) {
182             char c = s.charAt(i);
183             if (!PhoneNumberUtils.isNonSeparator(c)) {
184                 return true;
185             }
186         }
187         return false;
188     }
189 }
190