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