/*
 * Copyright (C) 2013 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 android.os.PowerWhitelistManager.REASON_EVENT_MMS;
import static android.os.PowerWhitelistManager.REASON_EVENT_SMS;
import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
import static android.provider.Telephony.Sms.Intents.RESULT_SMS_DATABASE_ERROR;
import static android.provider.Telephony.Sms.Intents.RESULT_SMS_DISPATCH_FAILURE;
import static android.provider.Telephony.Sms.Intents.RESULT_SMS_INVALID_URI;
import static android.provider.Telephony.Sms.Intents.RESULT_SMS_NULL_MESSAGE;
import static android.provider.Telephony.Sms.Intents.RESULT_SMS_NULL_PDU;
import static android.service.carrier.CarrierMessagingService.RECEIVE_OPTIONS_SKIP_NOTIFY_WHEN_CREDENTIAL_PROTECTED_STORAGE_UNAVAILABLE;
import static android.telephony.TelephonyManager.PHONE_TYPE_CDMA;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AppOpsManager;
import android.app.BroadcastOptions;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.SQLException;
import android.net.Uri;
import android.os.AsyncResult;
import android.os.Build;
import android.os.Bundle;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.os.PowerWhitelistManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.StorageManager;
import android.provider.Telephony;
import android.provider.Telephony.Sms.Intents;
import android.service.carrier.CarrierMessagingService;
import android.telephony.SmsMessage;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.LocalLog;
import android.util.Pair;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.SmsConstants.MessageClass;
import com.android.internal.telephony.analytics.TelephonyAnalytics;
import com.android.internal.telephony.analytics.TelephonyAnalytics.SmsMmsAnalytics;
import com.android.internal.telephony.flags.FeatureFlags;
import com.android.internal.telephony.flags.Flags;
import com.android.internal.telephony.metrics.TelephonyMetrics;
import com.android.internal.telephony.satellite.SatelliteController;
import com.android.internal.telephony.satellite.metrics.CarrierRoamingSatelliteSessionStats;
import com.android.internal.telephony.util.NotificationChannelController;
import com.android.internal.telephony.util.TelephonyUtils;
import com.android.internal.util.HexDump;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.telephony.Rlog;

import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

/**
 * This class broadcasts incoming SMS messages to interested apps after storing them in the
 * SmsProvider "raw" table and ACKing them to the SMSC. After each message has been broadcast, its
 * parts are removed from the raw table. If the device crashes after ACKing but before the broadcast
 * completes, the pending messages will be rebroadcast on the next boot.
 *
 * <p>The state machine starts in {@link IdleState} state. When we receive a new SMS from the radio,
 * the wakelock is acquired, then transition to {@link DeliveringState} state, where the message is
 * saved to the raw table, then acknowledged to the modem which in turn acknowledges it to the SMSC.
 *
 * <p>After saving the SMS, if the message is complete (either single-part or the final segment of a
 * multi-part SMS), we broadcast the completed PDUs as an ordered broadcast, then transition to
 * {@link WaitingState} state to wait for the broadcast to complete. When the local
 * {@link BroadcastReceiver} is called with the result, it sends {@link #EVENT_BROADCAST_COMPLETE}
 * to the state machine, causing us to either broadcast the next pending message (if one has arrived
 * while waiting for the broadcast to complete), or to transition back to the halted state after all
 * messages are processed. Then the wakelock is released and we wait for the next SMS.
 */
public abstract class InboundSmsHandler extends StateMachine {
    protected static final boolean DBG = true;
    protected static final boolean VDBG = false; // STOPSHIP if true, logs user data

    public static final int PDU_COLUMN = 0;
    public static final int SEQUENCE_COLUMN = 1;
    public static final int DESTINATION_PORT_COLUMN = 2;
    public static final int DATE_COLUMN = 3;
    public static final int REFERENCE_NUMBER_COLUMN = 4;
    public static final int COUNT_COLUMN = 5;
    public static final int ADDRESS_COLUMN = 6;
    public static final int ID_COLUMN = 7;
    public static final int MESSAGE_BODY_COLUMN = 8;
    public static final int DISPLAY_ADDRESS_COLUMN = 9;
    public static final int DELETED_FLAG_COLUMN = 10;
    public static final int SUBID_COLUMN = 11;

    /** Query projection for checking for duplicate message segments. */
    private static final String[] PDU_DELETED_FLAG_PROJECTION = {
            "pdu",
            "deleted"
    };

    /** Mapping from DB COLUMN to PDU_SEQUENCE_PORT PROJECTION index */
    private static final Map<Integer, Integer> PDU_DELETED_FLAG_PROJECTION_INDEX_MAPPING = Map.of(
            PDU_COLUMN, 0,
            DELETED_FLAG_COLUMN, 1);

    /** Query projection for combining concatenated message segments. */
    private static final String[] PDU_SEQUENCE_PORT_PROJECTION = {
            "pdu",
            "sequence",
            "destination_port",
            "display_originating_addr",
            "date"
    };

    /** Mapping from DB COLUMN to PDU_SEQUENCE_PORT PROJECTION index */
    private static final Map<Integer, Integer> PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING = Map.of(
            PDU_COLUMN, 0,
            SEQUENCE_COLUMN, 1,
            DESTINATION_PORT_COLUMN, 2,
            DISPLAY_ADDRESS_COLUMN, 3,
            DATE_COLUMN, 4);

    public static final String SELECT_BY_ID = "_id=?";

    /** New SMS received as an AsyncResult. */
    public static final int EVENT_NEW_SMS = 1;

    /** Message type containing a {@link InboundSmsTracker} ready to broadcast to listeners. */
    public static final int EVENT_BROADCAST_SMS = 2;

    /** Message from resultReceiver notifying {@link WaitingState} of a completed broadcast. */
    public static final int EVENT_BROADCAST_COMPLETE = 3;

    /** Sent on exit from {@link WaitingState} to return to idle after sending all broadcasts. */
    private static final int EVENT_RETURN_TO_IDLE = 4;

    /** Release wakelock after {@link #mWakeLockTimeout} when returning to idle state. */
    private static final int EVENT_RELEASE_WAKELOCK = 5;

    /** Sent by {@link SmsBroadcastUndelivered} after cleaning the raw table. */
    public static final int EVENT_START_ACCEPTING_SMS = 6;

    /** New SMS received as an AsyncResult. */
    public static final int EVENT_INJECT_SMS = 7;

    /** Update the sms tracker */
    public static final int EVENT_UPDATE_TRACKER = 8;

    /** BroadcastReceiver timed out waiting for an intent */
    public static final int EVENT_RECEIVER_TIMEOUT = 9;


    /** Wakelock release delay when returning to idle state. */
    private static final int WAKELOCK_TIMEOUT = 3000;

    /** Received SMS was not injected. */
    public static final int SOURCE_NOT_INJECTED = 0;

    /** Received SMS was received over IMS and injected. */
    public static final int SOURCE_INJECTED_FROM_IMS = 1;

    /** Received SMS was injected from source different than IMS. */
    public static final int SOURCE_INJECTED_FROM_UNKNOWN = 2;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = {"SOURCE_"},
            value = {
                SOURCE_NOT_INJECTED,
                SOURCE_INJECTED_FROM_IMS,
                SOURCE_INJECTED_FROM_UNKNOWN
    })
    public @interface SmsSource {}

    // The notitfication tag used when showing a notification. The combination of notification tag
    // and notification id should be unique within the phone app.
    @VisibleForTesting
    public static final String NOTIFICATION_TAG = "InboundSmsHandler";
    @VisibleForTesting
    public static final int NOTIFICATION_ID_NEW_MESSAGE = 1;

    /** URI for raw table of SMS provider. */
    protected static final Uri sRawUri = Uri.withAppendedPath(Telephony.Sms.CONTENT_URI, "raw");
    protected static final Uri sRawUriPermanentDelete =
            Uri.withAppendedPath(Telephony.Sms.CONTENT_URI, "raw/permanentDelete");

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    protected final Context mContext;
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private final ContentResolver mResolver;

    /** Special handler for WAP push messages. */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private final WapPushOverSms mWapPush;

    /** Wake lock to ensure device stays awake while dispatching the SMS intents. */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private final PowerManager.WakeLock mWakeLock;

    /** DefaultState throws an exception or logs an error for unhandled message types. */
    private final DefaultState mDefaultState = new DefaultState();

    /** Startup state. Waiting for {@link SmsBroadcastUndelivered} to complete. */
    private final StartupState mStartupState = new StartupState();

    /** Idle state. Waiting for messages to process. */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private final IdleState mIdleState = new IdleState();

    /** Delivering state. Saves the PDU in the raw table and acknowledges to SMSC. */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private final DeliveringState mDeliveringState = new DeliveringState();

    /** Broadcasting state. Waits for current broadcast to complete before delivering next. */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private final WaitingState mWaitingState = new WaitingState();

    /** Helper class to check whether storage is available for incoming messages. */
    protected SmsStorageMonitor mStorageMonitor;

    private final boolean mSmsReceiveDisabled;

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    protected Phone mPhone;

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private UserManager mUserManager;

    protected TelephonyMetrics mMetrics = TelephonyMetrics.getInstance();

    private LocalLog mLocalLog = new LocalLog(64);
    private LocalLog mCarrierServiceLocalLog = new LocalLog(8);

    PowerWhitelistManager mPowerWhitelistManager;

    protected CellBroadcastServiceManager mCellBroadcastServiceManager;

    // Delete permanently from raw table
    private final int DELETE_PERMANENTLY = 1;
    // Only mark deleted, but keep in db for message de-duping
    private final int MARK_DELETED = 2;

    private static String ACTION_OPEN_SMS_APP =
        "com.android.internal.telephony.OPEN_DEFAULT_SMS_APP";

    /** Timeout for releasing wakelock */
    private int mWakeLockTimeout;

    private List<SmsFilter> mSmsFilters;

    protected final @NonNull FeatureFlags mFeatureFlags;

    /**
     * Create a new SMS broadcast helper.
     * @param name the class name for logging
     * @param context the context of the phone app
     * @param storageMonitor the SmsStorageMonitor to check for storage availability
     */
    protected InboundSmsHandler(String name, Context context, SmsStorageMonitor storageMonitor,
            Phone phone, Looper looper, FeatureFlags featureFlags) {
        super(name, looper);

        mFeatureFlags = featureFlags;
        mContext = context;
        mStorageMonitor = storageMonitor;
        mPhone = phone;
        mResolver = context.getContentResolver();
        mWapPush = new WapPushOverSms(context, mFeatureFlags);

        TelephonyManager telephonyManager = TelephonyManager.from(mContext);
        boolean smsCapable = telephonyManager.isDeviceSmsCapable();
        mSmsReceiveDisabled = !telephonyManager.getSmsReceiveCapableForPhone(
                mPhone.getPhoneId(), smsCapable);

        PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
        mWakeLock.acquire();    // wake lock released after we enter idle state
        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
        mPowerWhitelistManager =
                (PowerWhitelistManager) mContext.getSystemService(Context.POWER_WHITELIST_MANAGER);
        mCellBroadcastServiceManager = new CellBroadcastServiceManager(context, phone);

        mSmsFilters = createDefaultSmsFilters();

        addState(mDefaultState);
        addState(mStartupState, mDefaultState);
        addState(mIdleState, mDefaultState);
        addState(mDeliveringState, mDefaultState);
            addState(mWaitingState, mDeliveringState);

        setInitialState(mStartupState);
        if (DBG) log("created InboundSmsHandler");
    }

    /**
     * Tell the state machine to quit after processing all messages.
     */
    public void dispose() {
        quit();
    }

    /**
     * Dispose of the WAP push object and release the wakelock.
     */
    @Override
    protected void onQuitting() {
        mWapPush.dispose();
        mCellBroadcastServiceManager.disable();

        while (mWakeLock.isHeld()) {
            mWakeLock.release();
        }
    }

    // CAF_MSIM Is this used anywhere ? if not remove it
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public Phone getPhone() {
        return mPhone;
    }

    @Override
    protected String getWhatToString(int what) {
        String whatString;
        switch (what) {
            case EVENT_NEW_SMS:
                whatString = "EVENT_NEW_SMS";
                break;
            case EVENT_BROADCAST_SMS:
                whatString = "EVENT_BROADCAST_SMS";
                break;
            case EVENT_BROADCAST_COMPLETE:
                whatString = "EVENT_BROADCAST_COMPLETE";
                break;
            case EVENT_RETURN_TO_IDLE:
                whatString = "EVENT_RETURN_TO_IDLE";
                break;
            case EVENT_RELEASE_WAKELOCK:
                whatString = "EVENT_RELEASE_WAKELOCK";
                break;
            case EVENT_START_ACCEPTING_SMS:
                whatString = "EVENT_START_ACCEPTING_SMS";
                break;
            case EVENT_INJECT_SMS:
                whatString = "EVENT_INJECT_SMS";
                break;
            case EVENT_UPDATE_TRACKER:
                whatString = "EVENT_UPDATE_TRACKER";
                break;
            case EVENT_RECEIVER_TIMEOUT:
                whatString = "EVENT_RECEIVER_TIMEOUT";
                break;
            default:
                whatString = "UNKNOWN EVENT " + what;
        }
        return whatString;
    }

    /**
     * This parent state throws an exception (for debug builds) or prints an error for unhandled
     * message types.
     */
    private class DefaultState extends State {
        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                default: {
                    String errorText = "processMessage: unhandled message type "
                            + getWhatToString(msg.what) + " currState="
                            + getCurrentState().getName();
                    if (TelephonyUtils.IS_DEBUGGABLE) {
                        loge("---- Dumping InboundSmsHandler ----");
                        loge("Total records=" + getLogRecCount());
                        for (int i = Math.max(getLogRecSize() - 20, 0); i < getLogRecSize(); i++) {
                            // getLogRec(i).toString() will call the overridden getWhatToString
                            // method which has more information
                            loge("Rec[%d]: %s\n" + i + getLogRec(i).toString());
                        }
                        loge("---- Dumped InboundSmsHandler ----");

                        throw new RuntimeException(errorText);
                    } else {
                        loge(errorText);
                    }
                    break;
                }
            }
            return HANDLED;
        }
    }

    /**
     * The Startup state waits for {@link SmsBroadcastUndelivered} to process the raw table and
     * notify the state machine to broadcast any complete PDUs that might not have been broadcast.
     */
    private class StartupState extends State {
        @Override
        public void enter() {
            if (DBG) log("StartupState.enter: entering StartupState");
            // Set wakelock timeout to 0 during startup, this will ensure that the wakelock is not
            // held if there are no pending messages to be handled.
            setWakeLockTimeout(0);
        }

        @Override
        public boolean processMessage(Message msg) {
            log("StartupState.processMessage: processing " + getWhatToString(msg.what));
            switch (msg.what) {
                case EVENT_NEW_SMS:
                case EVENT_INJECT_SMS:
                case EVENT_BROADCAST_SMS:
                    deferMessage(msg);
                    return HANDLED;

                case EVENT_START_ACCEPTING_SMS:
                    transitionTo(mIdleState);
                    return HANDLED;

                case EVENT_BROADCAST_COMPLETE:
                case EVENT_RETURN_TO_IDLE:
                case EVENT_RELEASE_WAKELOCK:
                default:
                    // let DefaultState handle these unexpected message types
                    return NOT_HANDLED;
            }
        }
    }

    /**
     * In the idle state the wakelock is released until a new SM arrives, then we transition
     * to Delivering mode to handle it, acquiring the wakelock on exit.
     */
    private class IdleState extends State {
        @Override
        public void enter() {
            if (DBG) log("IdleState.enter: entering IdleState");
            sendMessageDelayed(EVENT_RELEASE_WAKELOCK, getWakeLockTimeout());
        }

        @Override
        public void exit() {
            mWakeLock.acquire();
            if (DBG) log("IdleState.exit: acquired wakelock, leaving IdleState");
        }

        @Override
        public boolean processMessage(Message msg) {
            if (DBG) log("IdleState.processMessage: processing " + getWhatToString(msg.what));
            switch (msg.what) {
                case EVENT_NEW_SMS:
                case EVENT_INJECT_SMS:
                case EVENT_BROADCAST_SMS:
                    deferMessage(msg);
                    transitionTo(mDeliveringState);
                    return HANDLED;

                case EVENT_RELEASE_WAKELOCK:
                    mWakeLock.release();
                    if (DBG) {
                        if (mWakeLock.isHeld()) {
                            // this is okay as long as we call release() for every acquire()
                            log("IdleState.processMessage: EVENT_RELEASE_WAKELOCK: mWakeLock is "
                                    + "still held after release");
                        } else {
                            log("IdleState.processMessage: EVENT_RELEASE_WAKELOCK: mWakeLock "
                                    + "released");
                        }
                    }
                    return HANDLED;

                case EVENT_RETURN_TO_IDLE:
                    // already in idle state; ignore
                    return HANDLED;
                case EVENT_BROADCAST_COMPLETE:
                case EVENT_START_ACCEPTING_SMS:
                default:
                    // let DefaultState handle these unexpected message types
                    return NOT_HANDLED;
            }
        }
    }

    /**
     * In the delivering state, the inbound SMS is processed and stored in the raw table.
     * The message is acknowledged before we exit this state. If there is a message to broadcast,
     * transition to {@link WaitingState} state to send the ordered broadcast and wait for the
     * results. When all messages have been processed, the halting state will release the wakelock.
     */
    private class DeliveringState extends State {
        @Override
        public void enter() {
            if (DBG) log("DeliveringState.enter: entering DeliveringState");
        }

        @Override
        public void exit() {
            if (DBG) log("DeliveringState.exit: leaving DeliveringState");
        }

        @Override
        public boolean processMessage(Message msg) {
            if (DBG) log("DeliveringState.processMessage: processing " + getWhatToString(msg.what));
            switch (msg.what) {
                case EVENT_NEW_SMS:
                    // handle new SMS from RIL
                    handleNewSms((AsyncResult) msg.obj);
                    sendMessage(EVENT_RETURN_TO_IDLE);
                    return HANDLED;

                case EVENT_INJECT_SMS:
                    // handle new injected SMS
                    handleInjectSms((AsyncResult) msg.obj, msg.arg1 == 1 /* isOverIms */,
                            msg.arg2 /* token */);
                    sendMessage(EVENT_RETURN_TO_IDLE);
                    return HANDLED;

                case EVENT_BROADCAST_SMS:
                    // if any broadcasts were sent, transition to waiting state
                    InboundSmsTracker inboundSmsTracker = (InboundSmsTracker) msg.obj;
                    if (processMessagePart(inboundSmsTracker)) {
                        sendMessage(obtainMessage(EVENT_UPDATE_TRACKER, msg.obj));
                        transitionTo(mWaitingState);
                    } else {
                        // if event is sent from SmsBroadcastUndelivered.broadcastSms(), and
                        // processMessagePart() returns false, the state machine will be stuck in
                        // DeliveringState until next message is received. Send message to
                        // transition to idle to avoid that so that wakelock can be released
                        log("DeliveringState.processMessage: EVENT_BROADCAST_SMS: No broadcast "
                                + "sent. Return to IdleState");
                        sendMessage(EVENT_RETURN_TO_IDLE);
                    }
                    return HANDLED;

                case EVENT_RETURN_TO_IDLE:
                    // return to idle after processing all other messages
                    transitionTo(mIdleState);
                    return HANDLED;

                case EVENT_RELEASE_WAKELOCK:
                    mWakeLock.release();    // decrement wakelock from previous entry to Idle
                    if (!mWakeLock.isHeld()) {
                        // wakelock should still be held until 3 seconds after we enter Idle
                        loge("mWakeLock released while delivering/broadcasting!");
                    }
                    return HANDLED;

                case EVENT_UPDATE_TRACKER:
                    logd("process tracker message in DeliveringState " + msg.arg1);
                    return HANDLED;

                // we shouldn't get this message type in this state, log error and halt.
                case EVENT_BROADCAST_COMPLETE:
                case EVENT_START_ACCEPTING_SMS:
                default:
                    logeWithLocalLog("Unhandled msg in delivering state, msg.what = "
                            + getWhatToString(msg.what));
                    // let DefaultState handle these unexpected message types
                    return NOT_HANDLED;
            }
        }
    }

    /**
     * The waiting state delegates handling of new SMS to parent {@link DeliveringState}, but
     * defers handling of the {@link #EVENT_BROADCAST_SMS} phase until after the current
     * result receiver sends {@link #EVENT_BROADCAST_COMPLETE}. Before transitioning to
     * {@link DeliveringState}, {@link #EVENT_RETURN_TO_IDLE} is sent to transition to
     * {@link IdleState} after any deferred {@link #EVENT_BROADCAST_SMS} messages are handled.
     */
    private class WaitingState extends State {

        private InboundSmsTracker mLastDeliveredSmsTracker;

        @Override
        public void enter() {
            if (DBG) log("WaitingState.enter: entering WaitingState");
        }

        @Override
        public void exit() {
            if (DBG) log("WaitingState.exit: leaving WaitingState");
            // Before moving to idle state, set wakelock timeout to WAKE_LOCK_TIMEOUT milliseconds
            // to give any receivers time to take their own wake locks
            setWakeLockTimeout(WAKELOCK_TIMEOUT);
            mPhone.getIccSmsInterfaceManager().mDispatchersController.sendEmptyMessage(
                    SmsDispatchersController.EVENT_SMS_HANDLER_EXITING_WAITING_STATE);
        }

        @Override
        public boolean processMessage(Message msg) {
            if (DBG) log("WaitingState.processMessage: processing " + getWhatToString(msg.what));
            switch (msg.what) {
                case EVENT_BROADCAST_SMS:
                    // defer until the current broadcast completes
                    if (mLastDeliveredSmsTracker != null) {
                        String str = "Defer sms broadcast due to undelivered sms, "
                                + " messageCount = " + mLastDeliveredSmsTracker.getMessageCount()
                                + " destPort = " + mLastDeliveredSmsTracker.getDestPort()
                                + " timestamp = " + mLastDeliveredSmsTracker.getTimestamp()
                                + " currentTimestamp = " + System.currentTimeMillis();
                        logWithLocalLog(str, mLastDeliveredSmsTracker.getMessageId());
                    }
                    deferMessage(msg);
                    return HANDLED;

                case EVENT_RECEIVER_TIMEOUT:
                    logeWithLocalLog("WaitingState.processMessage: received "
                            + "EVENT_RECEIVER_TIMEOUT");
                    if (mLastDeliveredSmsTracker != null) {
                        mLastDeliveredSmsTracker.getSmsBroadcastReceiver(InboundSmsHandler.this)
                                .fakeNextAction();
                    }
                    return HANDLED;

                case EVENT_BROADCAST_COMPLETE:
                    mLastDeliveredSmsTracker = null;
                    // return to idle after handling all deferred messages
                    sendMessage(EVENT_RETURN_TO_IDLE);
                    transitionTo(mDeliveringState);
                    return HANDLED;

                case EVENT_RETURN_TO_IDLE:
                    // not ready to return to idle; ignore
                    return HANDLED;

                case EVENT_UPDATE_TRACKER:
                    mLastDeliveredSmsTracker = (InboundSmsTracker) msg.obj;
                    return HANDLED;

                default:
                    // parent state handles the other message types
                    return NOT_HANDLED;
            }
        }
    }
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private void handleNewSms(AsyncResult ar) {
        if (ar.exception != null) {
            loge("Exception processing incoming SMS: " + ar.exception);
            return;
        }

        int result;
        try {
            SmsMessage sms = (SmsMessage) ar.result;
            result = dispatchMessage(sms.mWrappedSmsMessage, SOURCE_NOT_INJECTED, 0 /*unused*/);
        } catch (RuntimeException ex) {
            loge("Exception dispatching message", ex);
            result = RESULT_SMS_DISPATCH_FAILURE;
        }

        if (mFeatureFlags.carrierRoamingNbIotNtn()) {
            if (result == Intents.RESULT_SMS_HANDLED) {
                SatelliteController satelliteController = SatelliteController.getInstance();
                if (satelliteController != null
                        && satelliteController.shouldSendSmsToDatagramDispatcher(mPhone)) {
                    satelliteController.onSmsReceived(mPhone.getSubId());
                }
            }
        }

        // RESULT_OK means that the SMS will be acknowledged by special handling,
        // e.g. for SMS-PP data download. Any other result, we should ack here.
        if (result != Activity.RESULT_OK) {
            boolean handled = (result == Intents.RESULT_SMS_HANDLED);
            notifyAndAcknowledgeLastIncomingSms(handled, result, null);
        }
    }

    /**
     * This method is called when a new SMS PDU is injected into application framework.
     * @param ar is the AsyncResult that has the SMS PDU to be injected.
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private void handleInjectSms(AsyncResult ar, boolean isOverIms, int token) {
        int result;
        SmsDispatchersController.SmsInjectionCallback callback = null;
        try {
            callback = (SmsDispatchersController.SmsInjectionCallback) ar.userObj;
            SmsMessage sms = (SmsMessage) ar.result;
            if (sms == null) {
                loge("Null injected sms");
                result = RESULT_SMS_NULL_PDU;
            } else {
                @SmsSource int smsSource =
                        isOverIms ? SOURCE_INJECTED_FROM_IMS : SOURCE_INJECTED_FROM_UNKNOWN;
                result = dispatchMessage(sms.mWrappedSmsMessage, smsSource, token);
            }
        } catch (RuntimeException ex) {
            loge("Exception dispatching message", ex);
            result = RESULT_SMS_DISPATCH_FAILURE;
        }

        if (callback != null) {
            callback.onSmsInjectedResult(result);
        }
    }

    /**
     * Process an SMS message from the RIL, calling subclass methods to handle 3GPP and
     * 3GPP2-specific message types.
     *
     * @param smsb the SmsMessageBase object from the RIL
     * @param smsSource the source of the SMS message
     * @return a result code from {@link android.provider.Telephony.Sms.Intents},
     *  or {@link Activity#RESULT_OK} for delayed acknowledgment to SMSC
     */
    private int dispatchMessage(SmsMessageBase smsb, @SmsSource int smsSource, int token) {
        // If sms is null, there was a parsing error.
        if (smsb == null) {
            loge("dispatchSmsMessage: message is null");
            return RESULT_SMS_NULL_MESSAGE;
        }

        if (mSmsReceiveDisabled) {
            // Device doesn't support receiving SMS,
            log("Received short message on device which doesn't support "
                    + "receiving SMS. Ignored.");
            return Intents.RESULT_SMS_HANDLED;
        }

        if (isMtSmsPollingMessage(smsb)) {
            log("Received MT SMS polling message. Ignored.");
            return Intents.RESULT_SMS_HANDLED;
        }

        if (mFeatureFlags.carrierRoamingNbIotNtn()) {
            SatelliteController satelliteController = SatelliteController.getInstance();
            if (satelliteController != null
                    && satelliteController.shouldDropSms(mPhone)) {
                log("SMS not supported during satellite session.");
                return Intents.RESULT_SMS_HANDLED;
            }
        }

        int result = dispatchMessageRadioSpecific(smsb, smsSource, token);

        // In case of error, add to metrics. This is not required in case of success, as the
        // data will be tracked when the message is processed (processMessagePart).
        if (result != Intents.RESULT_SMS_HANDLED && result != Activity.RESULT_OK) {
            mMetrics.writeIncomingSmsError(mPhone.getPhoneId(), is3gpp2(), smsSource, result);
            mPhone.getSmsStats().onIncomingSmsError(is3gpp2(), smsSource, result,
                    isEmergencyNumber(smsb.getOriginatingAddress()));
            if (mPhone != null) {
                TelephonyAnalytics telephonyAnalytics = mPhone.getTelephonyAnalytics();
                if (telephonyAnalytics != null) {
                    SmsMmsAnalytics smsMmsAnalytics = telephonyAnalytics.getSmsMmsAnalytics();
                    if (smsMmsAnalytics != null) {
                        smsMmsAnalytics.onIncomingSmsError(smsSource, result);
                    }
                }
            }

        }
        return result;
    }

    /**
     * Process voicemail notification, SMS-PP data download, CDMA CMAS, CDMA WAP push, and other
     * 3GPP/3GPP2-specific messages. Regular SMS messages are handled by calling the shared
     * {@link #dispatchNormalMessage} from this class.
     *
     * @param smsb the SmsMessageBase object from the RIL
     * @param smsSource the source of the SMS message
     * @return a result code from {@link android.provider.Telephony.Sms.Intents},
     *  or {@link Activity#RESULT_OK} for delayed acknowledgment to SMSC
     */
    protected abstract int dispatchMessageRadioSpecific(SmsMessageBase smsb,
            @SmsSource int smsSource, int token);

    /**
     * Send an acknowledge message to the SMSC.
     * @param success indicates that last message was successfully received.
     * @param result result code indicating any error
     * @param response callback message sent when operation completes.
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    protected abstract void acknowledgeLastIncomingSms(boolean success,
            int result, Message response);

    /**
     * Notify interested apps if the framework has rejected an incoming SMS,
     * and send an acknowledge message to the network.
     * @param success indicates that last message was successfully received.
     * @param result result code indicating any error
     * @param response callback message sent when operation completes.
     */
    private void notifyAndAcknowledgeLastIncomingSms(boolean success,
            int result, Message response) {
        if (!success) {
            // broadcast SMS_REJECTED_ACTION intent
            Intent intent = new Intent(Intents.SMS_REJECTED_ACTION);
            intent.putExtra("result", result);
            intent.putExtra("subId", mPhone.getSubId());
            if (mFeatureFlags.hsumBroadcast()) {
                mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
                        android.Manifest.permission.RECEIVE_SMS);
            } else {
                mContext.sendBroadcast(intent, android.Manifest.permission.RECEIVE_SMS);
            }
        }
        acknowledgeLastIncomingSms(success, result, response);
    }

    /**
     * Return true if this handler is for 3GPP2 messages; false for 3GPP format.
     * @return true for the 3GPP2 handler; false for the 3GPP handler
     */
    protected abstract boolean is3gpp2();

    /**
     * Dispatch a normal incoming SMS. This is called from {@link #dispatchMessageRadioSpecific}
     * if no format-specific handling was required. Saves the PDU to the SMS provider raw table,
     * creates an {@link InboundSmsTracker}, then sends it to the state machine as an
     * {@link #EVENT_BROADCAST_SMS}. Returns {@link Intents#RESULT_SMS_HANDLED} or an error value.
     *
     * @param sms the message to dispatch
     * @param smsSource the source of the SMS message
     * @return {@link Intents#RESULT_SMS_HANDLED} if the message was accepted, or an error status
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    protected int dispatchNormalMessage(SmsMessageBase sms, @SmsSource int smsSource) {
        SmsHeader smsHeader = sms.getUserDataHeader();
        InboundSmsTracker tracker;

        if ((smsHeader == null) || (smsHeader.concatRef == null)) {
            // Message is not concatenated.
            int destPort = -1;
            if (smsHeader != null && smsHeader.portAddrs != null) {
                // The message was sent to a port.
                destPort = smsHeader.portAddrs.destPort;
                if (DBG) log("destination port: " + destPort);
            }
            tracker = TelephonyComponentFactory.getInstance()
                    .inject(InboundSmsTracker.class.getName())
                    .makeInboundSmsTracker(mContext, sms.getPdu(),
                            sms.getTimestampMillis(), destPort, is3gpp2(), false,
                            sms.getOriginatingAddress(), sms.getDisplayOriginatingAddress(),
                            sms.getMessageBody(), sms.getMessageClass() == MessageClass.CLASS_0,
                            mPhone.getSubId(), smsSource);
        } else {
            // Create a tracker for this message segment.
            SmsHeader.ConcatRef concatRef = smsHeader.concatRef;
            SmsHeader.PortAddrs portAddrs = smsHeader.portAddrs;
            int destPort = (portAddrs != null ? portAddrs.destPort : -1);
            tracker = TelephonyComponentFactory.getInstance()
                    .inject(InboundSmsTracker.class.getName())
                    .makeInboundSmsTracker(mContext, sms.getPdu(),
                            sms.getTimestampMillis(), destPort, is3gpp2(),
                            sms.getOriginatingAddress(), sms.getDisplayOriginatingAddress(),
                            concatRef.refNumber, concatRef.seqNumber, concatRef.msgCount, false,
                            sms.getMessageBody(), sms.getMessageClass() == MessageClass.CLASS_0,
                            mPhone.getSubId(), smsSource);
        }

        if (VDBG) log("created tracker: " + tracker);

        // de-duping is done only for text messages
        // destPort = -1 indicates text messages, otherwise it's a data sms
        return addTrackerToRawTableAndSendMessage(tracker,
                tracker.getDestPort() == -1 /* de-dup if text message */);
    }

    /**
     * Helper to add the tracker to the raw table and then send a message to broadcast it, if
     * successful. Returns the SMS intent status to return to the SMSC.
     * @param tracker the tracker to save to the raw table and then deliver
     * @return {@link Intents#RESULT_SMS_HANDLED} or one of these errors:<br>
     * <code>RESULT_SMS_UNSUPPORTED</code><br>
     * <code>RESULT_SMS_DUPLICATED</code><br>
     * <code>RESULT_SMS_DISPATCH_FAILURE</code><br>
     * <code>RESULT_SMS_NULL_PDU</code><br>
     * <code>RESULT_SMS_NULL_MESSAGE</code><br>
     * <code>RESULT_SMS_DATABASE_ERROR</code><br>
     * <code>RESULT_SMS_INVALID_URI</code><br>
     */
    protected int addTrackerToRawTableAndSendMessage(InboundSmsTracker tracker, boolean deDup) {
        int result = addTrackerToRawTable(tracker, deDup);
        switch(result) {
            case Intents.RESULT_SMS_HANDLED:
                sendMessage(EVENT_BROADCAST_SMS, tracker);
                return Intents.RESULT_SMS_HANDLED;

            case Intents.RESULT_SMS_DUPLICATED:
                return Intents.RESULT_SMS_HANDLED;

            default:
                return result;
        }
    }

    /**
     * Process the inbound SMS segment. If the message is complete, send it as an ordered
     * broadcast to interested receivers and return true. If the message is a segment of an
     * incomplete multi-part SMS, return false.
     * @param tracker the tracker containing the message segment to process
     * @return true if an ordered broadcast was sent; false if waiting for more message segments
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private boolean processMessagePart(InboundSmsTracker tracker) {
        int messageCount = tracker.getMessageCount();
        byte[][] pdus;
        long[] timestamps;
        int destPort = tracker.getDestPort();
        boolean block = false;
        String address = tracker.getAddress();

        // Do not process when the message count is invalid.
        if (messageCount <= 0) {
            loge("processMessagePart: returning false due to invalid message count "
                    + messageCount, tracker.getMessageId());
            return false;
        }

        if (messageCount == 1) {
            // single-part message
            pdus = new byte[][]{tracker.getPdu()};
            timestamps = new long[]{tracker.getTimestamp()};
            block = BlockChecker.isBlocked(mContext, tracker.getDisplayAddress(), null);
        } else {
            // multi-part message
            Cursor cursor = null;
            try {
                // used by several query selection arguments
                String refNumber = Integer.toString(tracker.getReferenceNumber());
                String count = Integer.toString(tracker.getMessageCount());

                // query for all segments and broadcast message if we have all the parts
                String[] whereArgs = {address, refNumber, count};
                cursor = mResolver.query(sRawUri, PDU_SEQUENCE_PORT_PROJECTION,
                        tracker.getQueryForSegments(), whereArgs, null);

                int cursorCount = cursor.getCount();
                if (cursorCount < messageCount) {
                    // Wait for the other message parts to arrive. It's also possible for the last
                    // segment to arrive before processing the EVENT_BROADCAST_SMS for one of the
                    // earlier segments. In that case, the broadcast will be sent as soon as all
                    // segments are in the table, and any later EVENT_BROADCAST_SMS messages will
                    // get a row count of 0 and return.
                    log("processMessagePart: returning false. Only " + cursorCount + " of "
                            + messageCount + " segments " + " have arrived. refNumber: "
                            + refNumber, tracker.getMessageId());
                    return false;
                }

                // All the parts are in place, deal with them
                pdus = new byte[messageCount][];
                timestamps = new long[messageCount];
                while (cursor.moveToNext()) {
                    // subtract offset to convert sequence to 0-based array index
                    int index = cursor.getInt(PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING
                            .get(SEQUENCE_COLUMN)) - tracker.getIndexOffset();

                    // The invalid PDUs can be received and stored in the raw table. The range
                    // check ensures the process not crash even if the seqNumber in the
                    // UserDataHeader is invalid.
                    if (index >= pdus.length || index < 0) {
                        loge(String.format(
                                "processMessagePart: invalid seqNumber = %d, messageCount = %d",
                                index + tracker.getIndexOffset(),
                                messageCount),
                                tracker.getMessageId());
                        continue;
                    }

                    pdus[index] = HexDump.hexStringToByteArray(cursor.getString(
                            PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING.get(PDU_COLUMN)));

                    // Read the destination port from the first segment (needed for CDMA WAP PDU).
                    // It's not a bad idea to prefer the port from the first segment in other cases.
                    if (index == 0 && !cursor.isNull(PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING
                            .get(DESTINATION_PORT_COLUMN))) {
                        int port = cursor.getInt(PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING
                                .get(DESTINATION_PORT_COLUMN));
                        // strip format flags and convert to real port number, or -1
                        port = InboundSmsTracker.getRealDestPort(port);
                        if (port != -1) {
                            destPort = port;
                        }
                    }

                    timestamps[index] = cursor.getLong(
                            PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING.get(DATE_COLUMN));

                    // check if display address should be blocked or not
                    if (!block) {
                        // Depending on the nature of the gateway, the display origination address
                        // is either derived from the content of the SMS TP-OA field, or the TP-OA
                        // field contains a generic gateway address and the from address is added
                        // at the beginning in the message body. In that case only the first SMS
                        // (part of Multi-SMS) comes with the display originating address which
                        // could be used for block checking purpose.
                        block = BlockChecker.isBlocked(mContext,
                                cursor.getString(PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING
                                        .get(DISPLAY_ADDRESS_COLUMN)), null);
                    }
                }
                log("processMessagePart: all " + messageCount + " segments "
                        + " received. refNumber: " + refNumber, tracker.getMessageId());
            } catch (SQLException e) {
                loge("processMessagePart: Can't access multipart SMS database, "
                        + SmsController.formatCrossStackMessageId(tracker.getMessageId()), e);
                return false;
            } finally {
                if (cursor != null) {
                    cursor.close();
                }
            }
        }

        final boolean isWapPush = (destPort == SmsHeader.PORT_WAP_PUSH);
        String format = tracker.getFormat();

        // Do not process null pdu(s). Check for that and return false in that case.
        List<byte[]> pduList = Arrays.asList(pdus);
        if (pduList.size() == 0 || pduList.contains(null)) {
            String errorMsg = "processMessagePart: returning false due to "
                    + (pduList.size() == 0 ? "pduList.size() == 0" : "pduList.contains(null)");
            logeWithLocalLog(errorMsg, tracker.getMessageId());
            mPhone.getSmsStats().onIncomingSmsError(
                    is3gpp2(), tracker.getSource(), RESULT_SMS_NULL_PDU,
                    isEmergencyNumber(tracker.getAddress()));
            if (mPhone != null) {
                TelephonyAnalytics telephonyAnalytics = mPhone.getTelephonyAnalytics();
                if (telephonyAnalytics != null) {
                    SmsMmsAnalytics smsMmsAnalytics = telephonyAnalytics.getSmsMmsAnalytics();
                    if (smsMmsAnalytics != null) {
                        smsMmsAnalytics.onIncomingSmsError(
                                tracker.getSource(), RESULT_SMS_NULL_PDU);
                    }
                }
            }
            return false;
        }

        ByteArrayOutputStream output = new ByteArrayOutputStream();
        if (isWapPush) {
            for (byte[] pdu : pdus) {
                // 3GPP needs to extract the User Data from the PDU; 3GPP2 has already done this
                if (format == SmsConstants.FORMAT_3GPP) {
                    SmsMessage msg = SmsMessage.createFromPdu(pdu, SmsConstants.FORMAT_3GPP);
                    if (msg != null) {
                        pdu = msg.getUserData();
                    } else {
                        loge("processMessagePart: SmsMessage.createFromPdu returned null",
                                tracker.getMessageId());
                        mMetrics.writeIncomingWapPush(mPhone.getPhoneId(), tracker.getSource(),
                                SmsConstants.FORMAT_3GPP, timestamps, false,
                                tracker.getMessageId());
                        mPhone.getSmsStats().onIncomingSmsWapPush(tracker.getSource(),
                                messageCount, RESULT_SMS_NULL_MESSAGE, tracker.getMessageId(),
                                isEmergencyNumber(tracker.getAddress()));
                        return false;
                    }
                }
                output.write(pdu, 0, pdu.length);
            }
        }

        SmsBroadcastReceiver resultReceiver = tracker.getSmsBroadcastReceiver(this);

        if (!isMainUserUnlocked()) {
            log("processMessagePart: !isUserUnlocked; calling processMessagePartWithUserLocked. "
                    + "Port: " + destPort, tracker.getMessageId());
            return processMessagePartWithUserLocked(
                    tracker,
                    (isWapPush ? new byte[][] {output.toByteArray()} : pdus),
                    destPort,
                    resultReceiver,
                    block);
        }

        if (isWapPush) {
            int result = mWapPush.dispatchWapPdu(output.toByteArray(), resultReceiver,
                    this, address, tracker.getSubId(), tracker.getMessageId());
            if (DBG) {
                log("processMessagePart: dispatchWapPdu() returned " + result,
                        tracker.getMessageId());
            }
            // Add result of WAP-PUSH into metrics. RESULT_SMS_HANDLED indicates that the WAP-PUSH
            // needs to be ignored, so treating it as a success case.
            boolean wapPushResult =
                    result == Activity.RESULT_OK || result == Intents.RESULT_SMS_HANDLED;
            mMetrics.writeIncomingWapPush(mPhone.getPhoneId(), tracker.getSource(),
                    format, timestamps, wapPushResult, tracker.getMessageId());
            mPhone.getSmsStats().onIncomingSmsWapPush(tracker.getSource(), messageCount,
                    result, tracker.getMessageId(), isEmergencyNumber(tracker.getAddress()));
            // result is Activity.RESULT_OK if an ordered broadcast was sent
            if (result == Activity.RESULT_OK) {
                return true;
            } else {
                deleteFromRawTable(tracker.getDeleteWhere(), tracker.getDeleteWhereArgs(),
                        MARK_DELETED);
                loge("processMessagePart: returning false as the ordered broadcast for WAP push "
                        + "was not sent", tracker.getMessageId());
                return false;
            }
        }

        // All parts of SMS are received. Update metrics for incoming SMS.
        // The metrics are generated before SMS filters are invoked.
        // For messages composed by multiple parts, the metrics are generated considering the
        // characteristics of the last one.
        mMetrics.writeIncomingSmsSession(mPhone.getPhoneId(), tracker.getSource(),
                format, timestamps, block, tracker.getMessageId());
        mPhone.getSmsStats().onIncomingSmsSuccess(is3gpp2(), tracker.getSource(),
                messageCount, block, tracker.getMessageId(),
                isEmergencyNumber(tracker.getAddress()));
        CarrierRoamingSatelliteSessionStats sessionStats =
                CarrierRoamingSatelliteSessionStats.getInstance(mPhone.getSubId());
        sessionStats.onIncomingSms(mPhone.getSubId());
        if (mPhone != null) {
            TelephonyAnalytics telephonyAnalytics = mPhone.getTelephonyAnalytics();
            if (telephonyAnalytics != null) {
                SmsMmsAnalytics smsMmsAnalytics = telephonyAnalytics.getSmsMmsAnalytics();
                if (smsMmsAnalytics != null) {
                    smsMmsAnalytics.onIncomingSmsSuccess(tracker.getSource());
                }
            }

        }
        // Always invoke SMS filters, even if the number ends up being blocked, to prevent
        // surprising bugs due to blocking numbers that happen to be used for visual voicemail SMS
        // or other carrier system messages.
        boolean filterInvoked = filterSms(
                pdus, destPort, tracker, resultReceiver, true /* userUnlocked */, block);

        if (!filterInvoked) {
            // Block now if the filter wasn't invoked. Otherwise, it will be the responsibility of
            // the filter to delete the SMS once processing completes.
            if (block) {
                deleteFromRawTable(tracker.getDeleteWhere(), tracker.getDeleteWhereArgs(),
                        DELETE_PERMANENTLY);
                log("processMessagePart: returning false as the phone number is blocked",
                        tracker.getMessageId());
                return false;
            }

            dispatchSmsDeliveryIntent(pdus, format, destPort, resultReceiver,
                    tracker.isClass0(), tracker.getSubId(), tracker.getMessageId());
        }

        return true;
    }

    private boolean isEmergencyNumber(String number) {
        if (!mPhone.hasCalling()) return false;
        TelephonyManager manager = TelephonyManager.from(mContext);
        if (manager == null) return false;
        return manager.isEmergencyNumber(number);
    }

    /**
     * Processes the message part while the credential-encrypted storage is still locked.
     *
     * <p>If the message is a regular MMS, show a new message notification. If the message is a
     * SMS, ask the carrier app to filter it and show the new message notification if the carrier
     * app asks to keep the message.
     *
     * @return true if an ordered broadcast was sent to the carrier app; false otherwise.
     */
    private boolean processMessagePartWithUserLocked(InboundSmsTracker tracker,
            byte[][] pdus, int destPort, SmsBroadcastReceiver resultReceiver, boolean block) {
        if (destPort == SmsHeader.PORT_WAP_PUSH && mWapPush.isWapPushForMms(pdus[0], this)) {
            showNewMessageNotification();
            return false;
        }
        if (destPort == -1) {
            // This is a regular SMS - hand it to the carrier or system app for filtering.
            boolean filterInvoked = filterSms(
                    pdus, destPort, tracker, resultReceiver, false /* userUnlocked */,
                    block);
            if (filterInvoked) {
                // filter invoked, wait for it to return the result.
                return true;
            } else if (!block) {
                // filter not invoked and message not blocked, show the notification and do nothing
                // further. Even if the message is blocked, we keep it in the database so it can be
                // reprocessed by filters once credential-encrypted storage is available.
                showNewMessageNotification();
            }
        }
        return false;
    }

    private boolean isMainUserUnlocked() {
        UserHandle mainUser = mFeatureFlags.smsMmsDeliverBroadcastsRedirectToMainUser() ?
                mUserManager.getMainUser() : null;
        if (mainUser != null) {
            return mUserManager.isUserUnlocked(mainUser);
        }
        return mUserManager.isUserUnlocked();
    }

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private void showNewMessageNotification() {
        // Do not show the notification on non-FBE devices.
        if (!StorageManager.isFileEncrypted()) {
            return;
        }
        log("Show new message notification.");
        PendingIntent intent = PendingIntent.getBroadcast(
            mContext,
            0,
            new Intent(ACTION_OPEN_SMS_APP),
                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
        Notification.Builder mBuilder = new Notification.Builder(mContext)
                .setSmallIcon(com.android.internal.R.drawable.sym_action_chat)
                .setAutoCancel(true)
                .setVisibility(Notification.VISIBILITY_PUBLIC)
                .setDefaults(Notification.DEFAULT_ALL)
                .setContentTitle(mContext.getString(R.string.new_sms_notification_title))
                .setContentText(mContext.getString(R.string.new_sms_notification_content))
                .setContentIntent(intent)
                .setChannelId(NotificationChannelController.CHANNEL_ID_SMS);
        NotificationManager mNotificationManager =
            (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
        UserHandle mainUser = mFeatureFlags.smsMmsDeliverBroadcastsRedirectToMainUser() ?
                mUserManager.getMainUser() : null;
        if (mainUser != null) {
            mNotificationManager.notifyAsUser(
                    NOTIFICATION_TAG, NOTIFICATION_ID_NEW_MESSAGE, mBuilder.build(), mainUser);
        } else {
            mNotificationManager.notify(
                    NOTIFICATION_TAG, NOTIFICATION_ID_NEW_MESSAGE, mBuilder.build());
        }
    }

    static void cancelNewMessageNotification(Context context) {
        NotificationManager mNotificationManager =
            (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationManager.cancel(InboundSmsHandler.NOTIFICATION_TAG,
            InboundSmsHandler.NOTIFICATION_ID_NEW_MESSAGE);
    }

    /**
     * Creates the default filters used to filter SMS messages.
     *
     * <p>Currently 3 filters exist: the carrier package, the VisualVoicemailSmsFilter, and the
     * missed incoming call SMS filter.
     *
     * <p>Since the carrier filter is asynchronous, if a message passes through the carrier filter,
     * the remaining filters will be applied in the callback.
     */
    private List<SmsFilter> createDefaultSmsFilters() {
        List<SmsFilter> smsFilters = new ArrayList<>(3);
        smsFilters.add(
                (pdus, destPort, tracker, resultReceiver, userUnlocked, block, remainingFilters)
                        -> {
                    CarrierServicesSmsFilterCallback filterCallback =
                            new CarrierServicesSmsFilterCallback(
                                    pdus, destPort, tracker, tracker.getFormat(), resultReceiver,
                                    userUnlocked,
                                    tracker.isClass0(), tracker.getSubId(), tracker.getMessageId(),
                                    block, remainingFilters);
                    CarrierServicesSmsFilter carrierServicesFilter = new CarrierServicesSmsFilter(
                            mContext, mPhone, pdus, destPort, tracker.getFormat(),
                            filterCallback, getName() + "::CarrierServicesSmsFilter",
                            mCarrierServiceLocalLog, tracker.getMessageId());
                    if (carrierServicesFilter.filter()) {
                        log("SMS is being handled by carrier service", tracker.getMessageId());
                        return true;
                    } else {
                        return false;
                    }
                });
        smsFilters.add(
                (pdus, destPort, tracker, resultReceiver, userUnlocked, block, remainingFilters)
                        -> {
                    if (VisualVoicemailSmsFilter.filter(
                            mContext, pdus, tracker.getFormat(), destPort, tracker.getSubId())) {
                        logWithLocalLog("Visual voicemail SMS dropped", tracker.getMessageId());
                        dropFilteredSms(tracker, resultReceiver, block);
                        return true;
                    }
                    return false;
                });
        smsFilters.add(
                (pdus, destPort, tracker, resultReceiver, userUnlocked, block, remainingFilters)
                        -> {
                    MissedIncomingCallSmsFilter missedIncomingCallSmsFilter =
                            new MissedIncomingCallSmsFilter(mPhone);
                    if (missedIncomingCallSmsFilter.filter(pdus, tracker.getFormat())) {
                        logWithLocalLog("Missed incoming call SMS received",
                                tracker.getMessageId());
                        dropFilteredSms(tracker, resultReceiver, block);
                        return true;
                    }
                    return false;
                });
        return smsFilters;
    }

    private void dropFilteredSms(
            InboundSmsTracker tracker, SmsBroadcastReceiver resultReceiver, boolean block) {
        if (block) {
            deleteFromRawTable(
                    tracker.getDeleteWhere(), tracker.getDeleteWhereArgs(),
                    DELETE_PERMANENTLY);
            sendMessage(EVENT_BROADCAST_COMPLETE);
        } else {
            dropSms(resultReceiver);
        }
    }

    /**
     * Filters the SMS.
     *
     * <p>Each filter in {@link #mSmsFilters} is invoked sequentially. If any filter returns true,
     * this method returns true and subsequent filters are ignored.
     *
     * @return true if a filter is invoked and the SMS processing flow is diverted, false otherwise.
     */
    private boolean filterSms(byte[][] pdus, int destPort,
            InboundSmsTracker tracker, SmsBroadcastReceiver resultReceiver, boolean userUnlocked,
            boolean block) {
        return filterSms(pdus, destPort, tracker, resultReceiver, userUnlocked, block, mSmsFilters);
    }

    private static boolean filterSms(byte[][] pdus, int destPort,
            InboundSmsTracker tracker, SmsBroadcastReceiver resultReceiver, boolean userUnlocked,
            boolean block, List<SmsFilter> filters) {
        ListIterator<SmsFilter> iterator = filters.listIterator();
        while (iterator.hasNext()) {
            SmsFilter smsFilter = iterator.next();
            if (smsFilter.filterSms(pdus, destPort, tracker, resultReceiver, userUnlocked, block,
                    filters.subList(iterator.nextIndex(), filters.size()))) {
                return true;
            }
        }
        return false;
    }

    /**
     * Dispatch the intent with the specified permission, appOp, and result receiver, using
     * this state machine's handler thread to run the result receiver.
     *
     * @param intent the intent to broadcast
     * @param permission receivers are required to have this permission
     * @param appOp app op that is being performed when dispatching to a receiver
     * @param user user to deliver the intent to
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    @SuppressLint("MissingPermission")
    public void dispatchIntent(Intent intent, String permission, String appOp,
            Bundle opts, SmsBroadcastReceiver resultReceiver, UserHandle user, int subId) {
        intent.addFlags(Intent.FLAG_RECEIVER_NO_ABORT);
        final String action = intent.getAction();
        if (Intents.SMS_DELIVER_ACTION.equals(action)
                || Intents.SMS_RECEIVED_ACTION.equals(action)
                || Intents.WAP_PUSH_DELIVER_ACTION.equals(action)
                || Intents.WAP_PUSH_RECEIVED_ACTION.equals(action)) {
            // Some intents need to be delivered with high priority:
            // SMS_DELIVER, SMS_RECEIVED, WAP_PUSH_DELIVER, WAP_PUSH_RECEIVED
            // In some situations, like after boot up or system under load, normal
            // intent delivery could take a long time.
            // This flag should only be set for intents for visible, timely operations
            // which is true for the intents above.
            intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        }
        SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhone.getPhoneId());

        // override the subId value in the intent with the values from tracker as they can be
        // different, specifically if the message is coming from SmsBroadcastUndelivered
        if (SubscriptionManager.isValidSubscriptionId(subId)) {
            SubscriptionManager.putSubscriptionIdExtra(intent, subId);
        }

        if (user.equals(UserHandle.ALL)) {
            // Get a list of currently started users.
            int[] users = null;
            final List<UserHandle> userHandles = mUserManager.getUserHandles(false);
            final UserHandle mainUser = mUserManager.getMainUser();
            final List<UserHandle> runningUserHandles = new ArrayList();
            for (UserHandle handle : userHandles) {
                if (mUserManager.isUserRunning(handle)) {
                    runningUserHandles.add(handle);
                } else {
                    if (mFeatureFlags.smsMmsDeliverBroadcastsRedirectToMainUser()
                            && handle.equals(mainUser)) {
                        logeWithLocalLog("dispatchIntent: MAIN user is not running",
                                resultReceiver.mInboundSmsTracker.getMessageId());
                    } else if (handle.equals(UserHandle.SYSTEM)) {
                        logeWithLocalLog("dispatchIntent: SYSTEM user is not running",
                                resultReceiver.mInboundSmsTracker.getMessageId());
                    }
                }
            }
            if (runningUserHandles.isEmpty()) {
                users = new int[] {user.getIdentifier()};
            } else {
                users = new int[runningUserHandles.size()];
                for (int i = 0; i < runningUserHandles.size(); i++) {
                    users[i] = runningUserHandles.get(i).getIdentifier();
                }
            }
            // Deliver the broadcast only to those running users that are permitted
            // by user policy.
            for (int i = users.length - 1; i >= 0; i--) {
                UserHandle targetUser = UserHandle.of(users[i]);
                if (!isMainUser(users[i])) {
                    // Is the user not allowed to use SMS?
                    if (hasUserRestriction(UserManager.DISALLOW_SMS, targetUser)) {
                        continue;
                    }
                    // Skip unknown users and managed profiles as well
                    if (mUserManager.isManagedProfile(users[i])) {
                        continue;
                    }
                }
                // Only pass in the resultReceiver when the MAIN user is processed.
                try {
                    if (isMainUser(users[i])) {
                        resultReceiver.setWaitingForIntent(intent);
                    }
                    mContext.createPackageContextAsUser(mContext.getPackageName(), 0, targetUser)
                            .sendOrderedBroadcast(intent, Activity.RESULT_OK, permission, appOp,
                                    isMainUser(users[i])
                                            ? resultReceiver : null, getHandler(),
                                    null /* initialData */, null /* initialExtras */, opts);
                } catch (PackageManager.NameNotFoundException ignored) {
                }
            }
        } else {
            try {
                resultReceiver.setWaitingForIntent(intent);
                mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user)
                        .sendOrderedBroadcast(intent, Activity.RESULT_OK, permission, appOp,
                                resultReceiver, getHandler(), null /* initialData */,
                                null /* initialExtras */, opts);
            } catch (PackageManager.NameNotFoundException ignored) {
            }
        }
    }

    private boolean hasUserRestriction(String restrictionKey, UserHandle userHandle) {
        final List<UserManager.EnforcingUser> sources = mUserManager
                .getUserRestrictionSources(restrictionKey, userHandle);
        return (sources != null && !sources.isEmpty());
    }

    @SuppressLint("MissingPermission")
    private  boolean isMainUser(int userId) {
        if (mFeatureFlags.smsMmsDeliverBroadcastsRedirectToMainUser()) {
            return userId == mUserManager.getMainUser().getIdentifier();
        } else {
            return userId == UserHandle.SYSTEM.getIdentifier();
        }
    }

    /**
     * Helper for {@link SmsBroadcastUndelivered} to delete an old message in the raw table.
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private void deleteFromRawTable(String deleteWhere, String[] deleteWhereArgs,
                                    int deleteType) {
        Uri uri = deleteType == DELETE_PERMANENTLY ? sRawUriPermanentDelete : sRawUri;
        int rows = mResolver.delete(uri, deleteWhere, deleteWhereArgs);
        if (rows == 0) {
            loge("No rows were deleted from raw table!");
        } else if (DBG) {
            log("Deleted " + rows + " rows from raw table.");
        }
    }

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private Bundle handleSmsWhitelisting(ComponentName target, boolean bgActivityStartAllowed) {
        String pkgName;
        String reason;
        if (target != null) {
            pkgName = target.getPackageName();
            reason = "sms-app";
        } else {
            pkgName = mContext.getPackageName();
            reason = "sms-broadcast";
        }
        BroadcastOptions bopts = null;
        Bundle bundle = null;
        if (bgActivityStartAllowed) {
            bopts = BroadcastOptions.makeBasic();
            bopts.setBackgroundActivityStartsAllowed(true);
            bundle = bopts.toBundle();
        }
        long duration = mPowerWhitelistManager.whitelistAppTemporarilyForEvent(
                pkgName, PowerWhitelistManager.EVENT_SMS, REASON_EVENT_SMS, reason);
        if (bopts == null) bopts = BroadcastOptions.makeBasic();
        bopts.setTemporaryAppAllowlist(duration,
                TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
                REASON_EVENT_SMS,
                "");
        bundle = bopts.toBundle();

        return bundle;
    }

    /**
     * Creates and dispatches the intent to the default SMS app, appropriate port or via the {@link
     * AppSmsManager}.
     *
     * @param pdus message pdus
     * @param format the message format, typically "3gpp" or "3gpp2"
     * @param destPort the destination port
     * @param resultReceiver the receiver handling the delivery result
     */
    private void dispatchSmsDeliveryIntent(byte[][] pdus, String format, int destPort,
            SmsBroadcastReceiver resultReceiver, boolean isClass0, int subId, long messageId) {
        Intent intent = new Intent();
        intent.putExtra("pdus", pdus);
        intent.putExtra("format", format);
        if (messageId != 0L) {
            intent.putExtra("messageId", messageId);
        }

        UserHandle userHandle = null;
        if (destPort == -1) {
            intent.setAction(Intents.SMS_DELIVER_ACTION);
            // Direct the intent to only the default SMS app. If we can't find a default SMS app
            // then sent it to all broadcast receivers.
            userHandle = TelephonyUtils.getSubscriptionUserHandle(mContext, subId);
            ComponentName componentName = SmsApplication.getDefaultSmsApplicationAsUser(mContext,
                    true, userHandle);
            if (componentName != null) {
                // Deliver SMS message only to this receiver.
                intent.setComponent(componentName);
                logWithLocalLog("Delivering SMS to: " + componentName.getPackageName()
                        + " " + componentName.getClassName(), messageId);
            } else {
                intent.setComponent(null);
            }

            // Handle app specific sms messages.
            AppSmsManager appManager = mPhone.getAppSmsManager();
            if (appManager.handleSmsReceivedIntent(intent)) {
                // The AppSmsManager handled this intent, we're done.
                dropSms(resultReceiver);
                return;
            }
        } else {
            intent.setAction(Intents.DATA_SMS_RECEIVED_ACTION);
            Uri uri = Uri.parse("sms://localhost:" + destPort);
            intent.setData(uri);
            intent.setComponent(null);
        }

        if (userHandle == null) {
            if (mFeatureFlags.smsMmsDeliverBroadcastsRedirectToMainUser()) {
                userHandle = mUserManager.getMainUser();
            } else {
                userHandle = UserHandle.SYSTEM;
            }
        }
        Bundle options = handleSmsWhitelisting(intent.getComponent(), isClass0);
        dispatchIntent(intent, android.Manifest.permission.RECEIVE_SMS,
                AppOpsManager.OPSTR_RECEIVE_SMS, options, resultReceiver, userHandle, subId);
    }

    /**
     * Function to detect and handle duplicate messages. If the received message should replace an
     * existing message in the raw db, this function deletes the existing message. If an existing
     * message takes priority (for eg, existing message has already been broadcast), then this new
     * message should be dropped.
     * @return true if the message represented by the passed in tracker should be dropped,
     * false otherwise
     */
    private boolean checkAndHandleDuplicate(InboundSmsTracker tracker) throws SQLException {
        Pair<String, String[]> exactMatchQuery = tracker.getExactMatchDupDetectQuery();

        Cursor cursor = null;
        try {
            // Check for duplicate message segments
            cursor = mResolver.query(sRawUri, PDU_DELETED_FLAG_PROJECTION, exactMatchQuery.first,
                    exactMatchQuery.second, null);

            // moveToNext() returns false if no duplicates were found
            if (cursor != null && cursor.moveToNext()) {
                if (cursor.getCount() != 1) {
                    logeWithLocalLog("checkAndHandleDuplicate: Exact match query returned "
                            + cursor.getCount() + " rows", tracker.getMessageId());
                }

                // if the exact matching row is marked deleted, that means this message has already
                // been received and processed, and can be discarded as dup
                if (cursor.getInt(
                        PDU_DELETED_FLAG_PROJECTION_INDEX_MAPPING.get(DELETED_FLAG_COLUMN)) == 1) {
                    logWithLocalLog("checkAndHandleDuplicate: Discarding duplicate "
                            + "message/segment: " + tracker);
                    logDupPduMismatch(cursor, tracker);
                    return true;   // reject message
                } else {
                    // exact match duplicate is not marked deleted. If it is a multi-part segment,
                    // the code below for inexact match will take care of it. If it is a single
                    // part message, handle it here.
                    if (tracker.getMessageCount() == 1) {
                        // delete the old message segment permanently
                        deleteFromRawTable(exactMatchQuery.first, exactMatchQuery.second,
                                DELETE_PERMANENTLY);
                        logWithLocalLog("checkAndHandleDuplicate: Replacing duplicate message: "
                                + tracker);
                        logDupPduMismatch(cursor, tracker);
                    }
                }
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        // The code above does an exact match. Multi-part message segments need an additional check
        // on top of that: if there is a message segment that conflicts this new one (may not be an
        // exact match), replace the old message segment with this one.
        if (tracker.getMessageCount() > 1) {
            Pair<String, String[]> inexactMatchQuery = tracker.getInexactMatchDupDetectQuery();
            cursor = null;
            try {
                // Check for duplicate message segments
                cursor = mResolver.query(sRawUri, PDU_DELETED_FLAG_PROJECTION,
                        inexactMatchQuery.first, inexactMatchQuery.second, null);

                // moveToNext() returns false if no duplicates were found
                if (cursor != null && cursor.moveToNext()) {
                    if (cursor.getCount() != 1) {
                        logeWithLocalLog("checkAndHandleDuplicate: Inexact match query returned "
                                + cursor.getCount() + " rows", tracker.getMessageId());
                    }
                    // delete the old message segment permanently
                    deleteFromRawTable(inexactMatchQuery.first, inexactMatchQuery.second,
                            DELETE_PERMANENTLY);
                    logWithLocalLog("checkAndHandleDuplicate: Replacing duplicate message segment: "
                            + tracker);
                    logDupPduMismatch(cursor, tracker);
                }
            } finally {
                if (cursor != null) {
                    cursor.close();
                }
            }
        }

        return false;
    }

    private void logDupPduMismatch(Cursor cursor, InboundSmsTracker tracker) {
        String oldPduString = cursor.getString(
                PDU_DELETED_FLAG_PROJECTION_INDEX_MAPPING.get(PDU_COLUMN));
        byte[] pdu = tracker.getPdu();
        byte[] oldPdu = HexDump.hexStringToByteArray(oldPduString);
        if (!Arrays.equals(oldPdu, tracker.getPdu())) {
            logeWithLocalLog("Warning: dup message PDU of length " + pdu.length
                    + " is different from existing PDU of length " + oldPdu.length,
                    tracker.getMessageId());
        }
    }

    /**
     * Insert a message PDU into the raw table so we can acknowledge it immediately.
     * If the device crashes before the broadcast to listeners completes, it will be delivered
     * from the raw table on the next device boot. For single-part messages, the deleteWhere
     * and deleteWhereArgs fields of the tracker will be set to delete the correct row after
     * the ordered broadcast completes.
     *
     * @param tracker the tracker to add to the raw table
     * @return true on success; false on failure to write to database
     */
    private int addTrackerToRawTable(InboundSmsTracker tracker, boolean deDup) {
        if (deDup) {
            try {
                if (checkAndHandleDuplicate(tracker)) {
                    return Intents.RESULT_SMS_DUPLICATED;   // reject message
                }
            } catch (SQLException e) {
                loge("addTrackerToRawTable: Can't access SMS database, "
                        + SmsController.formatCrossStackMessageId(tracker.getMessageId()), e);
                return RESULT_SMS_DATABASE_ERROR;    // reject message
            }
        } else {
            log("addTrackerToRawTable: Skipped message de-duping logic", tracker.getMessageId());
        }

        String address = tracker.getAddress();
        String refNumber = Integer.toString(tracker.getReferenceNumber());
        String count = Integer.toString(tracker.getMessageCount());
        ContentValues values = tracker.getContentValues();

        if (VDBG) {
            log("addTrackerToRawTable: adding content values to raw table: " + values.toString(),
                    tracker.getMessageId());
        }
        Uri newUri = mResolver.insert(sRawUri, values);
        if (DBG) log("addTrackerToRawTable: URI of new row: " + newUri, tracker.getMessageId());

        try {
            long rowId = ContentUris.parseId(newUri);
            if (tracker.getMessageCount() == 1) {
                // set the delete selection args for single-part message
                tracker.setDeleteWhere(SELECT_BY_ID, new String[]{Long.toString(rowId)});
            } else {
                // set the delete selection args for multi-part message
                String[] deleteWhereArgs = {address, refNumber, count};
                tracker.setDeleteWhere(tracker.getQueryForSegments(), deleteWhereArgs);
            }
            return Intents.RESULT_SMS_HANDLED;
        } catch (Exception e) {
            loge("addTrackerToRawTable: error parsing URI for new row: " + newUri
                    + " " + SmsController.formatCrossStackMessageId(tracker.getMessageId()), e);
            return RESULT_SMS_INVALID_URI;
        }
    }

    /**
     * Returns whether the default message format for the current radio technology is 3GPP2.
     * @return true if the radio technology uses 3GPP2 format by default, false for 3GPP format
     */
    static boolean isCurrentFormat3gpp2() {
        int activePhone = TelephonyManager.getDefault().getCurrentPhoneType();
        return (PHONE_TYPE_CDMA == activePhone);
    }

    @VisibleForTesting
    public static int sTimeoutDurationMillis = 10 * 60 * 1000; // 10 minutes

    /**
     * Handler for an {@link InboundSmsTracker} broadcast. Deletes PDUs from the raw table and
     * logs the broadcast duration (as an error if the other receivers were especially slow).
     */
    public final class SmsBroadcastReceiver extends BroadcastReceiver {
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
        private final String mDeleteWhere;
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
        private final String[] mDeleteWhereArgs;
        private long mBroadcastTimeMillis;
        public Intent mWaitingForIntent;
        private final InboundSmsTracker mInboundSmsTracker;

        /**
         * This method must be called anytime an ordered broadcast is sent that is expected to be
         * received by this receiver.
         */
        public synchronized void setWaitingForIntent(Intent intent) {
            mWaitingForIntent = intent;
            mBroadcastTimeMillis = System.currentTimeMillis();
            removeMessages(EVENT_RECEIVER_TIMEOUT);
            sendMessageDelayed(EVENT_RECEIVER_TIMEOUT, sTimeoutDurationMillis);
        }

        public SmsBroadcastReceiver(InboundSmsTracker tracker) {
            mDeleteWhere = tracker.getDeleteWhere();
            mDeleteWhereArgs = tracker.getDeleteWhereArgs();
            mInboundSmsTracker = tracker;
        }

        /**
         * This method is called if the expected intent (mWaitingForIntent) is not received and
         * the timer for it expires. It fakes the receipt of the intent to unblock the state
         * machine.
         */
        public void fakeNextAction() {
            if (mWaitingForIntent != null) {
                logeWithLocalLog("fakeNextAction: " + mWaitingForIntent.getAction(),
                        mInboundSmsTracker.getMessageId());
                handleAction(mWaitingForIntent, false);
            } else {
                logeWithLocalLog("fakeNextAction: mWaitingForIntent is null",
                        mInboundSmsTracker.getMessageId());
            }
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent == null) {
                logeWithLocalLog("onReceive: received null intent, faking " + mWaitingForIntent,
                        mInboundSmsTracker.getMessageId());
                return;
            }
            handleAction(intent, true);
        }

        @SuppressLint("MissingPermission")
        private synchronized void handleAction(@NonNull Intent intent, boolean onReceive) {
            String action = intent.getAction();
            if (mWaitingForIntent == null || !mWaitingForIntent.getAction().equals(action)) {
                logeWithLocalLog("handleAction: Received " + action + " when expecting "
                        + mWaitingForIntent == null ? "none" : mWaitingForIntent.getAction(),
                        mInboundSmsTracker.getMessageId());
                return;
            }

            if (onReceive) {
                int durationMillis = (int) (System.currentTimeMillis() - mBroadcastTimeMillis);
                if (durationMillis >= 5000) {
                    loge("Slow ordered broadcast completion time for " + action + ": "
                            + durationMillis + " ms");
                } else if (DBG) {
                    log("Ordered broadcast completed for " + action + " in: "
                            + durationMillis + " ms");
                }
            }

            int subId = intent.getIntExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX,
                    SubscriptionManager.INVALID_SUBSCRIPTION_ID);
            if (action.equals(Intents.SMS_DELIVER_ACTION)) {
                // Now dispatch the notification only intent
                intent.setAction(Intents.SMS_RECEIVED_ACTION);
                // Allow registered broadcast receivers to get this intent even
                // when they are in the background.
                intent.setComponent(null);
                // All running users will be notified of the received sms.
                Bundle options = handleSmsWhitelisting(null, false /* bgActivityStartAllowed */);

                setWaitingForIntent(intent);
                dispatchIntent(intent, android.Manifest.permission.RECEIVE_SMS,
                        AppOpsManager.OPSTR_RECEIVE_SMS,
                        options, this, UserHandle.ALL, subId);
            } else if (action.equals(Intents.WAP_PUSH_DELIVER_ACTION)) {
                // Now dispatch the notification only intent
                intent.setAction(Intents.WAP_PUSH_RECEIVED_ACTION);
                intent.setComponent(null);
                // Only the primary user will receive notification of incoming mms.
                // That app will do the actual downloading of the mms.
                long duration = mPowerWhitelistManager.whitelistAppTemporarilyForEvent(
                        mContext.getPackageName(),
                        PowerWhitelistManager.EVENT_MMS,
                        REASON_EVENT_MMS,
                        "mms-broadcast");
                BroadcastOptions bopts = BroadcastOptions.makeBasic();
                bopts.setTemporaryAppAllowlist(duration,
                        TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
                        REASON_EVENT_MMS,
                        "");
                Bundle options = bopts.toBundle();

                String mimeType = intent.getType();

                setWaitingForIntent(intent);
                if (mFeatureFlags.smsMmsDeliverBroadcastsRedirectToMainUser()) {
                    dispatchIntent(intent, WapPushOverSms.getPermissionForType(mimeType),
                            WapPushOverSms.getAppOpsStringPermissionForIntent(mimeType), options,
                            this, mUserManager.getMainUser(), subId);
                } else {
                    dispatchIntent(intent, WapPushOverSms.getPermissionForType(mimeType),
                            WapPushOverSms.getAppOpsStringPermissionForIntent(mimeType), options,
                            this, UserHandle.SYSTEM, subId);
                }
            } else {
                // Now that the intents have been deleted we can clean up the PDU data.
                if (!Intents.DATA_SMS_RECEIVED_ACTION.equals(action)
                        && !Intents.SMS_RECEIVED_ACTION.equals(action)
                        && !Intents.WAP_PUSH_RECEIVED_ACTION.equals(action)) {
                    loge("unexpected BroadcastReceiver action: " + action);
                }

                if (onReceive) {
                    int rc = getResultCode();
                    if ((rc != Activity.RESULT_OK) && (rc != Intents.RESULT_SMS_HANDLED)) {
                        loge("a broadcast receiver set the result code to " + rc
                                + ", deleting from raw table anyway!");
                    } else if (DBG) {
                        log("successful broadcast, deleting from raw table.");
                    }
                }

                deleteFromRawTable(mDeleteWhere, mDeleteWhereArgs, MARK_DELETED);
                mWaitingForIntent = null;
                removeMessages(EVENT_RECEIVER_TIMEOUT);
                sendMessage(EVENT_BROADCAST_COMPLETE);
            }
        }
    }

    /**
     * Callback that handles filtering results by carrier services.
     */
    private final class CarrierServicesSmsFilterCallback implements
            CarrierServicesSmsFilter.CarrierServicesSmsFilterCallbackInterface {
        private final byte[][] mPdus;
        private final int mDestPort;
        private final InboundSmsTracker mTracker;
        private final String mSmsFormat;
        private final SmsBroadcastReceiver mSmsBroadcastReceiver;
        private final boolean mUserUnlocked;
        private final boolean mIsClass0;
        private final int mSubId;
        private final long mMessageId;
        private final boolean mBlock;
        private final List<SmsFilter> mRemainingFilters;

        CarrierServicesSmsFilterCallback(byte[][] pdus, int destPort, InboundSmsTracker tracker,
                String smsFormat, SmsBroadcastReceiver smsBroadcastReceiver, boolean userUnlocked,
                boolean isClass0, int subId, long messageId, boolean block,
                List<SmsFilter> remainingFilters) {
            mPdus = pdus;
            mDestPort = destPort;
            mTracker = tracker;
            mSmsFormat = smsFormat;
            mSmsBroadcastReceiver = smsBroadcastReceiver;
            mUserUnlocked = userUnlocked;
            mIsClass0 = isClass0;
            mSubId = subId;
            mMessageId = messageId;
            mBlock = block;
            mRemainingFilters = remainingFilters;
        }

        @Override
        public void onFilterComplete(int result) {
            log("onFilterComplete: result is " + result, mTracker.getMessageId());

            boolean carrierRequestedDrop =
                    (result & CarrierMessagingService.RECEIVE_OPTIONS_DROP) != 0;
            if (carrierRequestedDrop) {
                // Carrier app asked the platform to drop the SMS. Drop it from the database and
                // complete processing.
                dropFilteredSms(mTracker, mSmsBroadcastReceiver, mBlock);
                return;
            }

            boolean filterInvoked = filterSms(mPdus, mDestPort, mTracker, mSmsBroadcastReceiver,
                    mUserUnlocked, mBlock, mRemainingFilters);
            if (filterInvoked) {
                // A remaining filter has assumed responsibility for further message processing.
                return;
            }

            // Now that all filters have been invoked, drop the message if it is blocked.
            if (mBlock) {
                // Only delete the message if the user is unlocked. Otherwise, we should reprocess
                // the message after unlock so the filter has a chance to run while credential-
                // encrypted storage is available.
                if (mUserUnlocked) {
                    log("onFilterComplete: dropping message as the sender is blocked",
                            mTracker.getMessageId());
                    dropFilteredSms(mTracker, mSmsBroadcastReceiver, mBlock);
                } else {
                    // Just complete handling of the message without dropping it.
                    sendMessage(EVENT_BROADCAST_COMPLETE);
                }
                return;
            }

            // Message matched no filters and is not blocked, so complete processing.
            if (mUserUnlocked) {
                dispatchSmsDeliveryIntent(
                        mPdus, mSmsFormat, mDestPort, mSmsBroadcastReceiver, mIsClass0, mSubId,
                        mMessageId);
            } else {
                // Don't do anything further, leave the message in the raw table if the
                // credential-encrypted storage is still locked and show the new message
                // notification if the message is visible to the user.
                if (!isSkipNotifyFlagSet(result)) {
                    showNewMessageNotification();
                }
                sendMessage(EVENT_BROADCAST_COMPLETE);
            }
        }
    }

    private void dropSms(SmsBroadcastReceiver receiver) {
        // Needs phone package permissions.
        deleteFromRawTable(receiver.mDeleteWhere, receiver.mDeleteWhereArgs, MARK_DELETED);
        sendMessage(EVENT_BROADCAST_COMPLETE);
    }

    private boolean isMtSmsPollingMessage(@NonNull SmsMessageBase smsb) {
        if (!mFeatureFlags.carrierRoamingNbIotNtn()
                || !mContext.getResources().getBoolean(R.bool.config_enabled_mt_sms_polling)) {
            return false;
        }
        String mtSmsPollingText = mContext.getResources()
                .getString(R.string.config_mt_sms_polling_text);
        return !TextUtils.isEmpty(mtSmsPollingText)
                && mtSmsPollingText.equals(smsb.getMessageBody());
    }

    /** Checks whether the flag to skip new message notification is set in the bitmask returned
     *  from the carrier app.
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private boolean isSkipNotifyFlagSet(int callbackResult) {
        return (callbackResult
            & RECEIVE_OPTIONS_SKIP_NOTIFY_WHEN_CREDENTIAL_PROTECTED_STORAGE_UNAVAILABLE) > 0;
    }

    /**
     * Log with debug level in logcat and LocalLog
     * @param logMsg msg to log
     */
    protected void logWithLocalLog(String logMsg) {
        log(logMsg);
        mLocalLog.log(logMsg);
    }

    /**
     * Log with debug level in logcat and LocalLog
     * @param logMsg msg to log
     * @param id unique message id
     */
    protected void logWithLocalLog(String logMsg, long id) {
        log(logMsg, id);
        mLocalLog.log(logMsg + ", " + SmsController.formatCrossStackMessageId(id));
    }

    /**
     * Log with error level in logcat and LocalLog
     * @param logMsg msg to log
     */
    protected void logeWithLocalLog(String logMsg) {
        loge(logMsg);
        mLocalLog.log(logMsg);
    }

    /**
     * Log with error level in logcat and LocalLog
     * @param logMsg msg to log
     * @param id unique message id
     */
    protected void logeWithLocalLog(String logMsg, long id) {
        loge(logMsg, id);
        mLocalLog.log(logMsg + ", " + SmsController.formatCrossStackMessageId(id));
    }

    /**
     * Log with debug level.
     * @param s the string to log
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    @Override
    protected void log(String s) {
        Rlog.d(getName(), s);
    }

    /**
     * Log with debug level.
     * @param s the string to log
     * @param id unique message id
     */
    protected void log(String s, long id) {
        log(s + ", " + SmsController.formatCrossStackMessageId(id));
    }

    /**
     * Log with error level.
     * @param s the string to log
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    @Override
    protected void loge(String s) {
        Rlog.e(getName(), s);
    }

    /**
     * Log with error level.
     * @param s the string to log
     * @param id unique message id
     */
    protected void loge(String s, long id) {
        loge(s + ", " + SmsController.formatCrossStackMessageId(id));
    }

    /**
     * Log with error level.
     * @param s the string to log
     * @param e is a Throwable which logs additional information.
     */
    @Override
    protected void loge(String s, Throwable e) {
        Rlog.e(getName(), s, e);
    }

    /**
     * Build up the SMS message body from the SmsMessage array of received SMS
     *
     * @param msgs The SmsMessage array of the received SMS
     * @return The text message body
     */
    private static String buildMessageBodyFromPdus(SmsMessage[] msgs) {
        if (msgs.length == 1) {
            // There is only one part, so grab the body directly.
            return replaceFormFeeds(msgs[0].getDisplayMessageBody());
        } else {
            // Build up the body from the parts.
            StringBuilder body = new StringBuilder();
            for (SmsMessage msg: msgs) {
                // getDisplayMessageBody() can NPE if mWrappedMessage inside is null.
                body.append(msg.getDisplayMessageBody());
            }
            return replaceFormFeeds(body.toString());
        }
    }

    @Override
    public void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
        pw.println(getName() + " extends StateMachine:");
        pw.increaseIndent();
        super.dump(fd, pw, args);
        if (mCellBroadcastServiceManager != null) {
            mCellBroadcastServiceManager.dump(fd, pw, args);
        }
        pw.println("mLocalLog:");
        pw.increaseIndent();
        mLocalLog.dump(fd, pw, args);
        pw.decreaseIndent();
        pw.println("mCarrierServiceLocalLog:");
        pw.increaseIndent();
        mCarrierServiceLocalLog.dump(fd, pw, args);
        pw.decreaseIndent();
        pw.decreaseIndent();
    }

    // Some providers send formfeeds in their messages. Convert those formfeeds to newlines.
    private static String replaceFormFeeds(String s) {
        return s == null ? "" : s.replace('\f', '\n');
    }

    @VisibleForTesting
    public PowerManager.WakeLock getWakeLock() {
        return mWakeLock;
    }

    @VisibleForTesting
    public int getWakeLockTimeout() {
        return mWakeLockTimeout;
    }

    /**
    * Sets the wakelock timeout to {@link timeOut} milliseconds
    */
    private void setWakeLockTimeout(int timeOut) {
        mWakeLockTimeout = timeOut;
    }

    /**
     * Set the SMS filters used by {@link #filterSms} for testing purposes.
     *
     * @param smsFilters List of SMS filters, or null to restore the default filters.
     */
    @VisibleForTesting
    public void setSmsFiltersForTesting(@Nullable List<SmsFilter> smsFilters) {
        if (smsFilters == null) {
            mSmsFilters = createDefaultSmsFilters();
        } else {
            mSmsFilters = smsFilters;
        }
    }

    /**
     * Handler for the broadcast sent when the new message notification is clicked. It launches the
     * default SMS app.
     */
    private static class NewMessageNotificationActionReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (ACTION_OPEN_SMS_APP.equals(intent.getAction())) {
                // do nothing if the user had not unlocked the device yet
                // TODO(b/355049884): This is looking at sms package of the wrong user!
                UserManager userManager =
                        (UserManager) context.getSystemService(Context.USER_SERVICE);
                PackageManager pm = context.getPackageManager();
                if (Flags.hsumPackageManager()) {
                    pm = context.createContextAsUser(UserHandle.CURRENT, 0).getPackageManager();
                }
                if (userManager.isUserUnlocked()) {
                    context.startActivityAsUser(pm.getLaunchIntentForPackage(
                            Telephony.Sms.getDefaultSmsPackage(context)), UserHandle.CURRENT);
                }
            }
        }
    }

    protected byte[] decodeHexString(String hexString) {
        if (hexString == null || hexString.length() % 2 == 1) {
            return null;
        }
        byte[] bytes = new byte[hexString.length() / 2];
        for (int i = 0; i < hexString.length(); i += 2) {
            bytes[i / 2] = hexToByte(hexString.substring(i, i + 2));
        }
        return bytes;
    }

    private byte hexToByte(String hexString) {
        int firstDigit = toDigit(hexString.charAt(0));
        int secondDigit = toDigit(hexString.charAt(1));
        return (byte) ((firstDigit << 4) + secondDigit);
    }

    private int toDigit(char hexChar) {
        int digit = Character.digit(hexChar, 16);
        if (digit == -1) {
            return 0;
        }
        return digit;
    }


    /**
     * Registers the broadcast receiver to launch the default SMS app when the user clicks the
     * new message notification.
     */
    static void registerNewMessageNotificationActionHandler(Context context) {
        IntentFilter userFilter = new IntentFilter();
        userFilter.addAction(ACTION_OPEN_SMS_APP);
        context.registerReceiver(new NewMessageNotificationActionReceiver(), userFilter,
                Context.RECEIVER_NOT_EXPORTED);
    }

    protected abstract class CbTestBroadcastReceiver extends BroadcastReceiver {

        protected abstract void handleTestAction(Intent intent);

        protected final String mTestAction;

        public CbTestBroadcastReceiver(String testAction) {
            mTestAction = testAction;
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            logd("Received test intent action=" + intent.getAction());
            if (intent.getAction().equals(mTestAction)) {
                // Return early if phone_id is explicilty included and does not match mPhone.
                // If phone_id extra is not included, continue.
                int phoneId = mPhone.getPhoneId();
                if (intent.getIntExtra("phone_id", phoneId) != phoneId) {
                    return;
                }
                handleTestAction(intent);
            }
        }
    }

    /** A filter for incoming messages allowing the normal processing flow to be skipped. */
    @VisibleForTesting
    public interface SmsFilter {
        /**
         * Returns true if a filter is invoked and the SMS processing flow should be diverted, false
         * otherwise.
         *
         * <p>If the filter can immediately determine that the message matches, it must call
         * {@link #dropFilteredSms} to drop the message from the database once it has been
         * processed.
         *
         * <p>If the filter must perform some asynchronous work to determine if the message matches,
         * it should return true to defer processing. Once it has made a determination, if it finds
         * the message matches, it must call {@link #dropFilteredSms}. If the message does not
         * match, it must be passed through {@code remainingFilters} and either dropped if the
         * remaining filters all return false or if {@code block} is true, or else it must be
         * broadcast.
         */
        boolean filterSms(byte[][] pdus, int destPort, InboundSmsTracker tracker,
                SmsBroadcastReceiver resultReceiver, boolean userUnlocked, boolean block,
                List<SmsFilter> remainingFilters);
    }
}
