/* * 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.ContentResolver; import android.content.Context; import android.database.Cursor; import android.database.SQLException; import android.net.Uri; import android.provider.Telephony; 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 at boot time to clean out the raw table, 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 implements Runnable { 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" }; /** URI for raw table from SmsProvider. */ private static final Uri sRawUri = Uri.withAppendedPath(Telephony.Sms.CONTENT_URI, "raw"); /** 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; public SmsBroadcastUndelivered(Context context, GsmInboundSmsHandler gsmInboundSmsHandler, CdmaInboundSmsHandler cdmaInboundSmsHandler) { mResolver = context.getContentResolver(); mGsmInboundSmsHandler = gsmInboundSmsHandler; mCdmaInboundSmsHandler = cdmaInboundSmsHandler; } @Override public void run() { if (DBG) Rlog.d(TAG, "scanning raw table for undelivered messages"); scanRawTable(); // tell handlers to start processing new messages if (mGsmInboundSmsHandler != null) { mGsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS); } if (mCdmaInboundSmsHandler != null) { mCdmaInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS); } } /** * Scan the raw table for complete SMS messages to broadcast, and old PDUs to delete. */ private void scanRawTable() { long startTime = System.nanoTime(); HashMap multiPartReceivedCount = new HashMap(4); HashSet oldMultiPartMessages = new HashSet(4); Cursor cursor = null; try { cursor = mResolver.query(sRawUri, PDU_PENDING_MESSAGE_PROJECTION, null, 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 = new InboundSmsTracker(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) { int rows = mResolver.delete(sRawUri, InboundSmsHandler.SELECT_BY_REFERENCE, 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; SmsReferenceKey(InboundSmsTracker tracker) { mAddress = tracker.getAddress(); mReferenceNumber = tracker.getReferenceNumber(); mMessageCount = tracker.getMessageCount(); } String[] getDeleteWhereArgs() { return new String[]{mAddress, Integer.toString(mReferenceNumber), Integer.toString(mMessageCount)}; } @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; } } }