blob: 5932f9ed0caa4135afbabbd70d210c6a8e72ec48 [file] [log] [blame]
/*
* 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.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telephony.CarrierConfigManager;
import android.telephony.Rlog;
import android.telephony.SmsMessage;
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) {
long missedCallTime = 0;
String callerId = null;
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;
}
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(message.getMessageBody());
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);
return true;
}
}
Rlog.d(TAG, "SMS did not match any missed incoming call SMS pattern.");
return false;
}
// Create phone account. The logic is copied from PhoneUtils.makePstnPhoneAccountHandle.
private static PhoneAccountHandle makePstnPhoneAccountHandle(Phone phone) {
return new PhoneAccountHandle(PSTN_CONNECTION_SERVICE_COMPONENT,
String.valueOf(phone.getFullIccSerialNumber()));
}
/**
* 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);
}
}
}