PhoneNumberFormattingTextWatcher.java revision ef4fd8e12e1928b1e9cdc03378bb1185d9e05cbe
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
17package android.telephony;
18
19import com.google.i18n.phonenumbers.AsYouTypeFormatter;
20import com.google.i18n.phonenumbers.PhoneNumberUtil;
21
22import android.telephony.PhoneNumberUtils;
23import android.text.Editable;
24import android.text.Selection;
25import android.text.TextWatcher;
26
27import java.util.Locale;
28
29/**
30 * Watches a {@link TextView} and if a phone number is entered will format it.
31 * <p>
32 * Stop formatting when the user
33 * <ul>
34 * <li>Inputs non-dialable characters</li>
35 * <li>Removes the separator in the middle of string.</li>
36 * </ul>
37 * <p>
38 * The formatting will be restarted once the text is cleared.
39 */
40public class PhoneNumberFormattingTextWatcher implements TextWatcher {
41    /**
42     * One or more characters were removed from the end.
43     */
44    private final static int STATE_REMOVE_LAST = 0;
45
46    /**
47     * One or more characters were appended.
48     */
49    private final static int STATE_APPEND = 1;
50
51    /**
52     * One or more digits were changed in the beginning or the middle of text.
53     */
54    private final static int STATE_MODIFY_DIGITS = 2;
55
56    /**
57     * The changes other than the above.
58     */
59    private final static int STATE_OTHER = 3;
60
61    /**
62     * The state of this change could be one value of the above
63     */
64    private int mState;
65
66    /**
67     * Indicates the change was caused by ourselves.
68     */
69    private boolean mSelfChange = false;
70
71    /**
72     * Indicates the formatting has been stopped.
73     */
74    private boolean mStopFormatting;
75
76    private AsYouTypeFormatter mFormatter;
77
78    /**
79     * The formatting is based on the current system locale and future locale changes
80     * may not take effect on this instance.
81     */
82    public PhoneNumberFormattingTextWatcher() {
83        this (Locale.getDefault() != null ? Locale.getDefault().getCountry() : "US");
84    }
85
86    /**
87     * The formatting is based on the given <code>countryCode</code>.
88     *
89     * @param countryCode the ISO 3166-1 two-letter country code that indicates the country/region
90     * where the phone number is being entered.
91     *
92     * @hide
93     */
94    public PhoneNumberFormattingTextWatcher(String countryCode) {
95        mFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(countryCode);
96    }
97
98    public void beforeTextChanged(CharSequence s, int start, int count,
99            int after) {
100        if (mSelfChange || mStopFormatting) {
101            return;
102        }
103        if (count == 0 && s.length() == start) {
104            // Append one or more new chars
105            mState = STATE_APPEND;
106        } else if (after == 0 && start + count == s.length() && count > 0) {
107            // Remove one or more chars from the end of string.
108            mState = STATE_REMOVE_LAST;
109        } else if (count > 0 && !hasSeparator(s, start, count)) {
110            // Remove the dialable chars in the begin or middle of text.
111            mState = STATE_MODIFY_DIGITS;
112        } else {
113            mState = STATE_OTHER;
114        }
115    }
116
117    public void onTextChanged(CharSequence s, int start, int before, int count) {
118        if (mSelfChange || mStopFormatting) {
119            return;
120        }
121        if (mState == STATE_OTHER) {
122            if (count > 0 && !hasSeparator(s, start, count)) {
123                // User inserted the dialable characters in the middle of text.
124                mState = STATE_MODIFY_DIGITS;
125            }
126        }
127        // Check whether we should stop formatting.
128        if (mState == STATE_APPEND && count > 0 && hasSeparator(s, start, count)) {
129            // User appended the non-dialable character, stop formatting.
130            stopFormatting();
131        } else if (mState == STATE_OTHER) {
132            // User must insert or remove the non-dialable characters in the begin or middle of
133            // number, stop formatting.
134            stopFormatting();
135        }
136    }
137
138    public synchronized void afterTextChanged(Editable s) {
139        if (mStopFormatting) {
140            // Restart the formatting when all texts were clear.
141            mStopFormatting = !(s.length() == 0);
142            return;
143        }
144        if (mSelfChange) {
145            // Ignore the change caused by s.replace().
146            return;
147        }
148        String formatted = reformat(s, Selection.getSelectionEnd(s));
149        if (formatted != null) {
150            int rememberedPos = mFormatter.getRememberedPosition();
151            mSelfChange = true;
152            s.replace(0, s.length(), formatted, 0, formatted.length());
153            // The text could be changed by other TextWatcher after we changed it. If we found the
154            // text is not the one we were expecting, just give up calling setSelection().
155            if (formatted.equals(s.toString())) {
156                Selection.setSelection(s, rememberedPos);
157            }
158            mSelfChange = false;
159        }
160    }
161
162    /**
163     * Generate the formatted number by ignoring all non-dialable chars and stick the cursor to the
164     * nearest dialable char to the left. For instance, if the number is  (650) 123-45678 and '4' is
165     * removed then the cursor should be behind '3' instead of '-'.
166     */
167    private String reformat(CharSequence s, int cursor) {
168        // The index of char to the leftward of the cursor.
169        int curIndex = cursor - 1;
170        String formatted = null;
171        mFormatter.clear();
172        char lastNonSeparator = 0;
173        boolean hasCursor = false;
174        int len = s.length();
175        for (int i = 0; i < len; i++) {
176            char c = s.charAt(i);
177            if (PhoneNumberUtils.isNonSeparator(c)) {
178                if (lastNonSeparator != 0) {
179                    formatted = getFormattedNumber(lastNonSeparator, hasCursor);
180                    hasCursor = false;
181                }
182                lastNonSeparator = c;
183            }
184            if (i == curIndex) {
185                hasCursor = true;
186            }
187        }
188        if (lastNonSeparator != 0) {
189            formatted = getFormattedNumber(lastNonSeparator, hasCursor);
190        }
191        return formatted;
192    }
193
194    private String getFormattedNumber(char lastNonSeparator, boolean hasCursor) {
195        return hasCursor ? mFormatter.inputDigitAndRememberPosition(lastNonSeparator)
196                : mFormatter.inputDigit(lastNonSeparator);
197    }
198
199    private void stopFormatting() {
200        mStopFormatting = true;
201        mFormatter.clear();
202    }
203
204    private boolean hasSeparator(final CharSequence s, final int start, final int count) {
205        for (int i = start; i < start + count; i++) {
206            char c = s.charAt(i);
207            if (!PhoneNumberUtils.isNonSeparator(c)) {
208                return true;
209            }
210        }
211        return false;
212    }
213}
214