• 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.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
DialerPhoneNumberUtil(@onNull PhoneNumberUtil phoneNumberUtil)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
parse(@ullable String numberToParse, @Nullable String defaultRegion)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
isMatch( @onNull DialerPhoneNumber firstNumberIn, @NonNull DialerPhoneNumber secondNumberIn)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 
isServiceNumber(@onNull String rawNumber)171   private boolean isServiceNumber(@NonNull String rawNumber) {
172     return rawNumber.contains("#") || rawNumber.startsWith("*");
173   }
174 }
175