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