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