SmsBroadcastUndelivered.java revision dc7411e9fc8ac029a7bad8b85b0f24642a1774b6
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 17package com.android.internal.telephony; 18 19import android.content.BroadcastReceiver; 20import android.content.ContentResolver; 21import android.content.Context; 22import android.content.Intent; 23import android.content.IntentFilter; 24import android.database.Cursor; 25import android.database.SQLException; 26import android.net.Uri; 27import android.os.UserHandle; 28import android.provider.Telephony; 29import android.telephony.Rlog; 30 31import com.android.internal.telephony.cdma.CdmaInboundSmsHandler; 32import com.android.internal.telephony.gsm.GsmInboundSmsHandler; 33 34import java.util.HashMap; 35import java.util.HashSet; 36 37/** 38 * Called when the credential-encrypted storage is unlocked, collecting all acknowledged messages 39 * and deleting any partial message segments older than 30 days. Called from a worker thread to 40 * avoid delaying phone app startup. The last step is to broadcast the first pending message from 41 * the main thread, then the remaining pending messages will be broadcast after the previous 42 * ordered broadcast completes. 43 */ 44public class SmsBroadcastUndelivered { 45 private static final String TAG = "SmsBroadcastUndelivered"; 46 private static final boolean DBG = InboundSmsHandler.DBG; 47 48 /** Delete any partial message segments older than 30 days. */ 49 static final long PARTIAL_SEGMENT_EXPIRE_AGE = (long) (60 * 60 * 1000) * 24 * 30; 50 51 /** 52 * Query projection for dispatching pending messages at boot time. 53 * Column order must match the {@code *_COLUMN} constants in {@link InboundSmsHandler}. 54 */ 55 private static final String[] PDU_PENDING_MESSAGE_PROJECTION = { 56 "pdu", 57 "sequence", 58 "destination_port", 59 "date", 60 "reference_number", 61 "count", 62 "address", 63 "_id" 64 }; 65 66 /** URI for raw table from SmsProvider. */ 67 private static final Uri sRawUri = Uri.withAppendedPath(Telephony.Sms.CONTENT_URI, "raw"); 68 private static SmsBroadcastUndelivered instance; 69 70 /** Content resolver to use to access raw table from SmsProvider. */ 71 private final ContentResolver mResolver; 72 73 /** Handler for 3GPP-format messages (may be null). */ 74 private final GsmInboundSmsHandler mGsmInboundSmsHandler; 75 76 /** Handler for 3GPP2-format messages (may be null). */ 77 private final CdmaInboundSmsHandler mCdmaInboundSmsHandler; 78 79 /** Broadcast receiver that processes the raw table when the user unlocks the phone for the 80 * first time after reboot and the credential-encrypted storage is available. 81 */ 82 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 83 @Override 84 public void onReceive(Context context, Intent intent) { 85 Rlog.d(TAG, "Received broadcast " + intent.getAction()); 86 if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) { 87 Thread t = new Thread() { 88 @Override 89 public void run() { 90 scanRawTable(); 91 } 92 }; 93 t.start(); 94 } 95 } 96 }; 97 98 public static void initialize(Context context, GsmInboundSmsHandler gsmInboundSmsHandler, 99 CdmaInboundSmsHandler cdmaInboundSmsHandler) { 100 if (instance == null) { 101 instance = new SmsBroadcastUndelivered( 102 context, gsmInboundSmsHandler, cdmaInboundSmsHandler); 103 } 104 105 // Tell handlers to start processing new messages and transit from the startup state to the 106 // idle state. This method may be called multiple times for multi-sim devices. We must make 107 // sure the state transition happen to all inbound sms handlers. 108 if (gsmInboundSmsHandler != null) { 109 gsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS); 110 } 111 if (cdmaInboundSmsHandler != null) { 112 cdmaInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS); 113 } 114 } 115 116 private SmsBroadcastUndelivered(Context context, GsmInboundSmsHandler gsmInboundSmsHandler, 117 CdmaInboundSmsHandler cdmaInboundSmsHandler) { 118 mResolver = context.getContentResolver(); 119 mGsmInboundSmsHandler = gsmInboundSmsHandler; 120 mCdmaInboundSmsHandler = cdmaInboundSmsHandler; 121 122 IntentFilter userFilter = new IntentFilter(); 123 userFilter.addAction(Intent.ACTION_USER_UNLOCKED); 124 context.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, userFilter, null, null); 125 } 126 127 /** 128 * Scan the raw table for complete SMS messages to broadcast, and old PDUs to delete. 129 */ 130 private void scanRawTable() { 131 if (DBG) Rlog.d(TAG, "scanning raw table for undelivered messages"); 132 long startTime = System.nanoTime(); 133 HashMap<SmsReferenceKey, Integer> multiPartReceivedCount = 134 new HashMap<SmsReferenceKey, Integer>(4); 135 HashSet<SmsReferenceKey> oldMultiPartMessages = new HashSet<SmsReferenceKey>(4); 136 Cursor cursor = null; 137 try { 138 cursor = mResolver.query(sRawUri, PDU_PENDING_MESSAGE_PROJECTION, null, null, null); 139 if (cursor == null) { 140 Rlog.e(TAG, "error getting pending message cursor"); 141 return; 142 } 143 144 boolean isCurrentFormat3gpp2 = InboundSmsHandler.isCurrentFormat3gpp2(); 145 while (cursor.moveToNext()) { 146 InboundSmsTracker tracker; 147 try { 148 tracker = new InboundSmsTracker(cursor, isCurrentFormat3gpp2); 149 } catch (IllegalArgumentException e) { 150 Rlog.e(TAG, "error loading SmsTracker: " + e); 151 continue; 152 } 153 154 if (tracker.getMessageCount() == 1) { 155 // deliver single-part message 156 broadcastSms(tracker); 157 } else { 158 SmsReferenceKey reference = new SmsReferenceKey(tracker); 159 Integer receivedCount = multiPartReceivedCount.get(reference); 160 if (receivedCount == null) { 161 multiPartReceivedCount.put(reference, 1); // first segment seen 162 if (tracker.getTimestamp() < 163 (System.currentTimeMillis() - PARTIAL_SEGMENT_EXPIRE_AGE)) { 164 // older than 30 days; delete if we don't find all the segments 165 oldMultiPartMessages.add(reference); 166 } 167 } else { 168 int newCount = receivedCount + 1; 169 if (newCount == tracker.getMessageCount()) { 170 // looks like we've got all the pieces; send a single tracker 171 // to state machine which will find the other pieces to broadcast 172 if (DBG) Rlog.d(TAG, "found complete multi-part message"); 173 broadcastSms(tracker); 174 // don't delete this old message until after we broadcast it 175 oldMultiPartMessages.remove(reference); 176 } else { 177 multiPartReceivedCount.put(reference, newCount); 178 } 179 } 180 } 181 } 182 // Delete old incomplete message segments 183 for (SmsReferenceKey message : oldMultiPartMessages) { 184 int rows = mResolver.delete(sRawUri, InboundSmsHandler.SELECT_BY_REFERENCE, 185 message.getDeleteWhereArgs()); 186 if (rows == 0) { 187 Rlog.e(TAG, "No rows were deleted from raw table!"); 188 } else if (DBG) { 189 Rlog.d(TAG, "Deleted " + rows + " rows from raw table for incomplete " 190 + message.mMessageCount + " part message"); 191 } 192 } 193 } catch (SQLException e) { 194 Rlog.e(TAG, "error reading pending SMS messages", e); 195 } finally { 196 if (cursor != null) { 197 cursor.close(); 198 } 199 if (DBG) Rlog.d(TAG, "finished scanning raw table in " 200 + ((System.nanoTime() - startTime) / 1000000) + " ms"); 201 } 202 } 203 204 /** 205 * Send tracker to appropriate (3GPP or 3GPP2) inbound SMS handler for broadcast. 206 */ 207 private void broadcastSms(InboundSmsTracker tracker) { 208 InboundSmsHandler handler; 209 if (tracker.is3gpp2()) { 210 handler = mCdmaInboundSmsHandler; 211 } else { 212 handler = mGsmInboundSmsHandler; 213 } 214 if (handler != null) { 215 handler.sendMessage(InboundSmsHandler.EVENT_BROADCAST_SMS, tracker); 216 } else { 217 Rlog.e(TAG, "null handler for " + tracker.getFormat() + " format, can't deliver."); 218 } 219 } 220 221 /** 222 * Used as the HashMap key for matching concatenated message segments. 223 */ 224 private static class SmsReferenceKey { 225 final String mAddress; 226 final int mReferenceNumber; 227 final int mMessageCount; 228 229 SmsReferenceKey(InboundSmsTracker tracker) { 230 mAddress = tracker.getAddress(); 231 mReferenceNumber = tracker.getReferenceNumber(); 232 mMessageCount = tracker.getMessageCount(); 233 } 234 235 String[] getDeleteWhereArgs() { 236 return new String[]{mAddress, Integer.toString(mReferenceNumber), 237 Integer.toString(mMessageCount)}; 238 } 239 240 @Override 241 public int hashCode() { 242 return ((mReferenceNumber * 31) + mMessageCount) * 31 + mAddress.hashCode(); 243 } 244 245 @Override 246 public boolean equals(Object o) { 247 if (o instanceof SmsReferenceKey) { 248 SmsReferenceKey other = (SmsReferenceKey) o; 249 return other.mAddress.equals(mAddress) 250 && (other.mReferenceNumber == mReferenceNumber) 251 && (other.mMessageCount == mMessageCount); 252 } 253 return false; 254 } 255 } 256} 257