Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ce39aaea authored by Jack Yu's avatar Jack Yu
Browse files

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 c87247f0)
parent 5ef73c8d
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -1128,6 +1128,14 @@ public abstract class InboundSmsHandler extends StateMachine {
            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;
    }

+259 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
+94 −0
Original line number Diff line number Diff line
/*
 * 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());
    }
}