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