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.google.common.base.Verify.verifyNotNull; 20 21 import android.net.Uri; 22 import android.telephony.PhoneNumberUtils; 23 import android.telephony.TelephonyManager; 24 25 import com.android.car.calendar.common.Dialer.NumberAndAccess; 26 27 import com.google.common.base.Strings; 28 import com.google.common.collect.ImmutableList; 29 30 import java.util.LinkedHashMap; 31 import java.util.List; 32 import java.util.Locale; 33 import java.util.Map; 34 import java.util.regex.Matcher; 35 import java.util.regex.Pattern; 36 37 import javax.annotation.Nullable; 38 39 /** Utilities to manipulate the description of a calendar event which may contain meta-data. */ 40 public class EventDescriptions { 41 42 // Requires a phone number to include only numbers, spaces and dash, optionally a leading "+". 43 // The number must be at least 6 characters and can contain " " or "-" but end with a digit. 44 // The access code must be at least 3 characters. 45 // The number and the access to include "pin" or "code" between the numbers. 46 private static final Pattern PHONE_PIN_PATTERN = 47 Pattern.compile( 48 "(\\+?[\\d -]{6,}\\d)(?:.*\\b(?:PIN|code)\\b.*?([\\d,;#*]{3,}))?", 49 Pattern.CASE_INSENSITIVE); 50 51 // Matches numbers in the encoded format "<tel: ... >". 52 private static final Pattern TEL_PIN_PATTERN = 53 Pattern.compile("<tel:(\\+?[\\d -]{6,})([\\d,;#*]{3,})?>"); 54 55 // Ensure numbers are over 5 digits to reduce false positives. 56 private static final int MIN_DIGITS = 5; 57 58 private final String mCountryIso; 59 EventDescriptions(Locale locale, TelephonyManager telephonyManager)60 public EventDescriptions(Locale locale, TelephonyManager telephonyManager) { 61 String networkCountryIso = telephonyManager.getNetworkCountryIso().toUpperCase(); 62 if (!Strings.isNullOrEmpty(networkCountryIso)) { 63 mCountryIso = networkCountryIso; 64 } else { 65 mCountryIso = locale.getCountry(); 66 } 67 } 68 69 /** Find conference call data embedded in the description. */ extractNumberAndPins(String descriptionText)70 public List<NumberAndAccess> extractNumberAndPins(String descriptionText) { 71 String decoded = Uri.decode(descriptionText); 72 73 // Use a map keyed by number to act like a set and only add a single number. 74 Map<String, NumberAndAccess> results = new LinkedHashMap<>(); 75 addMatchedNumbers(decoded, results, PHONE_PIN_PATTERN); 76 77 // Add the most restrictive precise format last to replace others with the same number. 78 addMatchedNumbers(decoded, results, TEL_PIN_PATTERN); 79 80 // Reverse order so the most precise format is first. 81 return ImmutableList.copyOf(results.values()).reverse(); 82 } 83 addMatchedNumbers( String decoded, Map<String, NumberAndAccess> results, Pattern phonePinPattern)84 private void addMatchedNumbers( 85 String decoded, Map<String, NumberAndAccess> results, Pattern phonePinPattern) { 86 Matcher phoneFormatMatcher = phonePinPattern.matcher(decoded); 87 while (phoneFormatMatcher.find()) { 88 NumberAndAccess numberAndAccess = validNumberAndAccess(phoneFormatMatcher); 89 if (numberAndAccess != null) { 90 results.put(numberAndAccess.getNumber(), numberAndAccess); 91 } 92 } 93 } 94 95 @Nullable validNumberAndAccess(Matcher phoneFormatMatcher)96 private NumberAndAccess validNumberAndAccess(Matcher phoneFormatMatcher) { 97 String number = verifyNotNull(phoneFormatMatcher.group(1)); 98 String access = phoneFormatMatcher.group(2); 99 100 // Ensure that there are a minimum number of digits to reduce false positives. 101 String onlyDigits = number.replaceAll("\\D", ""); 102 if (onlyDigits.length() < MIN_DIGITS) { 103 return null; 104 } 105 106 // Keep local numbers in local format which the dialer can make more sense of. 107 String formatted = PhoneNumberUtils.formatNumber(number, mCountryIso); 108 if (formatted == null) { 109 return null; 110 } 111 return new NumberAndAccess(formatted, access); 112 } 113 } 114