1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.telephony; 18 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.database.Cursor; 22 import android.database.SQLException; 23 import android.net.Uri; 24 import android.provider.Telephony; 25 import android.telephony.Rlog; 26 27 import com.android.internal.telephony.cdma.CdmaInboundSmsHandler; 28 import com.android.internal.telephony.gsm.GsmInboundSmsHandler; 29 30 import java.util.HashMap; 31 import java.util.HashSet; 32 33 /** 34 * Called at boot time to clean out the raw table, collecting all acknowledged messages and 35 * deleting any partial message segments older than 30 days. Called from a worker thread to 36 * avoid delaying phone app startup. The last step is to broadcast the first pending message 37 * from the main thread, then the remaining pending messages will be broadcast after the 38 * previous ordered broadcast completes. 39 */ 40 public class SmsBroadcastUndelivered implements Runnable { 41 private static final String TAG = "SmsBroadcastUndelivered"; 42 private static final boolean DBG = InboundSmsHandler.DBG; 43 44 /** Delete any partial message segments older than 30 days. */ 45 static final long PARTIAL_SEGMENT_EXPIRE_AGE = (long) (60 * 60 * 1000) * 24 * 30; 46 47 /** 48 * Query projection for dispatching pending messages at boot time. 49 * Column order must match the {@code *_COLUMN} constants in {@link InboundSmsHandler}. 50 */ 51 private static final String[] PDU_PENDING_MESSAGE_PROJECTION = { 52 "pdu", 53 "sequence", 54 "destination_port", 55 "date", 56 "reference_number", 57 "count", 58 "address", 59 "_id" 60 }; 61 62 /** URI for raw table from SmsProvider. */ 63 private static final Uri sRawUri = Uri.withAppendedPath(Telephony.Sms.CONTENT_URI, "raw"); 64 65 /** Content resolver to use to access raw table from SmsProvider. */ 66 private final ContentResolver mResolver; 67 68 /** Handler for 3GPP-format messages (may be null). */ 69 private final GsmInboundSmsHandler mGsmInboundSmsHandler; 70 71 /** Handler for 3GPP2-format messages (may be null). */ 72 private final CdmaInboundSmsHandler mCdmaInboundSmsHandler; 73 74 public SmsBroadcastUndelivered(Context context, GsmInboundSmsHandler gsmInboundSmsHandler, 75 CdmaInboundSmsHandler cdmaInboundSmsHandler) { 76 mResolver = context.getContentResolver(); 77 mGsmInboundSmsHandler = gsmInboundSmsHandler; 78 mCdmaInboundSmsHandler = cdmaInboundSmsHandler; 79 } 80 81 @Override 82 public void run() { 83 if (DBG) Rlog.d(TAG, "scanning raw table for undelivered messages"); 84 scanRawTable(); 85 // tell handlers to start processing new messages 86 if (mGsmInboundSmsHandler != null) { 87 mGsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS); 88 } 89 if (mCdmaInboundSmsHandler != null) { 90 mCdmaInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS); 91 } 92 } 93 94 /** 95 * Scan the raw table for complete SMS messages to broadcast, and old PDUs to delete. 96 */ 97 private void scanRawTable() { 98 long startTime = System.nanoTime(); 99 HashMap<SmsReferenceKey, Integer> multiPartReceivedCount = 100 new HashMap<SmsReferenceKey, Integer>(4); 101 HashSet<SmsReferenceKey> oldMultiPartMessages = new HashSet<SmsReferenceKey>(4); 102 Cursor cursor = null; 103 try { 104 cursor = mResolver.query(sRawUri, PDU_PENDING_MESSAGE_PROJECTION, null, null, null); 105 if (cursor == null) { 106 Rlog.e(TAG, "error getting pending message cursor"); 107 return; 108 } 109 110 boolean isCurrentFormat3gpp2 = InboundSmsHandler.isCurrentFormat3gpp2(); 111 while (cursor.moveToNext()) { 112 InboundSmsTracker tracker; 113 try { 114 tracker = new InboundSmsTracker(cursor, isCurrentFormat3gpp2); 115 } catch (IllegalArgumentException e) { 116 Rlog.e(TAG, "error loading SmsTracker: " + e); 117 continue; 118 } 119 120 if (tracker.getMessageCount() == 1) { 121 // deliver single-part message 122 broadcastSms(tracker); 123 } else { 124 SmsReferenceKey reference = new SmsReferenceKey(tracker); 125 Integer receivedCount = multiPartReceivedCount.get(reference); 126 if (receivedCount == null) { 127 multiPartReceivedCount.put(reference, 1); // first segment seen 128 if (tracker.getTimestamp() < 129 (System.currentTimeMillis() - PARTIAL_SEGMENT_EXPIRE_AGE)) { 130 // older than 30 days; delete if we don't find all the segments 131 oldMultiPartMessages.add(reference); 132 } 133 } else { 134 int newCount = receivedCount + 1; 135 if (newCount == tracker.getMessageCount()) { 136 // looks like we've got all the pieces; send a single tracker 137 // to state machine which will find the other pieces to broadcast 138 if (DBG) Rlog.d(TAG, "found complete multi-part message"); 139 broadcastSms(tracker); 140 // don't delete this old message until after we broadcast it 141 oldMultiPartMessages.remove(reference); 142 } else { 143 multiPartReceivedCount.put(reference, newCount); 144 } 145 } 146 } 147 } 148 // Delete old incomplete message segments 149 for (SmsReferenceKey message : oldMultiPartMessages) { 150 int rows = mResolver.delete(sRawUri, InboundSmsHandler.SELECT_BY_REFERENCE, 151 message.getDeleteWhereArgs()); 152 if (rows == 0) { 153 Rlog.e(TAG, "No rows were deleted from raw table!"); 154 } else if (DBG) { 155 Rlog.d(TAG, "Deleted " + rows + " rows from raw table for incomplete " 156 + message.mMessageCount + " part message"); 157 } 158 } 159 } catch (SQLException e) { 160 Rlog.e(TAG, "error reading pending SMS messages", e); 161 } finally { 162 if (cursor != null) { 163 cursor.close(); 164 } 165 if (DBG) Rlog.d(TAG, "finished scanning raw table in " 166 + ((System.nanoTime() - startTime) / 1000000) + " ms"); 167 } 168 } 169 170 /** 171 * Send tracker to appropriate (3GPP or 3GPP2) inbound SMS handler for broadcast. 172 */ 173 private void broadcastSms(InboundSmsTracker tracker) { 174 InboundSmsHandler handler; 175 if (tracker.is3gpp2()) { 176 handler = mCdmaInboundSmsHandler; 177 } else { 178 handler = mGsmInboundSmsHandler; 179 } 180 if (handler != null) { 181 handler.sendMessage(InboundSmsHandler.EVENT_BROADCAST_SMS, tracker); 182 } else { 183 Rlog.e(TAG, "null handler for " + tracker.getFormat() + " format, can't deliver."); 184 } 185 } 186 187 /** 188 * Used as the HashMap key for matching concatenated message segments. 189 */ 190 private static class SmsReferenceKey { 191 final String mAddress; 192 final int mReferenceNumber; 193 final int mMessageCount; 194 195 SmsReferenceKey(InboundSmsTracker tracker) { 196 mAddress = tracker.getAddress(); 197 mReferenceNumber = tracker.getReferenceNumber(); 198 mMessageCount = tracker.getMessageCount(); 199 } 200 201 String[] getDeleteWhereArgs() { 202 return new String[]{mAddress, Integer.toString(mReferenceNumber), 203 Integer.toString(mMessageCount)}; 204 } 205 206 @Override 207 public int hashCode() { 208 return ((mReferenceNumber * 31) + mMessageCount) * 31 + mAddress.hashCode(); 209 } 210 211 @Override 212 public boolean equals(Object o) { 213 if (o instanceof SmsReferenceKey) { 214 SmsReferenceKey other = (SmsReferenceKey) o; 215 return other.mAddress.equals(mAddress) 216 && (other.mReferenceNumber == mReferenceNumber) 217 && (other.mMessageCount == mMessageCount); 218 } 219 return false; 220 } 221 } 222 } 223