/* * 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.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.ActivityManager; import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; 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.UserInfo; import android.database.Cursor; import android.database.SQLException; import android.net.Uri; import android.os.AsyncResult; import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IDeviceIdleController; import android.os.Message; import android.os.PowerManager; 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.Rlog; import android.telephony.SmsManager; import android.telephony.SmsMessage; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.util.NotificationChannelController; import com.android.internal.util.HexDump; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import java.io.ByteArrayOutputStream; 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;
private static final boolean VDBG = false; // STOPSHIP if true, logs user data
/** Query projection for checking for duplicate message segments. */
private static final String[] PDU_PROJECTION = {
"pdu"
};
/** Query projection for combining concatenated message segments. */
private static final String[] PDU_SEQUENCE_PORT_PROJECTION = {
"pdu",
"sequence",
"destination_port",
"display_originating_addr"
};
/** Mapping from DB COLUMN to PDU_SEQUENCE_PORT PROJECTION index */
private static final Map 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) {
log("Credential-encrypted storage not available. Port: " + destPort);
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;
}
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);
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.
*/
private boolean filterSms(byte[][] pdus, int destPort,
InboundSmsTracker tracker, SmsBroadcastReceiver resultReceiver, boolean userUnlocked) {
CarrierServicesSmsFilterCallback filterCallback =
new CarrierServicesSmsFilterCallback(
pdus, destPort, tracker.getFormat(), resultReceiver, userUnlocked);
CarrierServicesSmsFilter carrierServicesFilter = new CarrierServicesSmsFilter(
mContext, mPhone, pdus, destPort, tracker.getFormat(), filterCallback, getName());
if (carrierServicesFilter.filter()) {
return true;
}
if (VisualVoicemailSmsFilter.filter(
mContext, pdus, tracker.getFormat(), destPort, mPhone.getSubId())) {
log("Visual voicemail SMS dropped");
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
*/
public void dispatchIntent(Intent intent, String permission, int appOp,
Bundle opts, BroadcastReceiver resultReceiver, UserHandle user) {
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());
if (user.equals(UserHandle.ALL)) {
// Get a list of currently started users.
int[] users = null;
try {
users = ActivityManager.getService().getRunningUserIds();
} catch (RemoteException re) {
}
if (users == null) {
users = new int[] {user.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 = new UserHandle(users[i]);
if (users[i] != UserHandle.USER_SYSTEM) {
// Is the user not allowed to use SMS?
if (mUserManager.hasUserRestriction(UserManager.DISALLOW_SMS, targetUser)) {
continue;
}
// Skip unknown users and managed profiles as well
UserInfo info = mUserManager.getUserInfo(users[i]);
if (info == null || info.isManagedProfile()) {
continue;
}
}
// Only pass in the resultReceiver when the USER_SYSTEM is processed.
mContext.sendOrderedBroadcastAsUser(intent, targetUser, permission, appOp, opts,
users[i] == UserHandle.USER_SYSTEM ? resultReceiver : null,
getHandler(), Activity.RESULT_OK, null, null);
}
} else {
mContext.sendOrderedBroadcastAsUser(intent, user, permission, appOp, opts,
resultReceiver, getHandler(), Activity.RESULT_OK, null, null);
}
}
/**
* Helper for {@link SmsBroadcastUndelivered} to delete an old message in the raw table.
*/
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.");
}
}
private Bundle handleSmsWhitelisting(ComponentName target) {
String pkgName;
String reason;
if (target != null) {
pkgName = target.getPackageName();
reason = "sms-app";
} else {
pkgName = mContext.getPackageName();
reason = "sms-broadcast";
}
try {
long duration = mDeviceIdleController.addPowerSaveTempWhitelistAppForSms(
pkgName, 0, reason);
BroadcastOptions bopts = BroadcastOptions.makeBasic();
bopts.setTemporaryAppWhitelistDuration(duration);
return bopts.toBundle();
} catch (RemoteException e) {
}
return null;
}
/**
* 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) {
Intent intent = new Intent();
intent.putExtra("pdus", pdus);
intent.putExtra("format", format);
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);
log("Delivering SMS to: " + componentName.getPackageName() +
" " + componentName.getClassName());
} else {
intent.setComponent(null);
}
// TODO: Validate that this is the right place to store the SMS.
if (SmsManager.getDefault().getAutoPersisting()) {
final Uri uri = writeInboxMessage(intent);
if (uri != null) {
// Pass this to SMS apps so that they know where it is stored
intent.putExtra("uri", uri.toString());
}
}
// 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);
// Allow registered broadcast receivers to get this intent even
// when they are in the background.
intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
}
Bundle options = handleSmsWhitelisting(intent.getComponent());
dispatchIntent(intent, android.Manifest.permission.RECEIVE_SMS,
AppOpsManager.OP_RECEIVE_SMS, options, resultReceiver, UserHandle.SYSTEM);
}
/**
* Function to check if message should be dropped because same message has already been
* received. In certain cases it checks for similar messages instead of exact same (cases where
* keeping both messages in db can cause ambiguity)
* @return true if duplicate exists, false otherwise
*/
private boolean duplicateExists(InboundSmsTracker tracker) throws SQLException {
String address = tracker.getAddress();
// convert to strings for query
String refNumber = Integer.toString(tracker.getReferenceNumber());
String count = Integer.toString(tracker.getMessageCount());
// sequence numbers are 1-based except for CDMA WAP, which is 0-based
int sequence = tracker.getSequenceNumber();
String seqNumber = Integer.toString(sequence);
String date = Long.toString(tracker.getTimestamp());
String messageBody = tracker.getMessageBody();
String where;
if (tracker.getMessageCount() == 1) {
where = "address=? AND reference_number=? AND count=? AND sequence=? AND " +
"date=? AND message_body=?";
} else {
// for multi-part messages, deduping should also be done against undeleted
// segments that can cause ambiguity when contacenating the segments, that is,
// segments with same address, reference_number, count, sequence and message type.
where = tracker.getQueryForMultiPartDuplicates();
}
Cursor cursor = null;
try {
// Check for duplicate message segments
cursor = mResolver.query(sRawUri, PDU_PROJECTION, where,
new String[]{address, refNumber, count, seqNumber, date, messageBody},
null);
// moveToNext() returns false if no duplicates were found
if (cursor != null && cursor.moveToNext()) {
loge("Discarding duplicate message segment, refNumber=" + refNumber
+ " seqNumber=" + seqNumber + " count=" + count);
if (VDBG) {
loge("address=" + address + " date=" + date + " messageBody=" +
messageBody);
}
String oldPduString = cursor.getString(PDU_COLUMN);
byte[] pdu = tracker.getPdu();
byte[] oldPdu = HexDump.hexStringToByteArray(oldPduString);
if (!Arrays.equals(oldPdu, tracker.getPdu())) {
loge("Warning: dup message segment PDU of length " + pdu.length
+ " is different from existing PDU of length " + oldPdu.length);
}
return true; // reject message
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return false;
}
/**
* 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 (duplicateExists(tracker)) {
return Intents.RESULT_SMS_DUPLICATED; // reject message
}
} catch (SQLException e) {
loge("Can't access SMS database", e);
return Intents.RESULT_SMS_GENERIC_ERROR; // reject message
}
} else {
logd("Skipped message de-duping logic");
}
String address = tracker.getAddress();
String refNumber = Integer.toString(tracker.getReferenceNumber());
String count = Integer.toString(tracker.getMessageCount());
ContentValues values = tracker.getContentValues();
if (VDBG) log("adding content values to raw table: " + values.toString());
Uri newUri = mResolver.insert(sRawUri, values);
if (DBG) log("URI of new row -> " + newUri);
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("error parsing URI for new row: " + newUri, e);
return Intents.RESULT_SMS_GENERIC_ERROR;
}
}
/**
* 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);
}
/**
* 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).
*/
private final class SmsBroadcastReceiver extends BroadcastReceiver {
private final String mDeleteWhere;
private final String[] mDeleteWhereArgs;
private long mBroadcastTimeNano;
SmsBroadcastReceiver(InboundSmsTracker tracker) {
mDeleteWhere = tracker.getDeleteWhere();
mDeleteWhereArgs = tracker.getDeleteWhereArgs();
mBroadcastTimeNano = System.nanoTime();
}
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
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.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
intent.setComponent(null);
// All running users will be notified of the received sms.
Bundle options = handleSmsWhitelisting(null);
dispatchIntent(intent, android.Manifest.permission.RECEIVE_SMS,
AppOpsManager.OP_RECEIVE_SMS,
options, this, UserHandle.ALL);
} 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);
// Allow registered broadcast receivers to get this intent even
// when they are in the background.
intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
// Only the primary user will receive notification of incoming mms.
// That app will do the actual downloading of the mms.
Bundle options = null;
try {
long duration = mDeviceIdleController.addPowerSaveTempWhitelistAppForMms(
mContext.getPackageName(), 0, "mms-broadcast");
BroadcastOptions bopts = BroadcastOptions.makeBasic();
bopts.setTemporaryAppWhitelistDuration(duration);
options = bopts.toBundle();
} catch (RemoteException e) {
}
String mimeType = intent.getType();
dispatchIntent(intent, WapPushOverSms.getPermissionForType(mimeType),
WapPushOverSms.getAppOpsPermissionForIntent(mimeType), options, this,
UserHandle.SYSTEM);
} 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.
*/
private final class CarrierServicesSmsFilterCallback implements
CarrierServicesSmsFilter.CarrierServicesSmsFilterCallbackInterface {
private final byte[][] mPdus;
private final int mDestPort;
private final String mSmsFormat;
private final SmsBroadcastReceiver mSmsBroadcastReceiver;
private final boolean mUserUnlocked;
CarrierServicesSmsFilterCallback(byte[][] pdus, int destPort, String smsFormat,
SmsBroadcastReceiver smsBroadcastReceiver, boolean userUnlocked) {
mPdus = pdus;
mDestPort = destPort;
mSmsFormat = smsFormat;
mSmsBroadcastReceiver = smsBroadcastReceiver;
mUserUnlocked = userUnlocked;
}
@Override
public void onFilterComplete(int result) {
logv("onFilterComplete: result is " + result);
if ((result & CarrierMessagingService.RECEIVE_OPTIONS_DROP) == 0) {
if (VisualVoicemailSmsFilter.filter(mContext, mPdus,
mSmsFormat, mDestPort, mPhone.getSubId())) {
log("Visual voicemail SMS dropped");
dropSms(mSmsBroadcastReceiver);
return;
}
if (mUserUnlocked) {
dispatchSmsDeliveryIntent(
mPdus, mSmsFormat, mDestPort, mSmsBroadcastReceiver);
} 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.
*/
private boolean isSkipNotifyFlagSet(int callbackResult) {
return (callbackResult
& RECEIVE_OPTIONS_SKIP_NOTIFY_WHEN_CREDENTIAL_PROTECTED_STORAGE_UNAVAILABLE) > 0;
}
/**
* Log with debug level.
* @param s the string to log
*/
@Override
protected void log(String s) {
Rlog.d(getName(), s);
}
/**
* Log with error level.
* @param s the string to log
*/
@Override
protected void loge(String s) {
Rlog.e(getName(), s);
}
/**
* 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
*/
private Uri writeInboxMessage(Intent intent) {
final SmsMessage[] messages = Telephony.Sms.Intents.getMessagesFromIntent(intent);
if (messages == null || messages.length < 1) {
loge("Failed to parse SMS pdu");
return null;
}
// Sometimes, SmsMessage.mWrappedSmsMessage is null causing NPE when we access
// the methods on it although the SmsMessage itself is not null. So do this check
// before we do anything on the parsed SmsMessages.
for (final SmsMessage sms : messages) {
try {
sms.getDisplayMessageBody();
} catch (NullPointerException e) {
loge("NPE inside 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());
}
}
// 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 WAKELOCK_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())) {
context.startActivity(context.getPackageManager().getLaunchIntentForPackage(
Telephony.Sms.getDefaultSmsPackage(context)));
}
}
}
/**
* 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);
}
}