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