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