1 /*
2  * Copyright (C) 2015 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 
17 package com.android.contacts.common.compat;
18 
19 import android.telephony.PhoneNumberUtils;
20 import android.text.Spannable;
21 import android.text.TextUtils;
22 import android.text.style.TtsSpan;
23 import com.android.dialer.compat.CompatUtils;
24 import com.google.i18n.phonenumbers.NumberParseException;
25 import com.google.i18n.phonenumbers.PhoneNumberUtil;
26 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
27 
28 /**
29  * This class contains static utility methods extracted from PhoneNumberUtils, and the methods were
30  * added in API level 23. In this way, we could enable the corresponding functionality for pre-M
31  * devices. We need maintain this class and keep it synced with PhoneNumberUtils. Another thing to
32  * keep in mind is that we use com.google.i18n rather than com.android.i18n in here, so we need make
33  * sure the application behavior is preserved.
34  */
35 public class PhoneNumberUtilsCompat {
36 
37   /** Not instantiable. */
PhoneNumberUtilsCompat()38   private PhoneNumberUtilsCompat() {}
39 
normalizeNumber(String phoneNumber)40   public static String normalizeNumber(String phoneNumber) {
41     if (CompatUtils.isLollipopCompatible()) {
42       return PhoneNumberUtils.normalizeNumber(phoneNumber);
43     } else {
44       return normalizeNumberInternal(phoneNumber);
45     }
46   }
47 
48   /** Implementation copied from {@link PhoneNumberUtils#normalizeNumber} */
normalizeNumberInternal(String phoneNumber)49   private static String normalizeNumberInternal(String phoneNumber) {
50     if (TextUtils.isEmpty(phoneNumber)) {
51       return "";
52     }
53     StringBuilder sb = new StringBuilder();
54     int len = phoneNumber.length();
55     for (int i = 0; i < len; i++) {
56       char c = phoneNumber.charAt(i);
57       // Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.)
58       int digit = Character.digit(c, 10);
59       if (digit != -1) {
60         sb.append(digit);
61       } else if (sb.length() == 0 && c == '+') {
62         sb.append(c);
63       } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
64         return normalizeNumber(PhoneNumberUtils.convertKeypadLettersToDigits(phoneNumber));
65       }
66     }
67     return sb.toString();
68   }
69 
formatNumber( String phoneNumber, String phoneNumberE164, String defaultCountryIso)70   public static String formatNumber(
71       String phoneNumber, String phoneNumberE164, String defaultCountryIso) {
72     if (CompatUtils.isLollipopCompatible()) {
73       return PhoneNumberUtils.formatNumber(phoneNumber, phoneNumberE164, defaultCountryIso);
74     } else {
75       // This method was deprecated in API level 21, so it's only used on pre-L SDKs.
76       return PhoneNumberUtils.formatNumber(phoneNumber);
77     }
78   }
79 
createTtsSpannable(CharSequence phoneNumber)80   public static CharSequence createTtsSpannable(CharSequence phoneNumber) {
81     if (CompatUtils.isMarshmallowCompatible()) {
82       return PhoneNumberUtils.createTtsSpannable(phoneNumber);
83     } else {
84       return createTtsSpannableInternal(phoneNumber);
85     }
86   }
87 
createTtsSpan(String phoneNumber)88   public static TtsSpan createTtsSpan(String phoneNumber) {
89     if (CompatUtils.isMarshmallowCompatible()) {
90       return PhoneNumberUtils.createTtsSpan(phoneNumber);
91     } else if (CompatUtils.isLollipopCompatible()) {
92       return createTtsSpanLollipop(phoneNumber);
93     } else {
94       return null;
95     }
96   }
97 
98   /** Copied from {@link PhoneNumberUtils#createTtsSpannable} */
createTtsSpannableInternal(CharSequence phoneNumber)99   private static CharSequence createTtsSpannableInternal(CharSequence phoneNumber) {
100     if (phoneNumber == null) {
101       return null;
102     }
103     Spannable spannable = Spannable.Factory.getInstance().newSpannable(phoneNumber);
104     addTtsSpanInternal(spannable, 0, spannable.length());
105     return spannable;
106   }
107 
108   /** Compat method for addTtsSpan, see {@link PhoneNumberUtils#addTtsSpan} */
addTtsSpan(Spannable s, int start, int endExclusive)109   public static void addTtsSpan(Spannable s, int start, int endExclusive) {
110     if (CompatUtils.isMarshmallowCompatible()) {
111       PhoneNumberUtils.addTtsSpan(s, start, endExclusive);
112     } else {
113       addTtsSpanInternal(s, start, endExclusive);
114     }
115   }
116 
117   /** Copied from {@link PhoneNumberUtils#addTtsSpan} */
addTtsSpanInternal(Spannable s, int start, int endExclusive)118   private static void addTtsSpanInternal(Spannable s, int start, int endExclusive) {
119     s.setSpan(
120         createTtsSpan(s.subSequence(start, endExclusive).toString()),
121         start,
122         endExclusive,
123         Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
124   }
125 
126   /** Copied from {@link PhoneNumberUtils#createTtsSpan} */
createTtsSpanLollipop(String phoneNumberString)127   private static TtsSpan createTtsSpanLollipop(String phoneNumberString) {
128     if (phoneNumberString == null) {
129       return null;
130     }
131 
132     // Parse the phone number
133     final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
134     PhoneNumber phoneNumber = null;
135     try {
136       // Don't supply a defaultRegion so this fails for non-international numbers because
137       // we don't want to TalkBalk to read a country code (e.g. +1) if it is not already
138       // present
139       phoneNumber = phoneNumberUtil.parse(phoneNumberString, /* defaultRegion */ null);
140     } catch (NumberParseException ignored) {
141     }
142 
143     // Build a telephone tts span
144     final TtsSpan.TelephoneBuilder builder = new TtsSpan.TelephoneBuilder();
145     if (phoneNumber == null) {
146       // Strip separators otherwise TalkBack will be silent
147       // (this behavior was observed with TalkBalk 4.0.2 from their alpha channel)
148       builder.setNumberParts(splitAtNonNumerics(phoneNumberString));
149     } else {
150       if (phoneNumber.hasCountryCode()) {
151         builder.setCountryCode(Integer.toString(phoneNumber.getCountryCode()));
152       }
153       builder.setNumberParts(Long.toString(phoneNumber.getNationalNumber()));
154     }
155     return builder.build();
156   }
157 
158   /**
159    * Split a phone number using spaces, ignoring anything that is not a digit
160    *
161    * @param number A {@code CharSequence} before splitting, e.g., "+20(123)-456#"
162    * @return A {@code String} after splitting, e.g., "20 123 456".
163    */
splitAtNonNumerics(CharSequence number)164   private static String splitAtNonNumerics(CharSequence number) {
165     StringBuilder sb = new StringBuilder(number.length());
166     for (int i = 0; i < number.length(); i++) {
167       sb.append(PhoneNumberUtils.isISODigit(number.charAt(i)) ? number.charAt(i) : " ");
168     }
169     // It is very important to remove extra spaces. At time of writing, any leading or trailing
170     // spaces, or any sequence of more than one space, will confuse TalkBack and cause the TTS
171     // span to be non-functional!
172     return sb.toString().replaceAll(" +", " ").trim();
173   }
174 }
175