blob: 5470629b6cf716c3d62a0734db50a63189754b63 [file] [log] [blame]
/*
* 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<SmsReferenceKey, Integer> multiPartReceivedCount =
new HashMap<SmsReferenceKey, Integer>(4);
HashSet<SmsReferenceKey> oldMultiPartMessages = new HashSet<SmsReferenceKey>(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;
}
}
}