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