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