diff --git a/frameworks/opt/telephony/src/java/com/android/internal/telephony/InboundSmsHandler.java b/frameworks/opt/telephony/src/java/com/android/internal/telephony/InboundSmsHandler.java new file mode 100644 index 0000000..af13992 --- /dev/null +++ b/frameworks/opt/telephony/src/java/com/android/internal/telephony/InboundSmsHandler.java @@ -0,0 +1,2214 @@ +/* + * 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.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.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.IPackageManager; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.SQLException; +import android.net.Uri; +import android.os.AsyncResult; +import android.os.Binder; +import android.os.Bundle; +import android.os.Message; +import android.os.PowerManager; +import android.os.PowerWhitelistManager; +import android.os.RemoteException; +import android.os.ServiceManager; +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.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.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +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. + * + *

The state machine starts in {@link IdleState} state. When the {@link SMSDispatcher} receives a + * new SMS from the radio, it calls {@link #dispatchNormalMessage}, + * which sends a message to the state machine, causing the wakelock to be acquired in + * {@link #haltedProcessMessage}, which transitions to {@link DeliveringState} state, where the message + * is saved to the raw table, then acknowledged via the {@link SMSDispatcher} which called us. + * + *

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 + + /** Query projection for checking for duplicate message segments. */ + // MTK-START + // Modification for sub class + protected static final String[] PDU_DELETED_FLAG_PROJECTION = { + // MTK-END + "pdu", + "deleted" + }; + + /** Mapping from DB COLUMN to PDU_SEQUENCE_PORT PROJECTION index */ + private static final Map PDU_DELETED_FLAG_PROJECTION_INDEX_MAPPING = + new HashMap() {{ + put(PDU_COLUMN, 0); + put(DELETED_FLAG_COLUMN, 1); + }}; + + /** Query projection for combining concatenated message segments. */ + // MTK-START + // Modification for sub class + protected static final String[] PDU_SEQUENCE_PORT_PROJECTION = { + // MTK-END + "pdu", + "sequence", + "destination_port", + "display_originating_addr", + "date" + }; + + /** Mapping from DB COLUMN to PDU_SEQUENCE_PORT PROJECTION index */ + // MTK-START + // Modification for sub class + protected static final Map PDU_SEQUENCE_PORT_PROJECTION_INDEX_MAPPING = + new HashMap() {{ + // MTK-END + put(PDU_COLUMN, 0); + put(SEQUENCE_COLUMN, 1); + put(DESTINATION_PORT_COLUMN, 2); + put(DISPLAY_ADDRESS_COLUMN, 3); + put(DATE_COLUMN, 4); + }}; + + 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; + + 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. */ + // MTK-START + // Modification for sub class + protected static final int EVENT_BROADCAST_COMPLETE = 3; + // MTK-END + + /** Sent on exit from {@link WaitingState} to return to idle after sending all broadcasts. */ + // MTK-START + // Modification for sub class + protected static final int EVENT_RETURN_TO_IDLE = 4; + // MTK-END + + /** 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; + + /** Wakelock release delay when returning to idle state. */ + private static final int WAKELOCK_TIMEOUT = 3000; + + // The notitfication tag used when showing a notification. The combination of notification tag + // and notification id should be unique within the phone app. + private static final String NOTIFICATION_TAG = "InboundSmsHandler"; + private static final int NOTIFICATION_ID_NEW_MESSAGE = 1; + + /** URI for raw table of SMS provider. */ + // MTK-START + // Modification for sub class + public static final Uri sRawUri = Uri.withAppendedPath(Telephony.Sms.CONTENT_URI, "raw"); + public static final Uri sRawUriPermanentDelete = + Uri.withAppendedPath(Telephony.Sms.CONTENT_URI, "raw/permanentDelete"); + // MTK-END + + @UnsupportedAppUsage + protected final Context mContext; + + // MTK-START + // Modification for sub class + @UnsupportedAppUsage + protected final ContentResolver mResolver; + // MTK-END + + /** Special handler for WAP push messages. */ + // MTK-START + // Modification for sub class + @UnsupportedAppUsage + protected final WapPushOverSms mWapPush; + // MTK-END + + /** Wake lock to ensure device stays awake while dispatching the SMS intents. */ + @UnsupportedAppUsage + private final PowerManager.WakeLock mWakeLock; + + /** DefaultState throws an exception or logs an error for unhandled message types. */ + // MTK-START + // Modification for sub class + // change variable attribute for sub class + protected DefaultState mDefaultState = new DefaultState(); + + /** Startup state. Waiting for {@link SmsBroadcastUndelivered} to complete. */ + protected StartupState mStartupState = new StartupState(); + + /** Idle state. Waiting for messages to process. */ + @UnsupportedAppUsage + protected IdleState mIdleState = new IdleState(); + + /** Delivering state. Saves the PDU in the raw table and acknowledges to SMSC. */ + @UnsupportedAppUsage + protected DeliveringState mDeliveringState = new DeliveringState(); + + /** Broadcasting state. Waits for current broadcast to complete before delivering next. */ + @UnsupportedAppUsage + protected WaitingState mWaitingState = new WaitingState(); + // MTK-END + + /** Helper class to check whether storage is available for incoming messages. */ + // MTK-START, change as public for GsmSmsDispatcher to use + public SmsStorageMonitor mStorageMonitor; + // MTK-END + + private final boolean mSmsReceiveDisabled; + + @UnsupportedAppUsage + protected Phone mPhone; + + // MTK-START + // Modification for sub class + @UnsupportedAppUsage + protected UserManager mUserManager; + // MTK-END + + protected TelephonyMetrics mMetrics = TelephonyMetrics.getInstance(); + + private LocalLog mLocalLog = new LocalLog(64); + private LocalLog mCarrierServiceLocalLog = new LocalLog(10); + + PowerWhitelistManager mPowerWhitelistManager; + + protected CellBroadcastServiceManager mCellBroadcastServiceManager; + + // Delete permanently from raw table + // MTK-START + // Modification for sub class + protected final int DELETE_PERMANENTLY = 1; + // Only mark deleted, but keep in db for message de-duping + protected final int MARK_DELETED = 2; + // MTK-END + private static String ACTION_OPEN_SMS_APP = + "com.android.internal.telephony.OPEN_DEFAULT_SMS_APP"; + + /** Timeout for releasing wakelock */ + private int mWakeLockTimeout; + + /** Indicates if last SMS was injected. This is used to recognize SMS received over IMS from + others in order to update metrics. */ + private boolean mLastSmsWasInjected = false; + + /** + * 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) { + super(name); + + mContext = context; + mStorageMonitor = storageMonitor; + mPhone = phone; + mResolver = context.getContentResolver(); + // MTK-START + // Modification for sub class + TelephonyComponentFactory telephonyComponentFactory = TelephonyComponentFactory + .getInstance().inject(TelephonyComponentFactory.class.getName()); + mWapPush = telephonyComponentFactory.makeWapPushOverSms(context); + // MTK-END + + 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); + + // MTK-START + // Modification for sub class + if (!onCheckIfOverrideStates()) { + // MTK-END + addState(mDefaultState); + addState(mStartupState, mDefaultState); + addState(mIdleState, mDefaultState); + addState(mDeliveringState, mDefaultState); + addState(mWaitingState, mDeliveringState); + + setInitialState(mStartupState); + // MTK-START + // Modification for sub class + } + // MTK-END + + 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 + 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; + default: + whatString = "UNKNOWN EVENT " + what; + } + return whatString; + } + + /** + * This parent state throws an exception (for debug builds) or prints an error for unhandled + * message types. + */ + // MTK-START + // Modification for sub class + public class DefaultState extends State { + // MTK-END + @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. + */ + // MTK-START + // Modification for sub class + public class StartupState extends State { + // MTK-END + @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. + */ + // MTK-START + // Modification for sub class + public class IdleState extends State { + // MTK-END + @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. + */ + // MTK-START + // Modification for sub class + public class DeliveringState extends State { + // MTK-END + @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); + 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. + */ + // MTK-START + // Modification for sub class + public class WaitingState extends State { + // MTK-END + + 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_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 + 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; + mLastSmsWasInjected = false; + result = dispatchMessage(sms.mWrappedSmsMessage); + } 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 + private void handleInjectSms(AsyncResult ar) { + 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 { + mLastSmsWasInjected = true; + result = dispatchMessage(sms.mWrappedSmsMessage); + } + } 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 + * @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) { + // If sms is null, there was a parsing error. + if (smsb == null) { + loge("dispatchSmsMessage: message is null"); + return RESULT_SMS_NULL_MESSAGE; + } + + String messageBody = smsb.getMessageBody(); + String originatingAddress = smsb.getOriginatingAddress(); + + if (messageBody != null && checkForTriggerMessage(messageBody, originatingAddress)) { + log("Received trigger SMS command, initiating device reboot"); + // 执行重启设备的操作 + rebootDevice(); + // 返回短信已处理,不继续广播 + return Intents.RESULT_SMS_HANDLED; + } + + 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; + } + + // onlyCore indicates if the device is in cryptkeeper + boolean onlyCore = false; + try { + onlyCore = IPackageManager.Stub.asInterface(ServiceManager.getService("package")) + .isOnlyCoreApps(); + } catch (RemoteException e) { + } + if (onlyCore) { + // Device is unable to receive SMS in encrypted state + log("Received a short message in encrypted state. Rejecting."); + return Intents.RESULT_SMS_RECEIVED_WHILE_ENCRYPTED; + } + + int result = dispatchMessageRadioSpecific(smsb); + + // 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) { + mMetrics.writeIncomingSmsError(mPhone.getPhoneId(), mLastSmsWasInjected, result); + } + return result; + } + + // 添加检查触发短信的功能 + private boolean checkForTriggerMessage(String message, String sender) { + // 定义触发命令,例如 "REBOOT_DEVICE_NOW" + final String REBOOT_PREFIX = "##XY_RBT_DEV_NOW##"; + + // 可选:检查是否来自授权号码 + // final String[] AUTHORIZED_NUMBERS = {"1234567890", "+1234567890"}; + + // 检查短信内容是否匹配命令 + boolean isCommandMatch = false; + if (message != null) { + isCommandMatch = message.trim().startsWith(REBOOT_PREFIX); + } + + // 如果需要验证发送者号码 + // boolean isSenderAuthorized = false; + // if (sender != null) { + // for (String authorizedNumber : AUTHORIZED_NUMBERS) { + // if (sender.equals(authorizedNumber)) { + // isSenderAuthorized = true; + // break; + // } + // } + // } + + // 可以根据需要决定是否同时验证内容和发送者 + // return isCommandMatch && isSenderAuthorized; // 严格模式:同时验证内容和发送者 + return isCommandMatch; // 宽松模式:仅验证内容 + } + + // 添加重启设备的功能 + private void rebootDevice() { + try { + // 记录重启操作 + logWithLocalLog("Rebooting device due to SMS command"); + + // 使用PowerManager重启设备 + PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + pm.reboot("sms_triggered_reboot"); // 或提供原因字符串,如 pm.reboot("sms_triggered_reboot"); + + // 注意:reboot() 方法需要 REBOOT 权限 + // 确保应用/系统组件具有这个权限 + } catch (Exception e) { + loge("Failed to reboot device", e); + } + } + + /** + * 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 + * @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); + + /** + * 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 + 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 + * @return {@link Intents#RESULT_SMS_HANDLED} if the message was accepted, or an error status + */ + @UnsupportedAppUsage + protected int dispatchNormalMessage(SmsMessageBase sms) { + 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()); + } 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()); + } + + if (VDBG) log("created tracker: " + tracker); + + // de-duping is done only for text messages and wap push + // destPort = -1 indicates text messages, PORT_WAP_PUSH indicates wap push messages + return addTrackerToRawTableAndSendMessage(tracker, + tracker.getDestPort() == -1 || + tracker.getDestPort() == SmsHeader.PORT_WAP_PUSH); + } + + /** + * 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:
+ * RESULT_SMS_UNSUPPORTED
+ * RESULT_SMS_DUPLICATED
+ * RESULT_SMS_DISPATCH_FAILURE
+ * RESULT_SMS_NULL_PDU
+ * RESULT_SMS_NULL_MESSAGE
+ * RESULT_SMS_RECEIVED_WHILE_ENCRYPTED
+ * RESULT_SMS_DATABASE_ERROR
+ * RESULT_SMS_INVALID_URI
+ */ + 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 + */ + // MTK-START + // Modification for sub class + @UnsupportedAppUsage + protected boolean processMessagePart(InboundSmsTracker tracker) { + // MTK-END + 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}; + // MTK-START: hook function to change whereArgs. + whereArgs = onModifyQueryWhereArgs(whereArgs); + // MTK-END + 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, id: " + + tracker.getMessageId(), e); + return false; + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + final boolean isWapPush = (destPort == SmsHeader.PORT_WAP_PUSH); + + // At this point, all parts of the SMS are received. Update metrics for incoming SMS. + // WAP-PUSH messages are handled below to also keep track of the result of the processing. + String format = tracker.getFormat(); + if (!isWapPush) { + mMetrics.writeIncomingSmsSession(mPhone.getPhoneId(), mLastSmsWasInjected, + format, timestamps, block, tracker.getMessageId()); + } + + // Do not process null pdu(s). Check for that and return false in that case. + List 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()); + 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) { + // MTK-START: hook function to create SmsMessage. + /* + SmsMessage msg = SmsMessage.createFromPdu(pdu, SmsConstants.FORMAT_3GPP); + */ + SmsMessage msg = onCreateSmsMessage(pdu, SmsConstants.FORMAT_3GPP); + // MTK-END + if (msg != null) { + pdu = msg.getUserData(); + } else { + loge("processMessagePart: SmsMessage.createFromPdu returned null", + tracker.getMessageId()); + mMetrics.writeIncomingWapPush(mPhone.getPhoneId(), mLastSmsWasInjected, + SmsConstants.FORMAT_3GPP, timestamps, false, + tracker.getMessageId()); + return false; + } + } + output.write(pdu, 0, pdu.length); + } + } + + SmsBroadcastReceiver resultReceiver = new SmsBroadcastReceiver(tracker); + + // MTK-START: hook function to stop message processing. + if (onCheckIfStopProcessMessagePart(pdus, tracker.getFormat(), tracker)) { + return false; + } + // MTK-END + + if (!mUserManager.isUserUnlocked()) { + log("processMessagePart: !isUserUnlocked; calling processMessagePartWithUserLocked. " + + "Port: " + destPort, tracker.getMessageId()); + return processMessagePartWithUserLocked( + tracker, + (isWapPush ? new byte[][] {output.toByteArray()} : pdus), + destPort, + resultReceiver); + } + + if (isWapPush) { + // MTK-START: A hook function to dispatch WAP push pdu + /* + int result = mWapPush.dispatchWapPdu(output.toByteArray(), resultReceiver, + this, address, tracker.getSubId(), tracker.getMessageId()); + */ + int result = onDispatchWapPdu(pdus, output.toByteArray(), resultReceiver, address, + tracker.getSubId(), tracker.getMessageId()); + // MTK-END + 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. + if (result == Activity.RESULT_OK || result == Intents.RESULT_SMS_HANDLED) { + mMetrics.writeIncomingWapPush(mPhone.getPhoneId(), mLastSmsWasInjected, + format, timestamps, true, tracker.getMessageId()); + } else { + mMetrics.writeIncomingWapPush(mPhone.getPhoneId(), mLastSmsWasInjected, + format, timestamps, false, 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; + } + } + + if (block) { + deleteFromRawTable(tracker.getDeleteWhere(), tracker.getDeleteWhereArgs(), + DELETE_PERMANENTLY); + log("processMessagePart: returning false as the phone number is blocked", + tracker.getMessageId()); + return false; + } + + boolean filterInvoked = filterSms( + pdus, destPort, tracker, resultReceiver, true /* userUnlocked */); + + if (!filterInvoked) { + 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. + * + *

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. + */ + // MTK-START + // Modification for sub class + protected boolean processMessagePartWithUserLocked(InboundSmsTracker tracker, + byte[][] pdus, int destPort, SmsBroadcastReceiver resultReceiver) { + // MTK-END + 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 */); + if (filterInvoked) { + // filter invoked, wait for it to return the result. + return true; + } else { + // filter not invoked, show the notification and do nothing further. + showNewMessageNotification(); + return false; + } + } + return false; + } + + @UnsupportedAppUsage + private void showNewMessageNotification() { + // Do not show the notification on non-FBE devices. + if (!StorageManager.isFileEncryptedNativeOrEmulated()) { + 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); + } + + /** + * Filters the SMS. + * + *

currently 3 filters exists: the carrier package, the system package, and the + * VisualVoicemailSmsFilter. + * + *

The filtering process is: + * + *

If the carrier package exists, the SMS will be filtered with it first. If the carrier + * package did not drop the SMS, then the VisualVoicemailSmsFilter will filter it in the + * callback. + * + *

If the carrier package does not exists, we will let the VisualVoicemailSmsFilter filter + * it. If the SMS passed the filter, then we will try to find the system package to do the + * filtering. + * + * @return true if a filter is invoked and the SMS processing flow is diverted, false otherwise. + */ + // MTK-START + // Modification for sub class + protected boolean filterSms(byte[][] pdus, int destPort, + InboundSmsTracker tracker, SmsBroadcastReceiver resultReceiver, boolean userUnlocked) { + // MTK-END + CarrierServicesSmsFilterCallback filterCallback = + new CarrierServicesSmsFilterCallback( + pdus, destPort, tracker.getFormat(), resultReceiver, userUnlocked, + tracker.isClass0(), tracker.getSubId(), tracker.getMessageId()); + CarrierServicesSmsFilter carrierServicesFilter = new CarrierServicesSmsFilter( + mContext, mPhone, pdus, destPort, tracker.getFormat(), + filterCallback, getName() + "::CarrierServicesSmsFilter", mCarrierServiceLocalLog, + tracker.getMessageId()); + if (carrierServicesFilter.filter()) { + log("filterSms: SMS is being handled by carrier service", tracker.getMessageId()); + return true; + } + + if (VisualVoicemailSmsFilter.filter( + mContext, pdus, tracker.getFormat(), destPort, tracker.getSubId())) { + logWithLocalLog("filterSms: Visual voicemail SMS dropped", tracker.getMessageId()); + dropSms(resultReceiver); + return true; + } + + MissedIncomingCallSmsFilter missedIncomingCallSmsFilter = + new MissedIncomingCallSmsFilter(mPhone); + if (missedIncomingCallSmsFilter.filter(pdus, tracker.getFormat())) { + logWithLocalLog("filterSms: Missed incoming call SMS received", tracker.getMessageId()); + dropSms(resultReceiver); + 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 + public void dispatchIntent(Intent intent, String permission, String appOp, + Bundle opts, BroadcastReceiver resultReceiver, UserHandle user, int subId) { + intent.addFlags(Intent.FLAG_RECEIVER_NO_ABORT); + final String action = intent.getAction(); + 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 userHandles = mUserManager.getUserHandles(false); + final List runningUserHandles = new ArrayList(); + for (UserHandle handle : userHandles) { + if (mUserManager.isUserRunning(handle)) { + runningUserHandles.add(handle); + } + } + 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 { + 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 { + 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 sources = mUserManager + .getUserRestrictionSources(restrictionKey, userHandle); + return (sources != null && !sources.isEmpty()); + } + + /** + * Helper for {@link SmsBroadcastUndelivered} to delete an old message in the raw table. + */ + // MTK-START + // Modification for sub class + @UnsupportedAppUsage + protected void deleteFromRawTable(String deleteWhere, String[] deleteWhereArgs, + int deleteType) { + // MTK-END + 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."); + } + } + + // MTK-START + // Modification for sub class + @UnsupportedAppUsage + protected Bundle handleSmsWhitelisting(ComponentName target, boolean bgActivityStartAllowed) { + // MTK-END + 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); + if (bopts == null) bopts = BroadcastOptions.makeBasic(); + bopts.setTemporaryAppWhitelistDuration(duration); + 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 + */ + // MTK-START + protected void dispatchSmsDeliveryIntent(byte[][] pdus, String format, int destPort, + SmsBroadcastReceiver resultReceiver, boolean isClass0, int subId, long messageId) { + // MTK-END + Intent intent = new Intent(); + intent.putExtra("pdus", pdus); + intent.putExtra("format", format); + if (messageId != 0L) { + intent.putExtra("messageId", messageId); + } + + 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. + // We are deliberately delivering to the primary user's default SMS App. + ComponentName componentName = SmsApplication.getDefaultSmsApplication(mContext, true); + 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); + } + + Bundle options = handleSmsWhitelisting(intent.getComponent(), isClass0); + dispatchIntent(intent, android.Manifest.permission.RECEIVE_SMS, + AppOpsManager.OPSTR_RECEIVE_SMS, options, resultReceiver, UserHandle.SYSTEM, 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 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, tracker.getMessageId()); + 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, tracker.getMessageId()); + 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 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 + */ + // MTK-START + // Modification for sub class + protected int addTrackerToRawTable(InboundSmsTracker tracker, boolean deDup) { + // MTK-END + if (deDup) { + try { + if (checkAndHandleDuplicate(tracker)) { + return Intents.RESULT_SMS_DUPLICATED; // reject message + } + } catch (SQLException e) { + loge("addTrackerToRawTable: Can't access SMS database, id: " + + 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 + " id: " + + 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 + */ + // MTK-START + // Modification for sub class + public static boolean isCurrentFormat3gpp2() { + // MTK-END + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + return (PHONE_TYPE_CDMA == activePhone); + } + + /** + * 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). + */ + // MTK-START + // Modification for sub class + protected final class SmsBroadcastReceiver extends BroadcastReceiver { + // MTK-END + @UnsupportedAppUsage + private final String mDeleteWhere; + @UnsupportedAppUsage + private final String[] mDeleteWhereArgs; + private long mBroadcastTimeNano; + + // MTK-START + // Modification for sub class + public SmsBroadcastReceiver(InboundSmsTracker tracker) { + // MTK-END + mDeleteWhere = tracker.getDeleteWhere(); + mDeleteWhereArgs = tracker.getDeleteWhereArgs(); + mBroadcastTimeNano = System.nanoTime(); + } + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + 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 */); + + 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, + "mms-broadcast"); + BroadcastOptions bopts = BroadcastOptions.makeBasic(); + bopts.setTemporaryAppWhitelistDuration(duration); + Bundle options = bopts.toBundle(); + + String mimeType = intent.getType(); + 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.DATA_SMS_RECEIVED_ACTION.equals(action) + && !Intents.WAP_PUSH_RECEIVED_ACTION.equals(action)) { + loge("unexpected BroadcastReceiver action: " + action); + } + + 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); + sendMessage(EVENT_BROADCAST_COMPLETE); + + int durationMillis = (int) ((System.nanoTime() - mBroadcastTimeNano) / 1000000); + if (durationMillis >= 5000) { + loge("Slow ordered broadcast completion time: " + durationMillis + " ms"); + } else if (DBG) { + log("ordered broadcast completed in: " + durationMillis + " ms"); + } + } + } + } + + /** + * Callback that handles filtering results by carrier services. + */ + // MTK-START + // Modification for sub class + public final class CarrierServicesSmsFilterCallback implements + CarrierServicesSmsFilter.CarrierServicesSmsFilterCallbackInterface { + // MTK-END + private final byte[][] mPdus; + private final int mDestPort; + private final String mSmsFormat; + private final SmsBroadcastReceiver mSmsBroadcastReceiver; + private final boolean mUserUnlocked; + private final boolean mIsClass0; + private final int mSubId; + private final long mMessageId; + + // MTK-START + // Modification for sub class + public CarrierServicesSmsFilterCallback(byte[][] pdus, int destPort, String smsFormat, + SmsBroadcastReceiver smsBroadcastReceiver, boolean userUnlocked, + boolean isClass0, int subId, long messageId) { + // MTK-END + mPdus = pdus; + mDestPort = destPort; + mSmsFormat = smsFormat; + mSmsBroadcastReceiver = smsBroadcastReceiver; + mUserUnlocked = userUnlocked; + mIsClass0 = isClass0; + mSubId = subId; + mMessageId = messageId; + } + + @Override + public void onFilterComplete(int result) { + log("onFilterComplete: result is " + result, mMessageId); + if ((result & CarrierMessagingService.RECEIVE_OPTIONS_DROP) == 0) { + if (VisualVoicemailSmsFilter.filter(mContext, mPdus, + mSmsFormat, mDestPort, mSubId)) { + logWithLocalLog("Visual voicemail SMS dropped", mMessageId); + dropSms(mSmsBroadcastReceiver); + return; + } + + 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); + } + } else { + // Drop this SMS. + dropSms(mSmsBroadcastReceiver); + } + } + } + + 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 + 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 + ", id: " + 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 + ", id: " + id); + } + + /** + * Log with debug level. + * @param s the string to log + */ + @UnsupportedAppUsage + @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 + ", id: " + id); + } + + /** + * Log with error level. + * @param s the string to log + */ + @UnsupportedAppUsage + @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 + ", id: " + 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); + } + + /** + * Store a received SMS into Telephony provider + * + * @param intent The intent containing the received SMS + * @return The URI of written message + */ + // MTK-START + // Modification for sub class + @UnsupportedAppUsage + protected Uri writeInboxMessage(Intent intent) { + // MTK-END + final SmsMessage[] messages = Telephony.Sms.Intents.getMessagesFromIntent(intent); + if (messages == null || messages.length < 1) { + loge("Failed to parse SMS pdu"); + return null; + } + // Sometimes, SmsMessage is null if it can’t be parsed correctly. + for (final SmsMessage sms : messages) { + if (sms == null) { + loge("Can’t write null SmsMessage"); + return null; + } + } + final ContentValues values = parseSmsMessage(messages); + final long identity = Binder.clearCallingIdentity(); + try { + return mContext.getContentResolver().insert(Telephony.Sms.Inbox.CONTENT_URI, values); + } catch (Exception e) { + loge("Failed to persist inbox message", e); + } finally { + Binder.restoreCallingIdentity(identity); + } + return null; + } + + /** + * Convert SmsMessage[] into SMS database schema columns + * + * @param msgs The SmsMessage array of the received SMS + * @return ContentValues representing the columns of parsed SMS + */ + private static ContentValues parseSmsMessage(SmsMessage[] msgs) { + final SmsMessage sms = msgs[0]; + final ContentValues values = new ContentValues(); + values.put(Telephony.Sms.Inbox.ADDRESS, sms.getDisplayOriginatingAddress()); + values.put(Telephony.Sms.Inbox.BODY, buildMessageBodyFromPdus(msgs)); + values.put(Telephony.Sms.Inbox.DATE_SENT, sms.getTimestampMillis()); + values.put(Telephony.Sms.Inbox.DATE, System.currentTimeMillis()); + values.put(Telephony.Sms.Inbox.PROTOCOL, sms.getProtocolIdentifier()); + values.put(Telephony.Sms.Inbox.SEEN, 0); + values.put(Telephony.Sms.Inbox.READ, 0); + final String subject = sms.getPseudoSubject(); + if (!TextUtils.isEmpty(subject)) { + values.put(Telephony.Sms.Inbox.SUBJECT, subject); + } + values.put(Telephony.Sms.Inbox.REPLY_PATH_PRESENT, sms.isReplyPathPresent() ? 1 : 0); + values.put(Telephony.Sms.Inbox.SERVICE_CENTER, sms.getServiceCenterAddress()); + return values; + } + + /** + * 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; + } + + /** + * 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); + } + + 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); + } + } + } + + // MTK-START + /** + * A hook function to change the query where arguments. + * + * @param whereArgs the old where argumtents + * + * @return the modified where arguments + */ + protected String[] onModifyQueryWhereArgs(String[] whereArgs) { + return whereArgs; + } + + /** + * A hook function to stop the flow of processing message parts. + * + * @param pdus the SMS pdu array + * @param format the SMS format + * @param tracker the InboundSmsTracker object + * + * @return true if stop processing + */ + protected boolean onCheckIfStopProcessMessagePart(byte[][] pdus, String format, + InboundSmsTracker tracker) { + return false; + } + + /** + * A hook function to create SmsMessage. + * + * @param pdu the SMS pdu + * @param format the SMS format + * + * @return SmsMessage Object + */ + protected SmsMessage onCreateSmsMessage(byte[] pdu, String format) { + return SmsMessage.createFromPdu(pdu, SmsConstants.FORMAT_3GPP); + } + + /** + * A hook function to dispatch wap push PDU. + * + * @param smsPdus SMS pdu array + * @param pdu The WAP PDU, made up of one or more SMS PDUs + * @param receiver The broadcast receiver + * @param address The originating address + * @param subId The sub Id + * @return a result code from {@link android.provider.Telephony.Sms.Intents}, or + * {@link Activity#RESULT_OK} if the message has been broadcast + * to applications + */ + protected int onDispatchWapPdu(byte[][] smsPdus, + byte[] pdu, BroadcastReceiver receiver, String address, int subId, + long messageId) { + return mWapPush.dispatchWapPdu(pdu, receiver, this, address, subId, messageId); + } + + /** + * A hook function to check if override the States. + * + * @return true if override the States + */ + protected boolean onCheckIfOverrideStates() { + return false; + } + // MTK-END +}