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
diff --git a/Android.mk b/Android.mk
index a43e8c8..5fb7366 100644
--- a/Android.mk
+++ b/Android.mk
@@ -569,10 +569,14 @@
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_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 8a47339..5241088b 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.
+ * <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;
- 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 <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);
}
- 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 88eaecd..d2e573c 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");
}
}