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