1 /*
2  * Copyright (C) 2017 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.assisteddialing;
18 
19 import android.content.Context;
20 import android.support.annotation.NonNull;
21 import android.telephony.PhoneNumberUtils;
22 import android.text.TextUtils;
23 import com.android.dialer.common.LogUtil;
24 import com.android.dialer.logging.DialerImpression;
25 import com.android.dialer.logging.Logger;
26 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
27 import com.android.dialer.strictmode.StrictModeUtils;
28 import com.google.i18n.phonenumbers.NumberParseException;
29 import com.google.i18n.phonenumbers.PhoneNumberUtil;
30 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
31 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber.CountryCodeSource;
32 import java.util.Locale;
33 import java.util.Optional;
34 
35 /** Ensures that a number is eligible for Assisted Dialing */
36 final class Constraints {
37   private final PhoneNumberUtil phoneNumberUtil;
38   private final Context context;
39   private final CountryCodeProvider countryCodeProvider;
40 
41   /**
42    * Create a new instance of Constraints.
43    *
44    * @param context The context used to determine whether or not a number is an emergency number.
45    * @param countryCodeProvider A csv of supported country codes, e.g. "US,CA"
46    */
Constraints(@onNull Context context, @NonNull CountryCodeProvider countryCodeProvider)47   public Constraints(@NonNull Context context, @NonNull CountryCodeProvider countryCodeProvider) {
48     if (context == null) {
49       throw new NullPointerException("Provided context cannot be null");
50     }
51     this.context = context;
52 
53     if (countryCodeProvider == null) {
54       throw new NullPointerException("Provided configProviderCountryCodes cannot be null");
55     }
56 
57     this.countryCodeProvider = countryCodeProvider;
58     this.phoneNumberUtil = StrictModeUtils.bypass(() -> PhoneNumberUtil.getInstance());
59   }
60 
61   /**
62    * Determines whether or not we think Assisted Dialing is possible given the provided parameters.
63    *
64    * @param numberToCheck A string containing the phone number.
65    * @param userHomeCountryCode A string containing an ISO 3166-1 alpha-2 country code representing
66    *     the user's home country.
67    * @param userRoamingCountryCode A string containing an ISO 3166-1 alpha-2 country code
68    *     representing the user's roaming country.
69    * @return A boolean indicating whether or not the provided values are eligible for assisted
70    *     dialing.
71    */
meetsPreconditions( @onNull String numberToCheck, @NonNull String userHomeCountryCode, @NonNull String userRoamingCountryCode)72   boolean meetsPreconditions(
73       @NonNull String numberToCheck,
74       @NonNull String userHomeCountryCode,
75       @NonNull String userRoamingCountryCode) {
76 
77     if (TextUtils.isEmpty(numberToCheck)) {
78       LogUtil.i("Constraints.meetsPreconditions", "numberToCheck was empty");
79       return false;
80     }
81 
82     if (TextUtils.isEmpty(userHomeCountryCode)) {
83       LogUtil.i("Constraints.meetsPreconditions", "userHomeCountryCode was empty");
84       return false;
85     }
86 
87     if (TextUtils.isEmpty(userRoamingCountryCode)) {
88       LogUtil.i("Constraints.meetsPreconditions", "userRoamingCountryCode was empty");
89       return false;
90     }
91 
92     userHomeCountryCode = userHomeCountryCode.toUpperCase(Locale.US);
93     userRoamingCountryCode = userRoamingCountryCode.toUpperCase(Locale.US);
94 
95     Optional<PhoneNumber> parsedPhoneNumber = parsePhoneNumber(numberToCheck, userHomeCountryCode);
96 
97     if (!parsedPhoneNumber.isPresent()) {
98       LogUtil.i("Constraints.meetsPreconditions", "parsedPhoneNumber was empty");
99       return false;
100     }
101 
102     return areSupportedCountryCodes(userHomeCountryCode, userRoamingCountryCode)
103         && isUserRoaming(userHomeCountryCode, userRoamingCountryCode)
104         && isNotInternationalNumber(parsedPhoneNumber)
105         && isNotEmergencyNumber(numberToCheck, context)
106         && isValidNumber(parsedPhoneNumber)
107         && doesNotHaveExtension(parsedPhoneNumber);
108   }
109 
110   /** Returns a boolean indicating the value equivalence of the provided country codes. */
isUserRoaming( @onNull String userHomeCountryCode, @NonNull String userRoamingCountryCode)111   private boolean isUserRoaming(
112       @NonNull String userHomeCountryCode, @NonNull String userRoamingCountryCode) {
113     boolean result = !userHomeCountryCode.equals(userRoamingCountryCode);
114     LogUtil.i("Constraints.isUserRoaming", String.valueOf(result));
115     return result;
116   }
117 
118   /**
119    * Returns a boolean indicating the support of both provided country codes for assisted dialing.
120    * Both country codes must be allowed for the return value to be true.
121    */
areSupportedCountryCodes( @onNull String userHomeCountryCode, @NonNull String userRoamingCountryCode)122   private boolean areSupportedCountryCodes(
123       @NonNull String userHomeCountryCode, @NonNull String userRoamingCountryCode) {
124     if (TextUtils.isEmpty(userHomeCountryCode)) {
125       LogUtil.i("Constraints.areSupportedCountryCodes", "userHomeCountryCode was empty");
126       return false;
127     }
128 
129     if (TextUtils.isEmpty(userRoamingCountryCode)) {
130       LogUtil.i("Constraints.areSupportedCountryCodes", "userRoamingCountryCode was empty");
131       return false;
132     }
133 
134     boolean result =
135         countryCodeProvider.isSupportedCountryCode(userHomeCountryCode)
136             && countryCodeProvider.isSupportedCountryCode(userRoamingCountryCode);
137     LogUtil.i("Constraints.areSupportedCountryCodes", String.valueOf(result));
138     return result;
139   }
140 
141   /**
142    * A convenience method to take a number as a String and a specified country code, and return a
143    * PhoneNumber object.
144    */
parsePhoneNumber( @onNull String numberToParse, @NonNull String userHomeCountryCode)145   private Optional<PhoneNumber> parsePhoneNumber(
146       @NonNull String numberToParse, @NonNull String userHomeCountryCode) {
147     return StrictModeUtils.bypass(
148         () -> {
149           try {
150             return Optional.of(
151                 phoneNumberUtil.parseAndKeepRawInput(numberToParse, userHomeCountryCode));
152           } catch (NumberParseException e) {
153             Logger.get(context)
154                 .logImpression(DialerImpression.Type.ASSISTED_DIALING_CONSTRAINT_PARSING_FAILURE);
155             LogUtil.i("Constraints.parsePhoneNumber", "could not parse the number");
156             return Optional.empty();
157           }
158         });
159   }
160 
161   /** Returns a boolean indicating if the provided number is already internationally formatted. */
162   private boolean isNotInternationalNumber(@NonNull Optional<PhoneNumber> parsedPhoneNumber) {
163 
164     if (parsedPhoneNumber.get().hasCountryCode()
165         && parsedPhoneNumber.get().getCountryCodeSource()
166             != CountryCodeSource.FROM_DEFAULT_COUNTRY) {
167       Logger.get(context)
168           .logImpression(DialerImpression.Type.ASSISTED_DIALING_CONSTRAINT_NUMBER_HAS_COUNTRY_CODE);
169       LogUtil.i(
170           "Constraints.isNotInternationalNumber", "phone number already provided the country code");
171       return false;
172     }
173     return true;
174   }
175 
176   /**
177    * Returns a boolean indicating if the provided number has an extension.
178    *
179    * <p>Extensions are currently stripped when formatting a number for mobile dialing, so we don't
180    * want to purposefully truncate a number.
181    */
182   private boolean doesNotHaveExtension(@NonNull Optional<PhoneNumber> parsedPhoneNumber) {
183 
184     if (parsedPhoneNumber.get().hasExtension()
185         && !TextUtils.isEmpty(parsedPhoneNumber.get().getExtension())) {
186       Logger.get(context)
187           .logImpression(DialerImpression.Type.ASSISTED_DIALING_CONSTRAINT_NUMBER_HAS_EXTENSION);
188       LogUtil.i("Constraints.doesNotHaveExtension", "phone number has an extension");
189       return false;
190     }
191     return true;
192   }
193 
194   /** Returns a boolean indicating if the provided number is considered to be a valid number. */
195   private boolean isValidNumber(@NonNull Optional<PhoneNumber> parsedPhoneNumber) {
196     boolean result =
197         StrictModeUtils.bypass(() -> phoneNumberUtil.isValidNumber(parsedPhoneNumber.get()));
198     LogUtil.i("Constraints.isValidNumber", String.valueOf(result));
199 
200     return result;
201   }
202 
203   /** Returns a boolean indicating if the provided number is an emergency number. */
204   private boolean isNotEmergencyNumber(@NonNull String numberToCheck, @NonNull Context context) {
205     // isEmergencyNumber may depend on network state, so also use isLocalEmergencyNumber when
206     // roaming and out of service.
207     boolean result =
208         !PhoneNumberUtils.isEmergencyNumber(numberToCheck)
209             && !PhoneNumberHelper.isLocalEmergencyNumber(context, numberToCheck);
210     LogUtil.i("Constraints.isNotEmergencyNumber", String.valueOf(result));
211     return result;
212   }
213 }
214