/* * 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 android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.database.SQLException; import android.os.UserHandle; import android.os.UserManager; import android.telephony.Rlog; import com.android.internal.telephony.cdma.CdmaInboundSmsHandler; import com.android.internal.telephony.gsm.GsmInboundSmsHandler; import java.util.HashMap; import java.util.HashSet; /** * Called when the credential-encrypted storage is unlocked, collecting all acknowledged messages * and deleting any partial message segments older than 30 days. Called from a worker thread to * avoid delaying phone app startup. The last step is to broadcast the first pending message from * the main thread, then the remaining pending messages will be broadcast after the previous * ordered broadcast completes. */ public class SmsBroadcastUndelivered { private static final String TAG = "SmsBroadcastUndelivered"; private static final boolean DBG = InboundSmsHandler.DBG; /** Delete any partial message segments older than 30 days. */ static final long PARTIAL_SEGMENT_EXPIRE_AGE = (long) (60 * 60 * 1000) * 24 * 30; /** * Query projection for dispatching pending messages at boot time. * Column order must match the {@code *_COLUMN} constants in {@link InboundSmsHandler}. */ private static final String[] PDU_PENDING_MESSAGE_PROJECTION = { "pdu", "sequence", "destination_port", "date", "reference_number", "count", "address", "_id", "message_body", "display_originating_addr" }; private static SmsBroadcastUndelivered instance; /** Content resolver to use to access raw table from SmsProvider. */ private final ContentResolver mResolver; /** Handler for 3GPP-format messages (may be null). */ private final GsmInboundSmsHandler mGsmInboundSmsHandler; /** Handler for 3GPP2-format messages (may be null). */ private final CdmaInboundSmsHandler mCdmaInboundSmsHandler; /** Broadcast receiver that processes the raw table when the user unlocks the phone for the * first time after reboot and the credential-encrypted storage is available. */ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, Intent intent) { Rlog.d(TAG, "Received broadcast " + intent.getAction()); if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) { new ScanRawTableThread(context).start(); } } }; private class ScanRawTableThread extends Thread { private final Context context; private ScanRawTableThread(Context context) { this.context = context; } @Override public void run() { scanRawTable(); InboundSmsHandler.cancelNewMessageNotification(context); } } public static void initialize(Context context, GsmInboundSmsHandler gsmInboundSmsHandler, CdmaInboundSmsHandler cdmaInboundSmsHandler) { if (instance == null) { instance = new SmsBroadcastUndelivered( context, gsmInboundSmsHandler, cdmaInboundSmsHandler); } // Tell handlers to start processing new messages and transit from the startup state to the // idle state. This method may be called multiple times for multi-sim devices. We must make // sure the state transition happen to all inbound sms handlers. if (gsmInboundSmsHandler != null) { gsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS); } if (cdmaInboundSmsHandler != null) { cdmaInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS); } } private SmsBroadcastUndelivered(Context context, GsmInboundSmsHandler gsmInboundSmsHandler, CdmaInboundSmsHandler cdmaInboundSmsHandler) { mResolver = context.getContentResolver(); mGsmInboundSmsHandler = gsmInboundSmsHandler; mCdmaInboundSmsHandler = cdmaInboundSmsHandler; UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); if (userManager.isUserUnlocked()) { new ScanRawTableThread(context).start(); } else { IntentFilter userFilter = new IntentFilter(); userFilter.addAction(Intent.ACTION_USER_UNLOCKED); context.registerReceiver(mBroadcastReceiver, userFilter); } } /** * Scan the raw table for complete SMS messages to broadcast, and old PDUs to delete. */ private void scanRawTable() { if (DBG) Rlog.d(TAG, "scanning raw table for undelivered messages"); long startTime = System.nanoTime(); HashMap multiPartReceivedCount = new HashMap(4); HashSet oldMultiPartMessages = new HashSet(4); Cursor cursor = null; try { // query only non-deleted ones cursor = mResolver.query(InboundSmsHandler.sRawUri, PDU_PENDING_MESSAGE_PROJECTION, "deleted = 0", null, null); if (cursor == null) { Rlog.e(TAG, "error getting pending message cursor"); return; } boolean isCurrentFormat3gpp2 = InboundSmsHandler.isCurrentFormat3gpp2(); while (cursor.moveToNext()) { InboundSmsTracker tracker; try { tracker = TelephonyComponentFactory.getInstance().makeInboundSmsTracker(cursor, isCurrentFormat3gpp2); } catch (IllegalArgumentException e) { Rlog.e(TAG, "error loading SmsTracker: " + e); continue; } if (tracker.getMessageCount() == 1) { // deliver single-part message broadcastSms(tracker); } else { SmsReferenceKey reference = new SmsReferenceKey(tracker); Integer receivedCount = multiPartReceivedCount.get(reference); if (receivedCount == null) { multiPartReceivedCount.put(reference, 1); // first segment seen if (tracker.getTimestamp() < (System.currentTimeMillis() - PARTIAL_SEGMENT_EXPIRE_AGE)) { // older than 30 days; delete if we don't find all the segments oldMultiPartMessages.add(reference); } } else { int newCount = receivedCount + 1; if (newCount == tracker.getMessageCount()) { // looks like we've got all the pieces; send a single tracker // to state machine which will find the other pieces to broadcast if (DBG) Rlog.d(TAG, "found complete multi-part message"); broadcastSms(tracker); // don't delete this old message until after we broadcast it oldMultiPartMessages.remove(reference); } else { multiPartReceivedCount.put(reference, newCount); } } } } // Delete old incomplete message segments for (SmsReferenceKey message : oldMultiPartMessages) { // delete permanently int rows = mResolver.delete(InboundSmsHandler.sRawUriPermanentDelete, message.getDeleteWhere(), message.getDeleteWhereArgs()); if (rows == 0) { Rlog.e(TAG, "No rows were deleted from raw table!"); } else if (DBG) { Rlog.d(TAG, "Deleted " + rows + " rows from raw table for incomplete " + message.mMessageCount + " part message"); } } } catch (SQLException e) { Rlog.e(TAG, "error reading pending SMS messages", e); } finally { if (cursor != null) { cursor.close(); } if (DBG) Rlog.d(TAG, "finished scanning raw table in " + ((System.nanoTime() - startTime) / 1000000) + " ms"); } } /** * Send tracker to appropriate (3GPP or 3GPP2) inbound SMS handler for broadcast. */ private void broadcastSms(InboundSmsTracker tracker) { InboundSmsHandler handler; if (tracker.is3gpp2()) { handler = mCdmaInboundSmsHandler; } else { handler = mGsmInboundSmsHandler; } if (handler != null) { handler.sendMessage(InboundSmsHandler.EVENT_BROADCAST_SMS, tracker); } else { Rlog.e(TAG, "null handler for " + tracker.getFormat() + " format, can't deliver."); } } /** * Used as the HashMap key for matching concatenated message segments. */ private static class SmsReferenceKey { final String mAddress; final int mReferenceNumber; final int mMessageCount; final String mQuery; SmsReferenceKey(InboundSmsTracker tracker) { mAddress = tracker.getAddress(); mReferenceNumber = tracker.getReferenceNumber(); mMessageCount = tracker.getMessageCount(); mQuery = tracker.getQueryForSegments(); } String[] getDeleteWhereArgs() { return new String[]{mAddress, Integer.toString(mReferenceNumber), Integer.toString(mMessageCount)}; } String getDeleteWhere() { return mQuery; } @Override public int hashCode() { return ((mReferenceNumber * 31) + mMessageCount) * 31 + mAddress.hashCode(); } @Override public boolean equals(Object o) { if (o instanceof SmsReferenceKey) { SmsReferenceKey other = (SmsReferenceKey) o; return other.mAddress.equals(mAddress) && (other.mReferenceNumber == mReferenceNumber) && (other.mMessageCount == mMessageCount); } return false; } } }