Added missed incoming call SMS support
Provided a way for carrier to notify their users
about missed incoming call via special SMS.
Bug: 144068181
Test: MissedIncomingCallSmsFilterTest
Merged-In: I583be424e84c7c21bd59a6a0ef91e389c5b338d6
Change-Id: I583be424e84c7c21bd59a6a0ef91e389c5b338d6
(cherry picked from commit c87247f088f718e0ac4bc833312569db3f222a14)
diff --git a/src/java/com/android/internal/telephony/InboundSmsHandler.java b/src/java/com/android/internal/telephony/InboundSmsHandler.java
index 503b06f..c39ea98 100644
--- a/src/java/com/android/internal/telephony/InboundSmsHandler.java
+++ b/src/java/com/android/internal/telephony/InboundSmsHandler.java
@@ -1128,6 +1128,14 @@
return true;
}
+ MissedIncomingCallSmsFilter missedIncomingCallSmsFilter =
+ new MissedIncomingCallSmsFilter(mPhone);
+ if (missedIncomingCallSmsFilter.filter(pdus, tracker.getFormat())) {
+ log("Missed incoming call SMS received.");
+ dropSms(resultReceiver);
+ return true;
+ }
+
return false;
}
diff --git a/src/java/com/android/internal/telephony/MissedIncomingCallSmsFilter.java b/src/java/com/android/internal/telephony/MissedIncomingCallSmsFilter.java
new file mode 100644
index 0000000..428ccd7
--- /dev/null
+++ b/src/java/com/android/internal/telephony/MissedIncomingCallSmsFilter.java
@@ -0,0 +1,259 @@
+/*
+ * 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.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) {
+ SmsMessage message = SmsMessage.createFromPdu(pdus[0], format);
+ String[] originators = mCarrierConfig.getStringArray(CarrierConfigManager
+ .KEY_MISSED_INCOMING_CALL_SMS_ORIGINATOR_STRING_ARRAY);
+ if (originators != null
+ && 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(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);
+ }
+ }
+}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/MissedIncomingCallSmsFilterTest.java b/tests/telephonytests/src/com/android/internal/telephony/MissedIncomingCallSmsFilterTest.java
new file mode 100644
index 0000000..d97f7ab
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/MissedIncomingCallSmsFilterTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.telecom.TelecomManager;
+import android.telephony.CarrierConfigManager;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.internal.telephony.uicc.IccUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Unit test for {@link MissedIncomingCallSmsFilter}
+ */
+public class MissedIncomingCallSmsFilterTest extends TelephonyTest {
+
+ private static final String FAKE_CARRIER_SMS_ORIGINATOR = "+18584121234";
+
+ private static final String FAKE_CALLER_ID = "6501234567";
+
+ private MissedIncomingCallSmsFilter mFilterUT;
+
+ private PersistableBundle mBundle;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp(MissedIncomingCallSmsFilterTest.class.getSimpleName());
+
+ mBundle = mContextFixture.getCarrierConfigBundle();
+
+ mFilterUT = new MissedIncomingCallSmsFilter(mPhone);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testMissedIncomingCallwithCallerId() {
+ mBundle.putStringArray(
+ CarrierConfigManager.KEY_MISSED_INCOMING_CALL_SMS_ORIGINATOR_STRING_ARRAY,
+ new String[]{FAKE_CARRIER_SMS_ORIGINATOR});
+ mBundle.putStringArray(
+ CarrierConfigManager.KEY_MISSED_INCOMING_CALL_SMS_PATTERN_STRING_ARRAY,
+ new String[]{"^(?<month>0[1-9]|1[012])\\/(?<day>0[1-9]|1[0-9]|2[0-9]|3[0-1]) "
+ + "(?<hour>[0-1][0-9]|2[0-3]):(?<minute>[0-5][0-9])\\s*(?<callerId>[0-9]+)"
+ + "\\s*$"});
+
+ String smsPduString = "07919107739667F9040B918185141232F400000210413141114A17B0D82B4603C170"
+ + "BA580DA4B0D56031D98C56B3DD1A";
+ byte[][] pdus = {IccUtils.hexStringToBytes(smsPduString)};
+ assertTrue(mFilterUT.filter(pdus, SmsConstants.FORMAT_3GPP));
+
+ TelecomManager telecomManager = (TelecomManager) mContext.getSystemService(
+ Context.TELECOM_SERVICE);
+
+ ArgumentCaptor<Bundle> captor = ArgumentCaptor.forClass(Bundle.class);
+ verify(telecomManager).addNewIncomingCall(any(), captor.capture());
+
+ Bundle bundle = captor.getValue();
+ Uri uri = bundle.getParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS);
+
+ assertEquals(FAKE_CALLER_ID, uri.getSchemeSpecificPart());
+ }
+}