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

Commit 56613118 authored by Dmitri Plotnikov's avatar Dmitri Plotnikov Committed by Android (Google) Code Review
Browse files

Merge "Reimplement the PhoneNumberFormattingTextWatcher"

parents 0a87ae79 ef4fd8e1
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");
    }
}