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