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