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     /** Handler for 3GPP-format messages (may be null). */
96     private final GsmInboundSmsHandler mGsmInboundSmsHandler;
97 
98     /** Handler for 3GPP2-format messages (may be null). */
99     private final CdmaInboundSmsHandler mCdmaInboundSmsHandler;
100 
101     /** Broadcast receiver that processes the raw table when the user unlocks the phone for the
102      *  first time after reboot and the credential-encrypted storage is available.
103      */
104     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
105         @Override
106         public void onReceive(final Context context, Intent intent) {
107             Rlog.d(TAG, "Received broadcast " + intent.getAction());
108             if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
109                 new ScanRawTableThread(context).start();
110             }
111         }
112     };
113 
114     private class ScanRawTableThread extends Thread {
115         private final Context context;
116 
ScanRawTableThread(Context context)117         private ScanRawTableThread(Context context) {
118             this.context = context;
119         }
120 
121         @Override
run()122         public void run() {
123             scanRawTable(context, mCdmaInboundSmsHandler, mGsmInboundSmsHandler,
124                     System.currentTimeMillis() - getUndeliveredSmsExpirationTime(context));
125             InboundSmsHandler.cancelNewMessageNotification(context);
126         }
127     }
128 
initialize(Context context, GsmInboundSmsHandler gsmInboundSmsHandler, CdmaInboundSmsHandler cdmaInboundSmsHandler)129     public static void initialize(Context context, GsmInboundSmsHandler gsmInboundSmsHandler,
130         CdmaInboundSmsHandler cdmaInboundSmsHandler) {
131         if (instance == null) {
132             instance = new SmsBroadcastUndelivered(
133                 context, gsmInboundSmsHandler, cdmaInboundSmsHandler);
134         }
135 
136         // Tell handlers to start processing new messages and transit from the startup state to the
137         // idle state. This method may be called multiple times for multi-sim devices. We must make
138         // sure the state transition happen to all inbound sms handlers.
139         if (gsmInboundSmsHandler != null) {
140             gsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS);
141         }
142         if (cdmaInboundSmsHandler != null) {
143             cdmaInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS);
144         }
145     }
146 
147     @UnsupportedAppUsage
SmsBroadcastUndelivered(Context context, GsmInboundSmsHandler gsmInboundSmsHandler, CdmaInboundSmsHandler cdmaInboundSmsHandler)148     private SmsBroadcastUndelivered(Context context, GsmInboundSmsHandler gsmInboundSmsHandler,
149             CdmaInboundSmsHandler cdmaInboundSmsHandler) {
150         mResolver = context.getContentResolver();
151         mGsmInboundSmsHandler = gsmInboundSmsHandler;
152         mCdmaInboundSmsHandler = cdmaInboundSmsHandler;
153 
154         UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
155 
156         if (userManager.isUserUnlocked()) {
157             new ScanRawTableThread(context).start();
158         } else {
159             IntentFilter userFilter = new IntentFilter();
160             userFilter.addAction(Intent.ACTION_USER_UNLOCKED);
161             context.registerReceiver(mBroadcastReceiver, userFilter);
162         }
163     }
164 
165     /**
166      * Scan the raw table for complete SMS messages to broadcast, and old PDUs to delete.
167      */
scanRawTable(Context context, CdmaInboundSmsHandler cdmaInboundSmsHandler, GsmInboundSmsHandler gsmInboundSmsHandler, long oldMessageTimestamp)168     static void scanRawTable(Context context, CdmaInboundSmsHandler cdmaInboundSmsHandler,
169             GsmInboundSmsHandler gsmInboundSmsHandler, long oldMessageTimestamp) {
170         if (DBG) Rlog.d(TAG, "scanning raw table for undelivered messages");
171         long startTime = System.nanoTime();
172         ContentResolver contentResolver = context.getContentResolver();
173         HashMap<SmsReferenceKey, Integer> multiPartReceivedCount =
174                 new HashMap<SmsReferenceKey, Integer>(4);
175         HashSet<SmsReferenceKey> oldMultiPartMessages = new HashSet<SmsReferenceKey>(4);
176         Cursor cursor = null;
177         try {
178             // query only non-deleted ones
179             cursor = contentResolver.query(InboundSmsHandler.sRawUri,
180                     PDU_PENDING_MESSAGE_PROJECTION, "deleted = 0", null, null);
181             if (cursor == null) {
182                 Rlog.e(TAG, "error getting pending message cursor");
183                 return;
184             }
185 
186             boolean isCurrentFormat3gpp2 = InboundSmsHandler.isCurrentFormat3gpp2();
187             while (cursor.moveToNext()) {
188                 InboundSmsTracker tracker;
189                 try {
190                     tracker = TelephonyComponentFactory.getInstance()
191                             .inject(InboundSmsTracker.class.getName()).makeInboundSmsTracker(
192                                     context,
193                                     cursor,
194                                     isCurrentFormat3gpp2);
195                 } catch (IllegalArgumentException e) {
196                     Rlog.e(TAG, "error loading SmsTracker: " + e);
197                     continue;
198                 }
199 
200                 if (tracker.getMessageCount() == 1) {
201                     // deliver single-part message
202                     broadcastSms(tracker, cdmaInboundSmsHandler, gsmInboundSmsHandler);
203                 } else {
204                     SmsReferenceKey reference = new SmsReferenceKey(tracker);
205                     Integer receivedCount = multiPartReceivedCount.get(reference);
206                     if (receivedCount == null) {
207                         multiPartReceivedCount.put(reference, 1);    // first segment seen
208                         if (tracker.getTimestamp() < oldMessageTimestamp) {
209                             // older than oldMessageTimestamp; delete if we don't find all the
210                             // segments
211                             oldMultiPartMessages.add(reference);
212                         }
213                     } else {
214                         int newCount = receivedCount + 1;
215                         if (newCount == tracker.getMessageCount()) {
216                             // looks like we've got all the pieces; send a single tracker
217                             // to state machine which will find the other pieces to broadcast
218                             if (DBG) Rlog.d(TAG, "found complete multi-part message");
219                             broadcastSms(tracker, cdmaInboundSmsHandler, gsmInboundSmsHandler);
220                             // don't delete this old message until after we broadcast it
221                             oldMultiPartMessages.remove(reference);
222                         } else {
223                             multiPartReceivedCount.put(reference, newCount);
224                         }
225                     }
226                 }
227             }
228             // Retrieve the phone id, required for metrics
229             int phoneId = getPhoneId(gsmInboundSmsHandler, cdmaInboundSmsHandler);
230 
231             // Delete old incomplete message segments
232             for (SmsReferenceKey message : oldMultiPartMessages) {
233                 // delete permanently
234                 int rows = contentResolver.delete(InboundSmsHandler.sRawUriPermanentDelete,
235                         message.getDeleteWhere(), message.getDeleteWhereArgs());
236                 if (rows == 0) {
237                     Rlog.e(TAG, "No rows were deleted from raw table!");
238                 } else if (DBG) {
239                     Rlog.d(TAG, "Deleted " + rows + " rows from raw table for incomplete "
240                             + message.mMessageCount + " part message");
241                 }
242                 // Update metrics with dropped SMS
243                 if (rows > 0) {
244                     TelephonyMetrics metrics = TelephonyMetrics.getInstance();
245                     metrics.writeDroppedIncomingMultipartSms(phoneId, message.mFormat, rows,
246                             message.mMessageCount);
247                 }
248             }
249         } catch (SQLException e) {
250             Rlog.e(TAG, "error reading pending SMS messages", e);
251         } finally {
252             if (cursor != null) {
253                 cursor.close();
254             }
255             if (DBG) Rlog.d(TAG, "finished scanning raw table in "
256                     + ((System.nanoTime() - startTime) / 1000000) + " ms");
257         }
258     }
259 
260     /**
261      * Retrieve the phone id for the GSM or CDMA Inbound SMS handler
262      */
getPhoneId(GsmInboundSmsHandler gsmInboundSmsHandler, CdmaInboundSmsHandler cdmaInboundSmsHandler)263     private static int getPhoneId(GsmInboundSmsHandler gsmInboundSmsHandler,
264             CdmaInboundSmsHandler cdmaInboundSmsHandler) {
265         int phoneId = SubscriptionManager.INVALID_PHONE_INDEX;
266         if (gsmInboundSmsHandler != null) {
267             phoneId = gsmInboundSmsHandler.getPhone().getPhoneId();
268         } else if (cdmaInboundSmsHandler != null) {
269             phoneId = cdmaInboundSmsHandler.getPhone().getPhoneId();
270         }
271         return phoneId;
272     }
273 
274     /**
275      * Send tracker to appropriate (3GPP or 3GPP2) inbound SMS handler for broadcast.
276      */
broadcastSms(InboundSmsTracker tracker, CdmaInboundSmsHandler cdmaInboundSmsHandler, GsmInboundSmsHandler gsmInboundSmsHandler)277     private static void broadcastSms(InboundSmsTracker tracker,
278             CdmaInboundSmsHandler cdmaInboundSmsHandler,
279             GsmInboundSmsHandler gsmInboundSmsHandler) {
280         InboundSmsHandler handler;
281         if (tracker.is3gpp2()) {
282             handler = cdmaInboundSmsHandler;
283         } else {
284             handler = gsmInboundSmsHandler;
285         }
286         if (handler != null) {
287             handler.sendMessage(InboundSmsHandler.EVENT_BROADCAST_SMS, tracker);
288         } else {
289             Rlog.e(TAG, "null handler for " + tracker.getFormat() + " format, can't deliver.");
290         }
291     }
292 
getUndeliveredSmsExpirationTime(Context context)293     private long getUndeliveredSmsExpirationTime(Context context) {
294         int subId = SubscriptionManager.getDefaultSmsSubscriptionId();
295         CarrierConfigManager configManager =
296                 (CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
297         PersistableBundle bundle = configManager.getConfigForSubId(subId);
298 
299         if (bundle != null) {
300             return bundle.getLong(CarrierConfigManager.KEY_UNDELIVERED_SMS_MESSAGE_EXPIRATION_TIME,
301                     DEFAULT_PARTIAL_SEGMENT_EXPIRE_AGE);
302         } else {
303             return DEFAULT_PARTIAL_SEGMENT_EXPIRE_AGE;
304         }
305     }
306 
307     /**
308      * Used as the HashMap key for matching concatenated message segments.
309      */
310     private static class SmsReferenceKey {
311         final String mAddress;
312         final int mReferenceNumber;
313         final int mMessageCount;
314         final String mQuery;
315         final String mFormat;
316 
SmsReferenceKey(InboundSmsTracker tracker)317         SmsReferenceKey(InboundSmsTracker tracker) {
318             mAddress = tracker.getAddress();
319             mReferenceNumber = tracker.getReferenceNumber();
320             mMessageCount = tracker.getMessageCount();
321             mQuery = tracker.getQueryForSegments();
322             mFormat = tracker.getFormat();
323         }
324 
getDeleteWhereArgs()325         String[] getDeleteWhereArgs() {
326             return new String[]{mAddress, Integer.toString(mReferenceNumber),
327                     Integer.toString(mMessageCount)};
328         }
329 
getDeleteWhere()330         String getDeleteWhere() {
331             return mQuery;
332         }
333 
334         @Override
hashCode()335         public int hashCode() {
336             return ((mReferenceNumber * 31) + mMessageCount) * 31 + mAddress.hashCode();
337         }
338 
339         @Override
equals(Object o)340         public boolean equals(Object o) {
341             if (o instanceof SmsReferenceKey) {
342                 SmsReferenceKey other = (SmsReferenceKey) o;
343                 return other.mAddress.equals(mAddress)
344                         && (other.mReferenceNumber == mReferenceNumber)
345                         && (other.mMessageCount == mMessageCount);
346             }
347             return false;
348         }
349     }
350 }
351