Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ef4fd8e1 authored by Bai Tao's avatar Bai Tao
Browse files

Reimplement the PhoneNumberFormattingTextWatcher

a. Built the external/libphonenumberutil into the ext.jar. The file size increased 50K, the phone number meta file is 90K before the compression.
b. Used the external/libphonenumberutil to format the phone number for about 200 countries.
c. Beside the phone number formatting, the external/libphonenumberutil will also be used for phonenumber match and international dialing.

Change-Id: Ie5165dc60d66e1eddab7134725a8d1d1c826434a
parent 81d5dad1
Loading
Loading
Loading
Loading
+6 −2
Original line number Diff line number Diff line
@@ -569,10 +569,14 @@ include $(BUILD_DROIDDOC)

ext_dirs := \
	../../external/apache-http/src \
	../../external/tagsoup/src
	../../external/tagsoup/src \
	../../external/libphonenumber/java/src

ext_src_files := $(call all-java-files-under,$(ext_dirs))

ext_res_dirs := \
	../../external/libphonenumber/java/src

# ====  the library  =========================================
include $(CLEAR_VARS)

@@ -580,7 +584,7 @@ LOCAL_SRC_FILES := $(ext_src_files)

LOCAL_NO_STANDARD_LIBRARIES := true
LOCAL_JAVA_LIBRARIES := core

LOCAL_JAVA_RESOURCE_DIRS := $(ext_res_dirs)
LOCAL_MODULE := ext

LOCAL_NO_EMMA_INSTRUMENT := true
+168 −53
Original line number Diff line number Diff line
@@ -16,83 +16,198 @@

package android.telephony;

import com.google.i18n.phonenumbers.AsYouTypeFormatter;
import com.google.i18n.phonenumbers.PhoneNumberUtil;

import android.telephony.PhoneNumberUtils;
import android.text.Editable;
import android.text.Selection;
import android.text.TextWatcher;
import android.widget.TextView;

import java.util.Locale;

/**
 * Watches a {@link TextView} and if a phone number is entered will format it using
 * {@link PhoneNumberUtils#formatNumber(Editable, int)}. The formatting is based on
 * the current system locale when this object is created and future locale changes
 * may not take effect on this instance.
 * Watches a {@link TextView} and if a phone number is entered will format it.
 * <p>
 * Stop formatting when the user
 * <ul>
 * <li>Inputs non-dialable characters</li>
 * <li>Removes the separator in the middle of string.</li>
 * </ul>
 * <p>
 * The formatting will be restarted once the text is cleared.
 */
public class PhoneNumberFormattingTextWatcher implements TextWatcher {
    /**
     * One or more characters were removed from the end.
     */
    private final static int STATE_REMOVE_LAST = 0;

    /**
     * One or more characters were appended.
     */
    private final static int STATE_APPEND = 1;

    static private int sFormatType;
    static private Locale sCachedLocale;
    private boolean mFormatting;
    private boolean mDeletingHyphen;
    private int mHyphenStart;
    private boolean mDeletingBackward;
    /**
     * One or more digits were changed in the beginning or the middle of text.
     */
    private final static int STATE_MODIFY_DIGITS = 2;

    /**
     * The changes other than the above.
     */
    private final static int STATE_OTHER = 3;

    /**
     * The state of this change could be one value of the above
     */
    private int mState;

    /**
     * Indicates the change was caused by ourselves.
     */
    private boolean mSelfChange = false;

    /**
     * Indicates the formatting has been stopped.
     */
    private boolean mStopFormatting;

    private AsYouTypeFormatter mFormatter;

    /**
     * The formatting is based on the current system locale and future locale changes
     * may not take effect on this instance.
     */
    public PhoneNumberFormattingTextWatcher() {
        if (sCachedLocale == null || sCachedLocale != Locale.getDefault()) {
            sCachedLocale = Locale.getDefault();
            sFormatType = PhoneNumberUtils.getFormatTypeForLocale(sCachedLocale);
        }
        this (Locale.getDefault() != null ? Locale.getDefault().getCountry() : "US");
    }

    public synchronized void afterTextChanged(Editable text) {
        // Make sure to ignore calls to afterTextChanged caused by the work done below
        if (!mFormatting) {
            mFormatting = true;
    /**
     * The formatting is based on the given <code>countryCode</code>.
     *
     * @param countryCode the ISO 3166-1 two-letter country code that indicates the country/region
     * where the phone number is being entered.
     *
     * @hide
     */
    public PhoneNumberFormattingTextWatcher(String countryCode) {
        mFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(countryCode);
    }

            // If deleting the hyphen, also delete the char before or after that
            if (mDeletingHyphen && mHyphenStart > 0) {
                if (mDeletingBackward) {
                    if (mHyphenStart - 1 < text.length()) {
                        text.delete(mHyphenStart - 1, mHyphenStart);
    public void beforeTextChanged(CharSequence s, int start, int count,
            int after) {
        if (mSelfChange || mStopFormatting) {
            return;
        }
                } else if (mHyphenStart < text.length()) {
                    text.delete(mHyphenStart, mHyphenStart + 1);
        if (count == 0 && s.length() == start) {
            // Append one or more new chars
            mState = STATE_APPEND;
        } else if (after == 0 && start + count == s.length() && count > 0) {
            // Remove one or more chars from the end of string.
            mState = STATE_REMOVE_LAST;
        } else if (count > 0 && !hasSeparator(s, start, count)) {
            // Remove the dialable chars in the begin or middle of text.
            mState = STATE_MODIFY_DIGITS;
        } else {
            mState = STATE_OTHER;
        }
    }

            PhoneNumberUtils.formatNumber(text, sFormatType);
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        if (mSelfChange || mStopFormatting) {
            return;
        }
        if (mState == STATE_OTHER) {
            if (count > 0 && !hasSeparator(s, start, count)) {
                // User inserted the dialable characters in the middle of text.
                mState = STATE_MODIFY_DIGITS;
            }
        }
        // Check whether we should stop formatting.
        if (mState == STATE_APPEND && count > 0 && hasSeparator(s, start, count)) {
            // User appended the non-dialable character, stop formatting.
            stopFormatting();
        } else if (mState == STATE_OTHER) {
            // User must insert or remove the non-dialable characters in the begin or middle of
            // number, stop formatting. 
            stopFormatting();
        }
    }

            mFormatting = false;
    public synchronized void afterTextChanged(Editable s) {
        if (mStopFormatting) {
            // Restart the formatting when all texts were clear.
            mStopFormatting = !(s.length() == 0);
            return;
        }
        if (mSelfChange) {
            // Ignore the change caused by s.replace().
            return;
        }
        String formatted = reformat(s, Selection.getSelectionEnd(s));
        if (formatted != null) {
            int rememberedPos = mFormatter.getRememberedPosition();
            mSelfChange = true;
            s.replace(0, s.length(), formatted, 0, formatted.length());
            // The text could be changed by other TextWatcher after we changed it. If we found the
            // text is not the one we were expecting, just give up calling setSelection().
            if (formatted.equals(s.toString())) {
                Selection.setSelection(s, rememberedPos);
            }
            mSelfChange = false;
        }
    }

    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        // Check if the user is deleting a hyphen
        if (!mFormatting) {
            // Make sure user is deleting one char, without a selection
            final int selStart = Selection.getSelectionStart(s);
            final int selEnd = Selection.getSelectionEnd(s);
            if (s.length() > 1 // Can delete another character
                    && count == 1 // Deleting only one character
                    && after == 0 // Deleting
                    && s.charAt(start) == '-' // a hyphen
                    && selStart == selEnd) { // no selection
                mDeletingHyphen = true;
                mHyphenStart = start;
                // Check if the user is deleting forward or backward
                if (selStart == start + 1) {
                    mDeletingBackward = true;
                } else {
                    mDeletingBackward = false;
    /**
     * Generate the formatted number by ignoring all non-dialable chars and stick the cursor to the
     * nearest dialable char to the left. For instance, if the number is  (650) 123-45678 and '4' is
     * removed then the cursor should be behind '3' instead of '-'.
     */
    private String reformat(CharSequence s, int cursor) {
        // The index of char to the leftward of the cursor.
        int curIndex = cursor - 1;
        String formatted = null;
        mFormatter.clear();
        char lastNonSeparator = 0;
        boolean hasCursor = false;
        int len = s.length();
        for (int i = 0; i < len; i++) {
            char c = s.charAt(i);
            if (PhoneNumberUtils.isNonSeparator(c)) {
                if (lastNonSeparator != 0) {
                    formatted = getFormattedNumber(lastNonSeparator, hasCursor);
                    hasCursor = false;
                }
            } else {
                mDeletingHyphen = false;
                lastNonSeparator = c;
            }
            if (i == curIndex) {
                hasCursor = true;
            }
        }
        if (lastNonSeparator != 0) {
            formatted = getFormattedNumber(lastNonSeparator, hasCursor);
        }
        return formatted;
    }

    public void onTextChanged(CharSequence s, int start, int before, int count) {
        // Does nothing
    private String getFormattedNumber(char lastNonSeparator, boolean hasCursor) {
        return hasCursor ? mFormatter.inputDigitAndRememberPosition(lastNonSeparator)
                : mFormatter.inputDigit(lastNonSeparator);
    }

    private void stopFormatting() {
        mStopFormatting = true;
        mFormatter.clear();
    }

    private boolean hasSeparator(final CharSequence s, final int start, final int count) {
        for (int i = start; i < start + count; i++) {
            char c = s.charAt(i);
            if (!PhoneNumberUtils.isNonSeparator(c)) {
                return true;
            }
        }
        return false;
    }
}
+188 −39
Original line number Diff line number Diff line
@@ -13,53 +13,202 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.telephony;

import android.telephony.PhoneNumberFormattingTextWatcher;
import android.test.suitebuilder.annotation.SmallTest;
import android.test.AndroidTestCase;
import android.text.Editable;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;

import junit.framework.TestCase;

public class PhoneNumberWatcherTest extends TestCase {
    @SmallTest
    public void testHyphenation() throws Exception {
public class PhoneNumberWatcherTest extends AndroidTestCase {
    public void testAppendChars() {
        final String multiChars = "65012345";
        final String formatted1 = "(650) 123-45";
        TextWatcher textWatcher = getTextWatcher();
        SpannableStringBuilder number = new SpannableStringBuilder();
        TextWatcher tw = new PhoneNumberFormattingTextWatcher();
        number.append("555-1212");
        // Move the cursor to the left edge
        Selection.setSelection(number, 0);
        tw.beforeTextChanged(number, 0, 0, 1);
        // Insert an 8 at the beginning
        number.insert(0, "8");
        tw.afterTextChanged(number);
        assertEquals("855-512-12", number.toString());
        // Append more than one chars
        textWatcher.beforeTextChanged(number, 0, 0, multiChars.length());
        number.append(multiChars);
        Selection.setSelection(number, number.length());
        textWatcher.onTextChanged(number, 0, 0, number.length());
        textWatcher.afterTextChanged(number);
        assertEquals(formatted1, number.toString());
        assertEquals(formatted1.length(), Selection.getSelectionEnd(number));
        // Append one chars
        final char appendChar = '6';
        final String formatted2 = "(650) 123-456";
        int len = number.length();
        textWatcher.beforeTextChanged(number, number.length(), 0, 1);
        number.append(appendChar);
        Selection.setSelection(number, number.length());
        textWatcher.onTextChanged(number, len, 0, 1);
        textWatcher.afterTextChanged(number);
        assertEquals(formatted2, number.toString());
        assertEquals(formatted2.length(), Selection.getSelectionEnd(number));
    }

    @SmallTest
    public void testHyphenDeletion() throws Exception {
        SpannableStringBuilder number = new SpannableStringBuilder();
        TextWatcher tw = new PhoneNumberFormattingTextWatcher();
        number.append("555-1212");
        // Move the cursor to after the hyphen
        Selection.setSelection(number, 4);
        // Delete the hyphen
        tw.beforeTextChanged(number, 3, 1, 0);
        number.delete(3, 4);
        tw.afterTextChanged(number);
        // Make sure that it deleted the character before the hyphen 
        assertEquals("551-212", number.toString());
        
        // Make sure it deals with left edge boundary case
        number.insert(0, "-");
        Selection.setSelection(number, 1);
        tw.beforeTextChanged(number, 0, 1, 0);
        number.delete(0, 1);
        tw.afterTextChanged(number);
        // Make sure that it deleted the character before the hyphen 
        assertEquals("551-212", number.toString());
    public void testRemoveLastChars() {
        final String init = "65012345678";
        final String result1 = "(650) 123-4567";
        TextWatcher textWatcher = getTextWatcher();
        // Remove the last char.
        SpannableStringBuilder number = new SpannableStringBuilder(init);
        int len = number.length();
        textWatcher.beforeTextChanged(number, len - 1, 1, 0);
        number.delete(len - 1, len);
        Selection.setSelection(number, number.length());
        textWatcher.onTextChanged(number, number.length() - 1, 1, 0);
        textWatcher.afterTextChanged(number);
        assertEquals(result1, number.toString());
        assertEquals(result1.length(), Selection.getSelectionEnd(number));
        // Remove last 5 chars
        final String result2 = "(650) 123";
        textWatcher.beforeTextChanged(number, number.length() - 4, 4, 0);
        number.delete(number.length() - 5, number.length());
        Selection.setSelection(number, number.length());
        textWatcher.onTextChanged(number, number.length(), 4, 0);
        textWatcher.afterTextChanged(number);
        assertEquals(result2, number.toString());
        assertEquals(result2.length(), Selection.getSelectionEnd(number));
    }

    public void testInsertChars() {
        final String init = "(650) 23";
        final String expected1 = "(650) 123";
        TextWatcher textWatcher = getTextWatcher();

        // Insert one char
        SpannableStringBuilder number = new SpannableStringBuilder(init);
        textWatcher.beforeTextChanged(number, 4, 0, 1);
        number.insert(4, "1"); // (6501) 23
        Selection.setSelection(number, 5); // make the cursor at right of 1
        textWatcher.onTextChanged(number, 4, 0, 1);
        textWatcher.afterTextChanged(number);
        assertEquals(expected1, number.toString());
        // the cursor should still at the right of '1'
        assertEquals(7, Selection.getSelectionEnd(number));

        // Insert multiple chars
        final String expected2 = "(650) 145-6723";
        textWatcher.beforeTextChanged(number, 7, 0, 4);
        number.insert(7, "4567"); // change to (650) 1456723
        Selection.setSelection(number, 11); // the cursor is at the right of '7'.
        textWatcher.onTextChanged(number, 7, 0, 4);
        textWatcher.afterTextChanged(number);
        assertEquals(expected2, number.toString());
        // the cursor should be still at the right of '7'
        assertEquals(12, Selection.getSelectionEnd(number));
    }

    public void testStopFormatting() {
        final String init = "(650) 123";
        final String expected1 = "(650) 123 4";
        TextWatcher textWatcher = getTextWatcher();

        // Append space
        SpannableStringBuilder number = new SpannableStringBuilder(init);
        textWatcher.beforeTextChanged(number, 9, 0, 2);
        number.insert(9, " 4"); // (6501) 23 4
        Selection.setSelection(number, number.length()); // make the cursor at right of 4
        textWatcher.onTextChanged(number, 9, 0, 2);
        textWatcher.afterTextChanged(number);
        assertEquals(expected1, number.toString());
        // the cursor should still at the right of '1'
        assertEquals(expected1.length(), Selection.getSelectionEnd(number));

        // Delete a ')'
        final String expected2 ="(650 123";
        textWatcher = getTextWatcher();
        number = new SpannableStringBuilder(init);
        textWatcher.beforeTextChanged(number, 4, 1, 0);
        number.delete(4, 5); // (6501 23 4
        Selection.setSelection(number, 5); // make the cursor at right of 1
        textWatcher.onTextChanged(number, 4, 1, 0);
        textWatcher.afterTextChanged(number);
        assertEquals(expected2, number.toString());
        // the cursor should still at the right of '1'
        assertEquals(5, Selection.getSelectionEnd(number));

        // Insert a hyphen
        final String expected3 ="(650) 12-3";
        textWatcher = getTextWatcher();
        number = new SpannableStringBuilder(init);
        textWatcher.beforeTextChanged(number, 8, 0, 1);
        number.insert(8, "-"); // (650) 12-3
        Selection.setSelection(number, 9); // make the cursor at right of -
        textWatcher.onTextChanged(number, 8, 0, 1);
        textWatcher.afterTextChanged(number);
        assertEquals(expected3, number.toString());
        // the cursor should still at the right of '-'
        assertEquals(9, Selection.getSelectionEnd(number));
    }

    public void testRestartFormatting() {
        final String init = "(650) 123";
        final String expected1 = "(650) 123 4";
        TextWatcher textWatcher = getTextWatcher();

        // Append space
        SpannableStringBuilder number = new SpannableStringBuilder(init);
        textWatcher.beforeTextChanged(number, 9, 0, 2);
        number.insert(9, " 4"); // (650) 123 4
        Selection.setSelection(number, number.length()); // make the cursor at right of 4
        textWatcher.onTextChanged(number, 9, 0, 2);
        textWatcher.afterTextChanged(number);
        assertEquals(expected1, number.toString());
        // the cursor should still at the right of '4'
        assertEquals(expected1.length(), Selection.getSelectionEnd(number));

        // Clear the current string, and start formatting again.
        int len = number.length();
        textWatcher.beforeTextChanged(number, 0, len, 0);
        number.delete(0, len);
        textWatcher.onTextChanged(number, 0, len, 0);
        textWatcher.afterTextChanged(number);

        final String expected2 = "(650) 123-4";
        number = new SpannableStringBuilder(init);
        textWatcher.beforeTextChanged(number, 9, 0, 1);
        number.insert(9, "4"); // (650) 1234
        Selection.setSelection(number, number.length()); // make the cursor at right of 4
        textWatcher.onTextChanged(number, 9, 0, 1);
        textWatcher.afterTextChanged(number);
        assertEquals(expected2, number.toString());
        // the cursor should still at the right of '4'
        assertEquals(expected2.length(), Selection.getSelectionEnd(number));
    }

    public void testTextChangedByOtherTextWatcher() {
        final TextWatcher cleanupTextWatcher = new TextWatcher() {
            public void afterTextChanged(Editable s) {
                s.clear();
            }

            public void beforeTextChanged(CharSequence s, int start, int count,
                    int after) {
            }

            public void onTextChanged(CharSequence s, int start, int before,
                    int count) {
            }
        };
        final String init = "(650) 123";
        final String expected1 = "";
        TextWatcher textWatcher = getTextWatcher();

        SpannableStringBuilder number = new SpannableStringBuilder(init);
        textWatcher.beforeTextChanged(number, 5, 0, 1);
        number.insert(5, "4"); // (6504) 123
        Selection.setSelection(number, 5); // make the cursor at right of 4
        textWatcher.onTextChanged(number, 5, 0, 1);
        number.setSpan(cleanupTextWatcher, 0, number.length(), 0);
        textWatcher.afterTextChanged(number);
        assertEquals(expected1, number.toString());
    }

    private TextWatcher getTextWatcher() {
        return new PhoneNumberFormattingTextWatcher("US");
    }
}