From ef4fd8e12e1928b1e9cdc03378bb1185d9e05cbe Mon Sep 17 00:00:00 2001 From: Bai Tao Date: Mon, 7 Jun 2010 10:25:53 +0800 Subject: [PATCH] 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 --- Android.mk | 8 +- .../PhoneNumberFormattingTextWatcher.java | 231 +++++++++++++----- .../telephony/PhoneNumberWatcherTest.java | 227 ++++++++++++++--- 3 files changed, 367 insertions(+), 99 deletions(-) diff --git a/Android.mk b/Android.mk index a43e8c81dbf8e..5fb736666cd7c 100644 --- a/Android.mk +++ b/Android.mk @@ -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 diff --git a/telephony/java/android/telephony/PhoneNumberFormattingTextWatcher.java b/telephony/java/android/telephony/PhoneNumberFormattingTextWatcher.java index 8a4733941622d..5241088b8c38d 100644 --- a/telephony/java/android/telephony/PhoneNumberFormattingTextWatcher.java +++ b/telephony/java/android/telephony/PhoneNumberFormattingTextWatcher.java @@ -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. + *

+ * Stop formatting when the user + *

+ *

+ * 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; - static private int sFormatType; - static private Locale sCachedLocale; - private boolean mFormatting; - private boolean mDeletingHyphen; - private int mHyphenStart; - private boolean mDeletingBackward; + /** + * One or more characters were appended. + */ + private final static int STATE_APPEND = 1; + /** + * 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; - - // 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); - } - } else if (mHyphenStart < text.length()) { - text.delete(mHyphenStart, mHyphenStart + 1); - } - } - - PhoneNumberUtils.formatNumber(text, sFormatType); - - mFormatting = false; - } + /** + * The formatting is based on the given countryCode. + * + * @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); } - 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; - } - } else { - mDeletingHyphen = false; - } + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + if (mSelfChange || mStopFormatting) { + return; + } + 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; } } public void onTextChanged(CharSequence s, int start, int before, int count) { - // Does nothing + 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(); + } + } + + 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; + } + } + + /** + * 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; + } + lastNonSeparator = c; + } + if (i == curIndex) { + hasCursor = true; + } + } + if (lastNonSeparator != 0) { + formatted = getFormattedNumber(lastNonSeparator, hasCursor); + } + return formatted; + } + + 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; } } diff --git a/telephony/tests/telephonytests/src/com/android/internal/telephony/PhoneNumberWatcherTest.java b/telephony/tests/telephonytests/src/com/android/internal/telephony/PhoneNumberWatcherTest.java index 88eaecd5e5caa..d2e573ce0564f 100644 --- a/telephony/tests/telephonytests/src/com/android/internal/telephony/PhoneNumberWatcherTest.java +++ b/telephony/tests/telephonytests/src/com/android/internal/telephony/PhoneNumberWatcherTest.java @@ -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"); } }