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.provider.CallLog;
21 import android.support.annotation.Nullable;
22 import android.telecom.PhoneAccountHandle;
23 import android.telephony.PhoneNumberUtils;
24 import android.telephony.TelephonyManager;
25 import android.text.TextUtils;
26 import com.android.dialer.common.LogUtil;
27 import com.android.dialer.telecom.TelecomUtil;
28 import com.google.i18n.phonenumbers.NumberParseException;
29 import com.google.i18n.phonenumbers.PhoneNumberUtil;
30 import com.google.i18n.phonenumbers.Phonenumber;
31 import com.google.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder;
32 import java.util.Arrays;
33 import java.util.HashSet;
34 import java.util.Locale;
35 import java.util.Set;
36 
37 public class PhoneNumberHelper {
38 
39   private static final String TAG = "PhoneNumberUtil";
40   private static final Set<String> LEGACY_UNKNOWN_NUMBERS =
41       new HashSet<>(Arrays.asList("-1", "-2", "-3"));
42 
43   /** Returns true if it is possible to place a call to the given number. */
canPlaceCallsTo(CharSequence number, int presentation)44   public static boolean canPlaceCallsTo(CharSequence number, int presentation) {
45     return presentation == CallLog.Calls.PRESENTATION_ALLOWED
46         && !TextUtils.isEmpty(number)
47         && !isLegacyUnknownNumbers(number);
48   }
49 
50   /**
51    * Returns true if the given number is the number of the configured voicemail. To be able to
52    * mock-out this, it is not a static method.
53    */
isVoicemailNumber( Context context, PhoneAccountHandle accountHandle, CharSequence number)54   public static boolean isVoicemailNumber(
55       Context context, PhoneAccountHandle accountHandle, CharSequence number) {
56     if (TextUtils.isEmpty(number)) {
57       return false;
58     }
59     return TelecomUtil.isVoicemailNumber(context, accountHandle, number.toString());
60   }
61 
62   /**
63    * Returns true if the given number is a SIP address. To be able to mock-out this, it is not a
64    * static method.
65    */
isSipNumber(CharSequence number)66   public static boolean isSipNumber(CharSequence number) {
67     return number != null && isUriNumber(number.toString());
68   }
69 
isUnknownNumberThatCanBeLookedUp( Context context, PhoneAccountHandle accountHandle, CharSequence number, int presentation)70   public static boolean isUnknownNumberThatCanBeLookedUp(
71       Context context, PhoneAccountHandle accountHandle, CharSequence number, int presentation) {
72     if (presentation == CallLog.Calls.PRESENTATION_UNKNOWN) {
73       return false;
74     }
75     if (presentation == CallLog.Calls.PRESENTATION_RESTRICTED) {
76       return false;
77     }
78     if (presentation == CallLog.Calls.PRESENTATION_PAYPHONE) {
79       return false;
80     }
81     if (TextUtils.isEmpty(number)) {
82       return false;
83     }
84     if (isVoicemailNumber(context, accountHandle, number)) {
85       return false;
86     }
87     if (isLegacyUnknownNumbers(number)) {
88       return false;
89     }
90     return true;
91   }
92 
isLegacyUnknownNumbers(CharSequence number)93   public static boolean isLegacyUnknownNumbers(CharSequence number) {
94     return number != null && LEGACY_UNKNOWN_NUMBERS.contains(number.toString());
95   }
96 
97   /**
98    * @return a geographical description string for the specified number.
99    * @see com.android.i18n.phonenumbers.PhoneNumberOfflineGeocoder
100    */
getGeoDescription(Context context, String number)101   public static String getGeoDescription(Context context, String number) {
102     LogUtil.v("PhoneNumberHelper.getGeoDescription", "" + LogUtil.sanitizePii(number));
103 
104     if (TextUtils.isEmpty(number)) {
105       return null;
106     }
107 
108     PhoneNumberUtil util = PhoneNumberUtil.getInstance();
109     PhoneNumberOfflineGeocoder geocoder = PhoneNumberOfflineGeocoder.getInstance();
110 
111     Locale locale = context.getResources().getConfiguration().locale;
112     String countryIso = getCurrentCountryIso(context, locale);
113     Phonenumber.PhoneNumber pn = null;
114     try {
115       LogUtil.v(
116           "PhoneNumberHelper.getGeoDescription",
117           "parsing '" + LogUtil.sanitizePii(number) + "' for countryIso '" + countryIso + "'...");
118       pn = util.parse(number, countryIso);
119       LogUtil.v(
120           "PhoneNumberHelper.getGeoDescription", "- parsed number: " + LogUtil.sanitizePii(pn));
121     } catch (NumberParseException e) {
122       LogUtil.e(
123           "PhoneNumberHelper.getGeoDescription",
124           "getGeoDescription: NumberParseException for incoming number '"
125               + LogUtil.sanitizePii(number)
126               + "'");
127     }
128 
129     if (pn != null) {
130       String description = geocoder.getDescriptionForNumber(pn, locale);
131       LogUtil.v("PhoneNumberHelper.getGeoDescription", "- got description: '" + description + "'");
132       return description;
133     }
134 
135     return null;
136   }
137 
138   /**
139    * @return The ISO 3166-1 two letters country code of the country the user is in based on the
140    *     network location. If the network location does not exist, fall back to the locale setting.
141    */
getCurrentCountryIso(Context context, Locale locale)142   public static String getCurrentCountryIso(Context context, Locale locale) {
143     // Without framework function calls, this seems to be the most accurate location service
144     // we can rely on.
145     final TelephonyManager telephonyManager =
146         (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
147 
148     String countryIso = telephonyManager.getNetworkCountryIso();
149     if (TextUtils.isEmpty(countryIso)) {
150       countryIso = locale.getCountry();
151       LogUtil.i(
152           "PhoneNumberHelper.getCurrentCountryIso",
153           "No CountryDetector; falling back to countryIso based on locale: " + countryIso);
154     }
155     countryIso = countryIso.toUpperCase();
156 
157     return countryIso;
158   }
159 
160   /**
161    * @return Formatted phone number. e.g. 1-123-456-7890. Returns the original number if formatting
162    *     failed.
163    */
formatNumber(@ullable String number, Context context)164   public static String formatNumber(@Nullable String number, Context context) {
165     // The number can be null e.g. schema is voicemail and uri content is empty.
166     if (number == null) {
167       return null;
168     }
169     String formattedNumber =
170         PhoneNumberUtils.formatNumber(
171             number, PhoneNumberHelper.getCurrentCountryIso(context, Locale.getDefault()));
172     return formattedNumber != null ? formattedNumber : number;
173   }
174 
175   /**
176    * Determines if the specified number is actually a URI (i.e. a SIP address) rather than a regular
177    * PSTN phone number, based on whether or not the number contains an "@" character.
178    *
179    * @param number Phone number
180    * @return true if number contains @
181    *     <p>TODO: Remove if PhoneNumberUtils.isUriNumber(String number) is made public.
182    */
isUriNumber(String number)183   public static boolean isUriNumber(String number) {
184     // Note we allow either "@" or "%40" to indicate a URI, in case
185     // the passed-in string is URI-escaped.  (Neither "@" nor "%40"
186     // will ever be found in a legal PSTN number.)
187     return number != null && (number.contains("@") || number.contains("%40"));
188   }
189 
190   /**
191    * @param number SIP address of the form "username@domainname" (or the URI-escaped equivalent
192    *     "username%40domainname")
193    *     <p>TODO: Remove if PhoneNumberUtils.getUsernameFromUriNumber(String number) is made public.
194    * @return the "username" part of the specified SIP address, i.e. the part before the "@"
195    *     character (or "%40").
196    */
getUsernameFromUriNumber(String number)197   public static String getUsernameFromUriNumber(String number) {
198     // The delimiter between username and domain name can be
199     // either "@" or "%40" (the URI-escaped equivalent.)
200     int delimiterIndex = number.indexOf('@');
201     if (delimiterIndex < 0) {
202       delimiterIndex = number.indexOf("%40");
203     }
204     if (delimiterIndex < 0) {
205       LogUtil.i(
206           "PhoneNumberHelper.getUsernameFromUriNumber",
207           "getUsernameFromUriNumber: no delimiter found in SIP address: "
208               + LogUtil.sanitizePii(number));
209       return number;
210     }
211     return number.substring(0, delimiterIndex);
212   }
213 
214 
isVerizon(Context context)215   private static boolean isVerizon(Context context) {
216     // Verizon MCC/MNC codes copied from com/android/voicemailomtp/res/xml/vvm_config.xml.
217     // TODO: Need a better way to do per carrier and per OEM configurations.
218     switch (context.getSystemService(TelephonyManager.class).getSimOperator()) {
219       case "310004":
220       case "310010":
221       case "310012":
222       case "310013":
223       case "310590":
224       case "310890":
225       case "310910":
226       case "311110":
227       case "311270":
228       case "311271":
229       case "311272":
230       case "311273":
231       case "311274":
232       case "311275":
233       case "311276":
234       case "311277":
235       case "311278":
236       case "311279":
237       case "311280":
238       case "311281":
239       case "311282":
240       case "311283":
241       case "311284":
242       case "311285":
243       case "311286":
244       case "311287":
245       case "311288":
246       case "311289":
247       case "311390":
248       case "311480":
249       case "311481":
250       case "311482":
251       case "311483":
252       case "311484":
253       case "311485":
254       case "311486":
255       case "311487":
256       case "311488":
257       case "311489":
258         return true;
259       default:
260         return false;
261     }
262   }
263 
264   /**
265    * Gets the label to display for a phone call where the presentation is set as
266    * PRESENTATION_RESTRICTED. For Verizon we want this to be displayed as "Restricted". For all
267    * other carriers we want this to be be displayed as "Private number".
268    */
getDisplayNameForRestrictedNumber(Context context)269   public static CharSequence getDisplayNameForRestrictedNumber(Context context) {
270     if (isVerizon(context)) {
271       return context.getString(R.string.private_num_verizon);
272     } else {
273       return context.getString(R.string.private_num_non_verizon);
274     }
275   }
276 }
277