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