1 /*
2  * Copyright 2020 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.car.calendar.common;
18 
19 import static com.android.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL;
20 import static com.android.i18n.phonenumbers.PhoneNumberUtil.ValidationResult.IS_POSSIBLE;
21 import static com.android.i18n.phonenumbers.PhoneNumberUtil.ValidationResult.IS_POSSIBLE_LOCAL_ONLY;
22 import static com.android.i18n.phonenumbers.PhoneNumberUtil.ValidationResult.TOO_LONG;
23 
24 import static com.google.common.base.Verify.verifyNotNull;
25 
26 import android.net.Uri;
27 
28 import com.android.car.calendar.common.Dialer.NumberAndAccess;
29 import com.android.i18n.phonenumbers.NumberParseException;
30 import com.android.i18n.phonenumbers.PhoneNumberUtil;
31 import com.android.i18n.phonenumbers.Phonenumber;
32 
33 import com.google.common.collect.ImmutableList;
34 
35 import java.util.LinkedHashMap;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Map;
39 import java.util.regex.Matcher;
40 import java.util.regex.Pattern;
41 
42 import javax.annotation.Nullable;
43 
44 /** Utilities to manipulate the description of a calendar event which may contain meta-data. */
45 public class EventDescriptions {
46 
47     // Requires a phone number to include only numbers, spaces and dash, optionally a leading "+".
48     // The number must be at least 6 characters.
49     // The access code must be at least 3 characters.
50     // The number and the access to include "pin" or "code" between the numbers.
51     private static final Pattern PHONE_PIN_PATTERN =
52             Pattern.compile(
53                     "(\\+?[\\d -]{6,})(?:.*\\b(?:PIN|code)\\b.*?([\\d,;#*]{3,}))?",
54                     Pattern.CASE_INSENSITIVE);
55 
56     // Matches numbers in the encoded format "<tel: ... >".
57     private static final Pattern TEL_PIN_PATTERN =
58             Pattern.compile("<tel:(\\+?[\\d -]{6,})([\\d,;#*]{3,})?>");
59 
60     private static final PhoneNumberUtil PHONE_NUMBER_UTIL = PhoneNumberUtil.getInstance();
61 
62     // Ensure numbers are over 5 digits to reduce false positives.
63     private static final int MIN_NATIONAL_NUMBER = 10_000;
64 
65     private final Locale mLocale;
66 
EventDescriptions(Locale locale)67     public EventDescriptions(Locale locale) {
68         mLocale = locale;
69     }
70 
71     /** Find conference call data embedded in the description. */
extractNumberAndPins(String descriptionText)72     public List<NumberAndAccess> extractNumberAndPins(String descriptionText) {
73         String decoded = Uri.decode(descriptionText);
74 
75         Map<String, NumberAndAccess> results = new LinkedHashMap<>();
76         addMatchedNumbers(decoded, results, PHONE_PIN_PATTERN);
77         addMatchedNumbers(decoded, results, TEL_PIN_PATTERN);
78         return ImmutableList.copyOf(results.values());
79     }
80 
addMatchedNumbers( String decoded, Map<String, NumberAndAccess> results, Pattern phonePinPattern)81     private void addMatchedNumbers(
82             String decoded, Map<String, NumberAndAccess> results, Pattern phonePinPattern) {
83         Matcher phoneFormatMatcher = phonePinPattern.matcher(decoded);
84         while (phoneFormatMatcher.find()) {
85             NumberAndAccess numberAndAccess = validNumberAndAccess(phoneFormatMatcher);
86             if (numberAndAccess != null) {
87                 results.put(numberAndAccess.getNumber(), numberAndAccess);
88             }
89         }
90     }
91 
92     @Nullable
validNumberAndAccess(Matcher phoneFormatMatcher)93     private NumberAndAccess validNumberAndAccess(Matcher phoneFormatMatcher) {
94         String number = verifyNotNull(phoneFormatMatcher.group(1));
95         String access = phoneFormatMatcher.group(2);
96         try {
97             Phonenumber.PhoneNumber phoneNumber =
98                     PHONE_NUMBER_UTIL.parse(number, mLocale.getCountry());
99             PhoneNumberUtil.ValidationResult result =
100                     PHONE_NUMBER_UTIL.isPossibleNumberWithReason(phoneNumber);
101             if (isAcceptableResult(result)) {
102                 if (phoneNumber.getNationalNumber() < MIN_NATIONAL_NUMBER) {
103                     return null;
104                 }
105                 String formatted = PHONE_NUMBER_UTIL.format(phoneNumber, INTERNATIONAL);
106                 return new NumberAndAccess(formatted, access);
107             }
108         } catch (NumberParseException e) {
109             // Ignore invalid numbers.
110         }
111         return null;
112     }
113 
isAcceptableResult(PhoneNumberUtil.ValidationResult result)114     private boolean isAcceptableResult(PhoneNumberUtil.ValidationResult result) {
115         // The result can be too long and still valid because the US locale is used by default
116         // which does not accept valid long numbers from other regions.
117         return result == IS_POSSIBLE || result == IS_POSSIBLE_LOCAL_ONLY || result == TOO_LONG;
118     }
119 }
120