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.phonenumberproto; 18 19 import android.support.annotation.NonNull; 20 import android.support.annotation.Nullable; 21 import android.support.annotation.WorkerThread; 22 import android.telephony.PhoneNumberUtils; 23 import android.text.TextUtils; 24 import com.android.dialer.DialerPhoneNumber; 25 import com.android.dialer.common.Assert; 26 import com.android.dialer.common.LogUtil; 27 import com.google.i18n.phonenumbers.NumberParseException; 28 import com.google.i18n.phonenumbers.PhoneNumberUtil; 29 import com.google.i18n.phonenumbers.PhoneNumberUtil.MatchType; 30 import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; 31 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; 32 import com.google.i18n.phonenumbers.ShortNumberInfo; 33 34 /** 35 * Wrapper for selected methods in {@link PhoneNumberUtil} which uses the {@link DialerPhoneNumber} 36 * lite proto instead of the {@link com.google.i18n.phonenumbers.Phonenumber.PhoneNumber} POJO. 37 * 38 * <p>All methods should be called on a worker thread. 39 */ 40 public class DialerPhoneNumberUtil { 41 private final PhoneNumberUtil phoneNumberUtil; 42 private final ShortNumberInfo shortNumberInfo; 43 44 @WorkerThread DialerPhoneNumberUtil()45 public DialerPhoneNumberUtil() { 46 Assert.isWorkerThread(); 47 this.phoneNumberUtil = PhoneNumberUtil.getInstance(); 48 this.shortNumberInfo = ShortNumberInfo.getInstance(); 49 } 50 51 /** 52 * Parses the provided raw phone number into a {@link DialerPhoneNumber}. 53 * 54 * @see PhoneNumberUtil#parse(CharSequence, String) 55 */ 56 @WorkerThread parse(@ullable String numberToParse, @Nullable String defaultRegion)57 public DialerPhoneNumber parse(@Nullable String numberToParse, @Nullable String defaultRegion) { 58 Assert.isWorkerThread(); 59 60 DialerPhoneNumber.Builder dialerPhoneNumber = DialerPhoneNumber.newBuilder(); 61 62 if (defaultRegion != null) { 63 dialerPhoneNumber.setCountryIso(defaultRegion); 64 } 65 66 // Numbers can be null or empty for incoming "unknown" calls. 67 if (numberToParse == null) { 68 return dialerPhoneNumber.build(); 69 } 70 71 // If the number is a service number, just store the raw number and don't bother trying to parse 72 // it. PhoneNumberUtil#parse ignores these characters which can lead to confusing behavior, such 73 // as the numbers "#123" and "123" being considered the same. The "#" can appear in the middle 74 // of a service number and the "*" can appear at the beginning (see a bug). 75 if (isServiceNumber(numberToParse)) { 76 return dialerPhoneNumber.setNormalizedNumber(numberToParse).build(); 77 } 78 79 String postDialPortion = PhoneNumberUtils.extractPostDialPortion(numberToParse); 80 if (!postDialPortion.isEmpty()) { 81 dialerPhoneNumber.setPostDialPortion(postDialPortion); 82 } 83 84 String networkPortion = PhoneNumberUtils.extractNetworkPortion(numberToParse); 85 86 try { 87 PhoneNumber phoneNumber = phoneNumberUtil.parse(networkPortion, defaultRegion); 88 if (phoneNumberUtil.isValidNumber(phoneNumber)) { 89 String validNumber = phoneNumberUtil.format(phoneNumber, PhoneNumberFormat.E164); 90 if (TextUtils.isEmpty(validNumber)) { 91 throw new IllegalStateException( 92 "e164 number should not be empty: " + LogUtil.sanitizePii(numberToParse)); 93 } 94 // The E164 representation doesn't contain post-dial digits, but we need to preserve them. 95 if (!postDialPortion.isEmpty()) { 96 validNumber += postDialPortion; 97 } 98 return dialerPhoneNumber.setNormalizedNumber(validNumber).setIsValid(true).build(); 99 } 100 } catch (NumberParseException e) { 101 // fall through 102 } 103 return dialerPhoneNumber.setNormalizedNumber(networkPortion + postDialPortion).build(); 104 } 105 106 /** 107 * Returns true if the two numbers: 108 * 109 * <ul> 110 * <li>were parseable by libphonenumber (see {@link #parse(String, String)}), 111 * <li>are a {@link MatchType#SHORT_NSN_MATCH}, {@link MatchType#NSN_MATCH}, or {@link 112 * MatchType#EXACT_MATCH}, and 113 * <li>have the same post-dial digits. 114 * </ul> 115 * 116 * <p>If either number is not parseable, returns true if their raw inputs have the same network 117 * and post-dial portions. 118 * 119 * <p>An empty number is never considered to match another number. 120 * 121 * @see PhoneNumberUtil#isNumberMatch(PhoneNumber, PhoneNumber) 122 */ 123 @WorkerThread isMatch( @onNull DialerPhoneNumber firstNumberIn, @NonNull DialerPhoneNumber secondNumberIn)124 public boolean isMatch( 125 @NonNull DialerPhoneNumber firstNumberIn, @NonNull DialerPhoneNumber secondNumberIn) { 126 Assert.isWorkerThread(); 127 128 // An empty number should not be combined with any other number. 129 if (firstNumberIn.getNormalizedNumber().isEmpty() 130 || secondNumberIn.getNormalizedNumber().isEmpty()) { 131 return false; 132 } 133 134 // Two numbers with different countries should not match. 135 if (!firstNumberIn.getCountryIso().equals(secondNumberIn.getCountryIso())) { 136 return false; 137 } 138 139 PhoneNumber phoneNumber1 = null; 140 try { 141 phoneNumber1 = 142 phoneNumberUtil.parse(firstNumberIn.getNormalizedNumber(), firstNumberIn.getCountryIso()); 143 } catch (NumberParseException e) { 144 // fall through 145 } 146 147 PhoneNumber phoneNumber2 = null; 148 try { 149 phoneNumber2 = 150 phoneNumberUtil.parse( 151 secondNumberIn.getNormalizedNumber(), secondNumberIn.getCountryIso()); 152 } catch (NumberParseException e) { 153 // fall through 154 } 155 156 // If either number is a service number or either number can't be parsed by libphonenumber, just 157 // fallback to basic textual matching. 158 if (isServiceNumber(firstNumberIn.getNormalizedNumber()) 159 || isServiceNumber(secondNumberIn.getNormalizedNumber()) 160 || phoneNumber1 == null 161 || phoneNumber2 == null) { 162 return firstNumberIn.getNormalizedNumber().equals(secondNumberIn.getNormalizedNumber()); 163 } 164 165 // Both numbers are parseable, first check for short codes to so that a number like "5555" 166 // doesn't match "55555" (due to those being a SHORT_NSN_MATCH below). 167 if (shortNumberInfo.isPossibleShortNumber(phoneNumber1) 168 || shortNumberInfo.isPossibleShortNumber(phoneNumber2)) { 169 return firstNumberIn.getNormalizedNumber().equals(secondNumberIn.getNormalizedNumber()); 170 } 171 172 // Both numbers are parseable, use more sophisticated libphonenumber matching. 173 MatchType matchType = phoneNumberUtil.isNumberMatch(phoneNumber1, phoneNumber2); 174 175 return (matchType == MatchType.SHORT_NSN_MATCH 176 || matchType == MatchType.NSN_MATCH 177 || matchType == MatchType.EXACT_MATCH) 178 && firstNumberIn.getPostDialPortion().equals(secondNumberIn.getPostDialPortion()); 179 } 180 isServiceNumber(@onNull String rawNumber)181 private boolean isServiceNumber(@NonNull String rawNumber) { 182 return rawNumber.contains("#") || rawNumber.startsWith("*"); 183 } 184 } 185