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