| /* |
| * 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.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.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.metrics.TelephonyMetrics; |
| 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; |
| |
| /** |
| * 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) { |
| super(name, looper); |
| |
| mContext = context; |
| mStorageMonitor = storageMonitor; |
| mPhone = phone; |
| mResolver = context.getContentResolver(); |
| mWapPush = new WapPushOverSms(context); |
| |
| boolean smsCapable = mContext.getResources().getBoolean( |
| com.android.internal.R.bool.config_sms_capable); |
| mSmsReceiveDisabled = !TelephonyManager.from(mContext).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; |
| } |
| |
| // 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; |
| } |
| |
| 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); |
| 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()); |
| 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); |
| 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()); |
| return false; |
| } |
| } |
| output.write(pdu, 0, pdu.length); |
| } |
| } |
| |
| SmsBroadcastReceiver resultReceiver = tracker.getSmsBroadcastReceiver(this); |
| |
| if (!mUserManager.isUserUnlocked()) { |
| 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()); |
| // 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()); |
| 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; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| @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); |
| 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) |
| 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 List<UserHandle> runningUserHandles = new ArrayList(); |
| for (UserHandle handle : userHandles) { |
| if (mUserManager.isUserRunning(handle)) { |
| runningUserHandles.add(handle); |
| } 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 (users[i] != UserHandle.SYSTEM.getIdentifier()) { |
| // 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 user SYSTEM is processed. |
| try { |
| if (users[i] == UserHandle.SYSTEM.getIdentifier()) { |
| resultReceiver.setWaitingForIntent(intent); |
| } |
| mContext.createPackageContextAsUser(mContext.getPackageName(), 0, targetUser) |
| .sendOrderedBroadcast(intent, Activity.RESULT_OK, permission, appOp, |
| users[i] == UserHandle.SYSTEM.getIdentifier() |
| ? 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()); |
| } |
| |
| /** |
| * 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) { |
| 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); |
| } |
| |
| 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); |
| 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); |
| } |
| |
| /** 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 |
| UserManager userManager = |
| (UserManager) context.getSystemService(Context.USER_SERVICE); |
| if (userManager.isUserUnlocked()) { |
| context.startActivity(context.getPackageManager().getLaunchIntentForPackage( |
| Telephony.Sms.getDefaultSmsPackage(context))); |
| } |
| } |
| } |
| } |
| |
| 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); |
| } |
| } |