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