• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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