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

Commit fe152e76 authored by Jack Yu's avatar Jack Yu Committed by Android (Google) Code Review
Browse files

Merge "Added missed incoming call SMS support" into rvc-dev

parents aad704bd ce39aaea
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());
    }
}