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