/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.telephony; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.os.PersistableBundle; import android.os.UserHandle; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; import android.telephony.CarrierConfigManager; import android.telephony.Rlog; import android.telephony.SmsMessage; import android.telephony.SubscriptionManager; import android.text.TextUtils; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; /** * The SMS filter for parsing SMS from carrier to notify users about the missed incoming call. */ public class MissedIncomingCallSmsFilter { private static final String TAG = MissedIncomingCallSmsFilter.class.getSimpleName(); private static final boolean VDBG = false; // STOPSHIP if true private static final String SMS_YEAR_TAG = "year"; private static final String SMS_MONTH_TAG = "month"; private static final String SMS_DAY_TAG = "day"; private static final String SMS_HOUR_TAG = "hour"; private static final String SMS_MINUTE_TAG = "minute"; private static final String SMS_CALLER_ID_TAG = "callerId"; private static final ComponentName PSTN_CONNECTION_SERVICE_COMPONENT = new ComponentName("com.android.phone", "com.android.services.telephony.TelephonyConnectionService"); private final Phone mPhone; private PersistableBundle mCarrierConfig; /** * Constructor * * @param phone The phone instance */ public MissedIncomingCallSmsFilter(Phone phone) { mPhone = phone; CarrierConfigManager configManager = (CarrierConfigManager) mPhone.getContext() .getSystemService(Context.CARRIER_CONFIG_SERVICE); if (configManager != null) { mCarrierConfig = configManager.getConfigForSubId(mPhone.getSubId()); } } /** * Check if the message is missed incoming call SMS, which is sent from the carrier to notify * the user about the missed incoming call earlier. * * @param pdus SMS pdu binary * @param format Either {@link SmsConstants#FORMAT_3GPP} or {@link SmsConstants#FORMAT_3GPP2} * @return {@code true} if this is an SMS for notifying the user about missed incoming call. */ public boolean filter(byte[][] pdus, String format) { // The missed incoming call SMS must be one page only, and if not we should ignore it. if (pdus.length != 1) { return false; } if (mCarrierConfig != null) { String[] originators = mCarrierConfig.getStringArray(CarrierConfigManager .KEY_MISSED_INCOMING_CALL_SMS_ORIGINATOR_STRING_ARRAY); if (originators != null) { SmsMessage message = SmsMessage.createFromPdu(pdus[0], format); if (message != null && !TextUtils.isEmpty(message.getOriginatingAddress()) && Arrays.asList(originators).contains(message.getOriginatingAddress())) { return processSms(message); } } } return false; } /** * Get the Epoch time. * * @param year Year in string format. If this param is null or empty, a guessed year will be * used. Some carriers do not provide this information in the SMS. * @param month Month in string format. * @param day Day in string format. * @param hour Hour in string format. * @param minute Minute in string format. * @return The Epoch time in milliseconds. */ private long getEpochTime(String year, String month, String day, String hour, String minute) { LocalDateTime now = LocalDateTime.now(); if (TextUtils.isEmpty(year)) { // If year is not provided, guess the year from current time. year = Integer.toString(now.getYear()); } LocalDateTime time; // Check if the guessed year is reasonable. If it's the future, then the year must be // the previous year. For example, the missed call's month and day is 12/31, but current // date is 1/1/2020, then the year of missed call must be 2019. do { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmm"); time = LocalDateTime.parse(year + month + day + hour + minute, formatter); year = Integer.toString(Integer.parseInt(year) - 1); } while (time.isAfter(now)); Instant instant = time.atZone(ZoneId.systemDefault()).toInstant(); return instant.toEpochMilli(); } /** * Process the SMS message * * @param message SMS message * * @return {@code true} if the SMS message has been processed as a missed incoming call SMS. */ private boolean processSms(@NonNull SmsMessage message) { String[] smsPatterns = mCarrierConfig.getStringArray(CarrierConfigManager .KEY_MISSED_INCOMING_CALL_SMS_PATTERN_STRING_ARRAY); if (smsPatterns == null || smsPatterns.length == 0) { Rlog.w(TAG, "Missed incoming call SMS pattern is not configured!"); return false; } boolean result = false; String[] missedCallMsgs = splitCalls(message.getMessageBody()); if (missedCallMsgs != null && missedCallMsgs.length > 0) { for (String parsedMsg : missedCallMsgs) { long missedCallTime = 0; String callerId = null; for (String smsPattern : smsPatterns) { Pattern pattern; try { pattern = Pattern.compile(smsPattern, Pattern.DOTALL | Pattern.UNIX_LINES); } catch (PatternSyntaxException e) { Rlog.w(TAG, "Configuration error. Unexpected missed incoming call sms " + "pattern: " + smsPattern + ", e=" + e); continue; } Matcher matcher = pattern.matcher(parsedMsg); String year = null, month = null, day = null, hour = null, minute = null; if (matcher.find()) { try { month = matcher.group(SMS_MONTH_TAG); day = matcher.group(SMS_DAY_TAG); hour = matcher.group(SMS_HOUR_TAG); minute = matcher.group(SMS_MINUTE_TAG); if (VDBG) { Rlog.v(TAG, "month=" + month + ", day=" + day + ", hour=" + hour + ", minute=" + minute); } } catch (IllegalArgumentException e) { if (VDBG) { Rlog.v(TAG, "One of the critical date field is missing. Using the " + "current time for missed incoming call."); } missedCallTime = System.currentTimeMillis(); } // Year is an optional field. try { year = matcher.group(SMS_YEAR_TAG); } catch (IllegalArgumentException e) { if (VDBG) Rlog.v(TAG, "Year is missing."); } try { if (missedCallTime == 0) { missedCallTime = getEpochTime(year, month, day, hour, minute); if (missedCallTime == 0) { Rlog.e(TAG, "Can't get the time. Use the current time."); missedCallTime = System.currentTimeMillis(); } } if (VDBG) Rlog.v(TAG, "missedCallTime=" + missedCallTime); } catch (Exception e) { Rlog.e(TAG, "Can't get the time for missed incoming call"); } try { callerId = matcher.group(SMS_CALLER_ID_TAG); if (VDBG) Rlog.v(TAG, "caller id=" + callerId); } catch (IllegalArgumentException e) { Rlog.d(TAG, "Caller id is not provided or can't be parsed."); } createMissedIncomingCallEvent(missedCallTime, callerId); result = true; break; } } } } if (!result) { Rlog.d(TAG, "SMS did not match any missed incoming call SMS pattern."); } return result; } private String[] splitCalls(String messageBody) { String[] messages = null; if (messageBody != null) { messages = messageBody.split("(\\n|\\s\\n)" + "(\\n|\\s\\n)"); Rlog.d(TAG, "splitTheMultipleCalls no of calls = " + ((messages != null) ? messages.length : 0)); } return messages; } /** * Create the missed incoming call through TelecomManager. * * @param missedCallTime the time of missed incoming call in. This is the EPOCH time in * milliseconds. * @param callerId The caller id of the missed incoming call. */ private void createMissedIncomingCallEvent(long missedCallTime, @Nullable String callerId) { TelecomManager tm = (TelecomManager) mPhone.getContext() .getSystemService(Context.TELECOM_SERVICE); if (tm != null) { Bundle bundle = new Bundle(); if (callerId != null) { final Uri phoneUri = Uri.fromParts( PhoneAccount.SCHEME_TEL, callerId, null); bundle.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, phoneUri); } // Need to use the Epoch time instead of the elapsed time because it's possible // the missed incoming call occurred before the phone boots up. bundle.putLong(TelecomManager.EXTRA_CALL_CREATED_EPOCH_TIME_MILLIS, missedCallTime); tm.addNewIncomingCall(makePstnPhoneAccountHandle(mPhone), bundle); } } // Create phone account. The logic is copied from PhoneUtils.makePstnPhoneAccountHandle. private PhoneAccountHandle makePstnPhoneAccountHandle(Phone phone) { SubscriptionManager subscriptionManager = (SubscriptionManager) phone.getContext().getSystemService( Context.TELEPHONY_SUBSCRIPTION_SERVICE); UserHandle userHandle = subscriptionManager.getSubscriptionUserHandle(phone.getSubId()); if (userHandle != null) { return new PhoneAccountHandle(PSTN_CONNECTION_SERVICE_COMPONENT, String.valueOf(phone.getSubId()), userHandle); } return new PhoneAccountHandle(PSTN_CONNECTION_SERVICE_COMPONENT, String.valueOf(phone.getSubId())); } }