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(cursor,
192                             isCurrentFormat3gpp2);
193                 } catch (IllegalArgumentException e) {
194                     Rlog.e(TAG, "error loading SmsTracker: " + e);
195                     continue;
196                 }
197 
198                 if (tracker.getMessageCount() == 1) {
199                     // deliver single-part message
200                     broadcastSms(tracker, cdmaInboundSmsHandler, gsmInboundSmsHandler);
201                 } else {
202                     SmsReferenceKey reference = new SmsReferenceKey(tracker);
203                     Integer receivedCount = multiPartReceivedCount.get(reference);
204                     if (receivedCount == null) {
205                         multiPartReceivedCount.put(reference, 1);    // first segment seen
206                         if (tracker.getTimestamp() < oldMessageTimestamp) {
207                             // older than oldMessageTimestamp; delete if we don't find all the
208                             // segments
209                             oldMultiPartMessages.add(reference);
210                         }
211                     } else {
212                         int newCount = receivedCount + 1;
213                         if (newCount == tracker.getMessageCount()) {
214                             // looks like we've got all the pieces; send a single tracker
215                             // to state machine which will find the other pieces to broadcast
216                             if (DBG) Rlog.d(TAG, "found complete multi-part message");
217                             broadcastSms(tracker, cdmaInboundSmsHandler, gsmInboundSmsHandler);
218                             // don't delete this old message until after we broadcast it
219                             oldMultiPartMessages.remove(reference);
220                         } else {
221                             multiPartReceivedCount.put(reference, newCount);
222                         }
223                     }
224                 }
225             }
226             // Retrieve the phone id, required for metrics
227             int phoneId = getPhoneId(gsmInboundSmsHandler, cdmaInboundSmsHandler);
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                 }
246             }
247         } catch (SQLException e) {
248             Rlog.e(TAG, "error reading pending SMS messages", e);
249         } finally {
250             if (cursor != null) {
251                 cursor.close();
252             }
253             if (DBG) Rlog.d(TAG, "finished scanning raw table in "
254                     + ((System.nanoTime() - startTime) / 1000000) + " ms");
255         }
256     }
257 
258     /**
259      * Retrieve the phone id for the GSM or CDMA Inbound SMS handler
260      */
getPhoneId(GsmInboundSmsHandler gsmInboundSmsHandler, CdmaInboundSmsHandler cdmaInboundSmsHandler)261     private static int getPhoneId(GsmInboundSmsHandler gsmInboundSmsHandler,
262             CdmaInboundSmsHandler cdmaInboundSmsHandler) {
263         int phoneId = SubscriptionManager.INVALID_PHONE_INDEX;
264         if (gsmInboundSmsHandler != null) {
265             phoneId = gsmInboundSmsHandler.getPhone().getPhoneId();
266         } else if (cdmaInboundSmsHandler != null) {
267             phoneId = cdmaInboundSmsHandler.getPhone().getPhoneId();
268         }
269         return phoneId;
270     }
271 
272     /**
273      * Send tracker to appropriate (3GPP or 3GPP2) inbound SMS handler for broadcast.
274      */
broadcastSms(InboundSmsTracker tracker, CdmaInboundSmsHandler cdmaInboundSmsHandler, GsmInboundSmsHandler gsmInboundSmsHandler)275     private static void broadcastSms(InboundSmsTracker tracker,
276             CdmaInboundSmsHandler cdmaInboundSmsHandler,
277             GsmInboundSmsHandler gsmInboundSmsHandler) {
278         InboundSmsHandler handler;
279         if (tracker.is3gpp2()) {
280             handler = cdmaInboundSmsHandler;
281         } else {
282             handler = gsmInboundSmsHandler;
283         }
284         if (handler != null) {
285             handler.sendMessage(InboundSmsHandler.EVENT_BROADCAST_SMS, tracker);
286         } else {
287             Rlog.e(TAG, "null handler for " + tracker.getFormat() + " format, can't deliver.");
288         }
289     }
290 
getUndeliveredSmsExpirationTime(Context context)291     private long getUndeliveredSmsExpirationTime(Context context) {
292         int subId = SubscriptionManager.getDefaultSmsSubscriptionId();
293         CarrierConfigManager configManager =
294                 (CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
295         PersistableBundle bundle = configManager.getConfigForSubId(subId);
296 
297         if (bundle != null) {
298             return bundle.getLong(CarrierConfigManager.KEY_UNDELIVERED_SMS_MESSAGE_EXPIRATION_TIME,
299                     DEFAULT_PARTIAL_SEGMENT_EXPIRE_AGE);
300         } else {
301             return DEFAULT_PARTIAL_SEGMENT_EXPIRE_AGE;
302         }
303     }
304 
305     /**
306      * Used as the HashMap key for matching concatenated message segments.
307      */
308     private static class SmsReferenceKey {
309         final String mAddress;
310         final int mReferenceNumber;
311         final int mMessageCount;
312         final String mQuery;
313         final String mFormat;
314 
SmsReferenceKey(InboundSmsTracker tracker)315         SmsReferenceKey(InboundSmsTracker tracker) {
316             mAddress = tracker.getAddress();
317             mReferenceNumber = tracker.getReferenceNumber();
318             mMessageCount = tracker.getMessageCount();
319             mQuery = tracker.getQueryForSegments();
320             mFormat = tracker.getFormat();
321         }
322 
getDeleteWhereArgs()323         String[] getDeleteWhereArgs() {
324             return new String[]{mAddress, Integer.toString(mReferenceNumber),
325                     Integer.toString(mMessageCount)};
326         }
327 
getDeleteWhere()328         String getDeleteWhere() {
329             return mQuery;
330         }
331 
332         @Override
hashCode()333         public int hashCode() {
334             return ((mReferenceNumber * 31) + mMessageCount) * 31 + mAddress.hashCode();
335         }
336 
337         @Override
equals(Object o)338         public boolean equals(Object o) {
339             if (o instanceof SmsReferenceKey) {
340                 SmsReferenceKey other = (SmsReferenceKey) o;
341                 return other.mAddress.equals(mAddress)
342                         && (other.mReferenceNumber == mReferenceNumber)
343                         && (other.mMessageCount == mMessageCount);
344             }
345             return false;
346         }
347     }
348 }
349