• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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.dialer.phonenumberutil;
18 
19 import android.content.Context;
20 import android.database.Cursor;
21 import android.net.Uri;
22 import android.os.Trace;
23 import android.provider.CallLog;
24 import android.support.annotation.NonNull;
25 import android.support.annotation.Nullable;
26 import android.telecom.PhoneAccountHandle;
27 import android.telephony.PhoneNumberUtils;
28 import android.telephony.TelephonyManager;
29 import android.text.BidiFormatter;
30 import android.text.TextDirectionHeuristics;
31 import android.text.TextUtils;
32 import com.android.dialer.common.Assert;
33 import com.android.dialer.common.LogUtil;
34 import com.android.dialer.compat.CompatUtils;
35 import com.android.dialer.compat.telephony.TelephonyManagerCompat;
36 import com.android.dialer.phonenumbergeoutil.PhoneNumberGeoUtilComponent;
37 import com.android.dialer.telecom.TelecomUtil;
38 import com.google.common.base.Ascii;
39 import java.util.Arrays;
40 import java.util.HashSet;
41 import java.util.Set;
42 
43 public class PhoneNumberHelper {
44 
45   private static final String TAG = "PhoneNumberUtil";
46   private static final Set<String> LEGACY_UNKNOWN_NUMBERS =
47       new HashSet<>(Arrays.asList("-1", "-2", "-3"));
48 
49   /** Returns true if it is possible to place a call to the given number. */
canPlaceCallsTo(CharSequence number, int presentation)50   public static boolean canPlaceCallsTo(CharSequence number, int presentation) {
51     return presentation == CallLog.Calls.PRESENTATION_ALLOWED
52         && !TextUtils.isEmpty(number)
53         && !isLegacyUnknownNumbers(number);
54   }
55 
56   /**
57    * Move the given cursor to a position where the number it points to matches the number in a
58    * contact lookup URI.
59    *
60    * <p>We assume the cursor is one returned by the Contacts Provider when the URI asks for a
61    * specific number. This method's behavior is undefined when the cursor doesn't meet the
62    * assumption.
63    *
64    * <p>When determining whether two phone numbers are identical enough for caller ID purposes, the
65    * Contacts Provider ignores special characters such as '#'. This makes it possible for the cursor
66    * returned by the Contacts Provider to have multiple rows even when the URI asks for a specific
67    * number.
68    *
69    * <p>For example, suppose the user has two contacts whose numbers are "#123" and "123",
70    * respectively. When the URI asks for number "123", both numbers will be returned. Therefore, the
71    * following strategy is employed to find a match.
72    *
73    * <p>In the following description, we use E to denote a number the cursor points to (an existing
74    * contact number), and L to denote the number in the contact lookup URI.
75    *
76    * <p>If neither E nor L contains special characters, return true to indicate a match is found.
77    *
78    * <p>If either E or L contains special characters, return true when the raw numbers of E and L
79    * are the same. Otherwise, move the cursor to its next position and start over.
80    *
81    * <p>Return false in all other circumstances to indicate that no match can be found.
82    *
83    * <p>When no match can be found, the cursor is after the last result when the method returns.
84    *
85    * @param cursor A cursor returned by the Contacts Provider.
86    * @param columnIndexForNumber The index of the column where phone numbers are stored. It is the
87    *     caller's responsibility to pass the correct column index.
88    * @param contactLookupUri A URI used to retrieve a contact via the Contacts Provider. It is the
89    *     caller's responsibility to ensure the URI is one that asks for a specific phone number.
90    * @return true if a match can be found.
91    */
updateCursorToMatchContactLookupUri( @ullable Cursor cursor, int columnIndexForNumber, @Nullable Uri contactLookupUri)92   public static boolean updateCursorToMatchContactLookupUri(
93       @Nullable Cursor cursor, int columnIndexForNumber, @Nullable Uri contactLookupUri) {
94     if (cursor == null || contactLookupUri == null) {
95       return false;
96     }
97 
98     if (!cursor.moveToFirst()) {
99       return false;
100     }
101 
102     Assert.checkArgument(
103         0 <= columnIndexForNumber && columnIndexForNumber < cursor.getColumnCount());
104 
105     String lookupNumber = contactLookupUri.getLastPathSegment();
106     if (TextUtils.isEmpty(lookupNumber)) {
107       return false;
108     }
109 
110     boolean lookupNumberHasSpecialChars = numberHasSpecialChars(lookupNumber);
111 
112     do {
113       String existingContactNumber = cursor.getString(columnIndexForNumber);
114       boolean existingContactNumberHasSpecialChars = numberHasSpecialChars(existingContactNumber);
115 
116       if ((!lookupNumberHasSpecialChars && !existingContactNumberHasSpecialChars)
117           || sameRawNumbers(existingContactNumber, lookupNumber)) {
118         return true;
119       }
120 
121     } while (cursor.moveToNext());
122 
123     return false;
124   }
125 
126   /** Returns true if the input phone number contains special characters. */
numberHasSpecialChars(String number)127   public static boolean numberHasSpecialChars(String number) {
128     return !TextUtils.isEmpty(number) && number.contains("#");
129   }
130 
131   /** Returns true if the raw numbers of the two input phone numbers are the same. */
sameRawNumbers(String number1, String number2)132   public static boolean sameRawNumbers(String number1, String number2) {
133     String rawNumber1 =
134         PhoneNumberUtils.stripSeparators(PhoneNumberUtils.convertKeypadLettersToDigits(number1));
135     String rawNumber2 =
136         PhoneNumberUtils.stripSeparators(PhoneNumberUtils.convertKeypadLettersToDigits(number2));
137 
138     return rawNumber1.equals(rawNumber2);
139   }
140 
141   /**
142    * Returns true if the given number is the number of the configured voicemail. To be able to
143    * mock-out this, it is not a static method.
144    */
isVoicemailNumber( Context context, PhoneAccountHandle accountHandle, CharSequence number)145   public static boolean isVoicemailNumber(
146       Context context, PhoneAccountHandle accountHandle, CharSequence number) {
147     if (TextUtils.isEmpty(number)) {
148       return false;
149     }
150     return TelecomUtil.isVoicemailNumber(context, accountHandle, number.toString());
151   }
152 
153   /**
154    * Returns true if the given number is a SIP address. To be able to mock-out this, it is not a
155    * static method.
156    */
isSipNumber(CharSequence number)157   public static boolean isSipNumber(CharSequence number) {
158     return number != null && isUriNumber(number.toString());
159   }
160 
isUnknownNumberThatCanBeLookedUp( Context context, PhoneAccountHandle accountHandle, CharSequence number, int presentation)161   public static boolean isUnknownNumberThatCanBeLookedUp(
162       Context context, PhoneAccountHandle accountHandle, CharSequence number, int presentation) {
163     if (presentation == CallLog.Calls.PRESENTATION_UNKNOWN) {
164       return false;
165     }
166     if (presentation == CallLog.Calls.PRESENTATION_RESTRICTED) {
167       return false;
168     }
169     if (presentation == CallLog.Calls.PRESENTATION_PAYPHONE) {
170       return false;
171     }
172     if (TextUtils.isEmpty(number)) {
173       return false;
174     }
175     if (isVoicemailNumber(context, accountHandle, number)) {
176       return false;
177     }
178     if (isLegacyUnknownNumbers(number)) {
179       return false;
180     }
181     return true;
182   }
183 
isLegacyUnknownNumbers(CharSequence number)184   public static boolean isLegacyUnknownNumbers(CharSequence number) {
185     return number != null && LEGACY_UNKNOWN_NUMBERS.contains(number.toString());
186   }
187 
188   /**
189    * @param countryIso Country ISO used if there is no country code in the number, may be null
190    *     otherwise.
191    * @return a geographical description string for the specified number.
192    */
getGeoDescription( Context context, String number, @Nullable String countryIso)193   public static String getGeoDescription(
194       Context context, String number, @Nullable String countryIso) {
195     return PhoneNumberGeoUtilComponent.get(context)
196         .getPhoneNumberGeoUtil()
197         .getGeoDescription(context, number, countryIso);
198   }
199 
200   /**
201    * @param phoneAccountHandle {@code PhonAccountHandle} used to get current network country ISO.
202    *     May be null if no account is in use or selected, in which case default account will be
203    *     used.
204    * @return The ISO 3166-1 two letters country code of the country the user is in based on the
205    *     network location. If the network location does not exist, fall back to the locale setting.
206    */
getCurrentCountryIso( Context context, @Nullable PhoneAccountHandle phoneAccountHandle)207   public static String getCurrentCountryIso(
208       Context context, @Nullable PhoneAccountHandle phoneAccountHandle) {
209     Trace.beginSection("PhoneNumberHelper.getCurrentCountryIso");
210     // Without framework function calls, this seems to be the most accurate location service
211     // we can rely on.
212     String countryIso =
213         TelephonyManagerCompat.getNetworkCountryIsoForPhoneAccountHandle(
214             context, phoneAccountHandle);
215     if (TextUtils.isEmpty(countryIso)) {
216       countryIso = CompatUtils.getLocale(context).getCountry();
217       LogUtil.i(
218           "PhoneNumberHelper.getCurrentCountryIso",
219           "No CountryDetector; falling back to countryIso based on locale: " + countryIso);
220     }
221     countryIso = countryIso.toUpperCase();
222     Trace.endSection();
223 
224     return countryIso;
225   }
226 
227   /**
228    * An enhanced version of {@link PhoneNumberUtils#formatNumber(String, String, String)}.
229    *
230    * <p>The {@link Context} parameter allows us to tweak formatting according to device properties.
231    *
232    * <p>Returns the formatted phone number (e.g, 1-123-456-7890) or the original number if
233    * formatting fails or is intentionally ignored.
234    */
formatNumber( Context context, @Nullable String number, @Nullable String numberE164, String countryIso)235   public static String formatNumber(
236       Context context, @Nullable String number, @Nullable String numberE164, String countryIso) {
237     // The number can be null e.g. schema is voicemail and uri content is empty.
238     if (number == null) {
239       return null;
240     }
241 
242     // Argentina phone number formats are complex and PhoneNumberUtils doesn't format all Argentina
243     // numbers correctly.
244     // To ensure consistent user experience, we disable phone number formatting for all numbers
245     // (not just Argentinian ones) for devices with Argentinian SIMs.
246     TelephonyManager telephonyManager =
247         (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
248     if (telephonyManager != null
249         && "AR".equals(Ascii.toUpperCase(telephonyManager.getSimCountryIso()))) {
250       return number;
251     }
252 
253     String formattedNumber = PhoneNumberUtils.formatNumber(number, numberE164, countryIso);
254     return formattedNumber != null ? formattedNumber : number;
255   }
256 
257   /** @see #formatNumber(Context, String, String, String). */
formatNumber(Context context, @Nullable String number, String countryIso)258   public static String formatNumber(Context context, @Nullable String number, String countryIso) {
259     return formatNumber(context, number, /* numberE164 = */ null, countryIso);
260   }
261 
262   @Nullable
formatNumberForDisplay( Context context, @Nullable String number, @NonNull String countryIso)263   public static CharSequence formatNumberForDisplay(
264       Context context, @Nullable String number, @NonNull String countryIso) {
265     if (number == null) {
266       return null;
267     }
268 
269     return PhoneNumberUtils.createTtsSpannable(
270         BidiFormatter.getInstance()
271             .unicodeWrap(formatNumber(context, number, countryIso), TextDirectionHeuristics.LTR));
272   }
273 
274   /**
275    * Determines if the specified number is actually a URI (i.e. a SIP address) rather than a regular
276    * PSTN phone number, based on whether or not the number contains an "@" character.
277    *
278    * @param number Phone number
279    * @return true if number contains @
280    *     <p>TODO: Remove if PhoneNumberUtils.isUriNumber(String number) is made public.
281    */
isUriNumber(String number)282   public static boolean isUriNumber(String number) {
283     // Note we allow either "@" or "%40" to indicate a URI, in case
284     // the passed-in string is URI-escaped.  (Neither "@" nor "%40"
285     // will ever be found in a legal PSTN number.)
286     return number != null && (number.contains("@") || number.contains("%40"));
287   }
288 
289   /**
290    * @param number SIP address of the form "username@domainname" (or the URI-escaped equivalent
291    *     "username%40domainname")
292    *     <p>TODO: Remove if PhoneNumberUtils.getUsernameFromUriNumber(String number) is made public.
293    * @return the "username" part of the specified SIP address, i.e. the part before the "@"
294    *     character (or "%40").
295    */
getUsernameFromUriNumber(String number)296   public static String getUsernameFromUriNumber(String number) {
297     // The delimiter between username and domain name can be
298     // either "@" or "%40" (the URI-escaped equivalent.)
299     int delimiterIndex = number.indexOf('@');
300     if (delimiterIndex < 0) {
301       delimiterIndex = number.indexOf("%40");
302     }
303     if (delimiterIndex < 0) {
304       LogUtil.i(
305           "PhoneNumberHelper.getUsernameFromUriNumber",
306           "getUsernameFromUriNumber: no delimiter found in SIP address: "
307               + LogUtil.sanitizePii(number));
308       return number;
309     }
310     return number.substring(0, delimiterIndex);
311   }
312 
isVerizon(Context context)313   private static boolean isVerizon(Context context) {
314     // Verizon MCC/MNC codes copied from com/android/voicemailomtp/res/xml/vvm_config.xml.
315     // TODO(sail): Need a better way to do per carrier and per OEM configurations.
316     switch (context.getSystemService(TelephonyManager.class).getSimOperator()) {
317       case "310004":
318       case "310010":
319       case "310012":
320       case "310013":
321       case "310590":
322       case "310890":
323       case "310910":
324       case "311110":
325       case "311270":
326       case "311271":
327       case "311272":
328       case "311273":
329       case "311274":
330       case "311275":
331       case "311276":
332       case "311277":
333       case "311278":
334       case "311279":
335       case "311280":
336       case "311281":
337       case "311282":
338       case "311283":
339       case "311284":
340       case "311285":
341       case "311286":
342       case "311287":
343       case "311288":
344       case "311289":
345       case "311390":
346       case "311480":
347       case "311481":
348       case "311482":
349       case "311483":
350       case "311484":
351       case "311485":
352       case "311486":
353       case "311487":
354       case "311488":
355       case "311489":
356         return true;
357       default:
358         return false;
359     }
360   }
361 
362   /**
363    * Gets the label to display for a phone call where the presentation is set as
364    * PRESENTATION_RESTRICTED. For Verizon we want this to be displayed as "Restricted". For all
365    * other carriers we want this to be be displayed as "Private number".
366    */
getDisplayNameForRestrictedNumber(Context context)367   public static String getDisplayNameForRestrictedNumber(Context context) {
368     if (isVerizon(context)) {
369       return context.getString(R.string.private_num_verizon);
370     } else {
371       return context.getString(R.string.private_num_non_verizon);
372     }
373   }
374 }
375