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