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