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());
+    }
+}