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 static java.util.Map.entry;
20 
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.content.BroadcastReceiver;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.database.Cursor;
28 import android.database.SQLException;
29 import android.os.PersistableBundle;
30 import android.os.UserManager;
31 import android.telephony.CarrierConfigManager;
32 import android.telephony.SubscriptionManager;
33 import android.telephony.TelephonyManager;
34 
35 import com.android.internal.telephony.analytics.TelephonyAnalytics;
36 import com.android.internal.telephony.analytics.TelephonyAnalytics.SmsMmsAnalytics;
37 import com.android.internal.telephony.cdma.CdmaInboundSmsHandler;
38 import com.android.internal.telephony.gsm.GsmInboundSmsHandler;
39 import com.android.internal.telephony.metrics.TelephonyMetrics;
40 import com.android.internal.telephony.subscription.SubscriptionManagerService;
41 import com.android.telephony.Rlog;
42 
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.Map;
46 
47 /**
48  * Called when the credential-encrypted storage is unlocked, collecting all acknowledged messages
49  * and deleting any partial message segments older than 7 days. Called from a worker thread to
50  * avoid delaying phone app startup. The last step is to broadcast the first pending message from
51  * the main thread, then the remaining pending messages will be broadcast after the previous
52  * ordered broadcast completes.
53  */
54 public class SmsBroadcastUndelivered {
55     private static final String TAG = "SmsBroadcastUndelivered";
56     private static final boolean DBG = InboundSmsHandler.DBG;
57 
58     /** Delete any partial message segments older than 7 days. */
59     static final long DEFAULT_PARTIAL_SEGMENT_EXPIRE_AGE = (long) (60 * 60 * 1000) * 24 * 7;
60 
61     /**
62      * Query projection for dispatching pending messages at boot time.
63      * Column order must match the {@code *_COLUMN} constants in {@link InboundSmsHandler}.
64      */
65     private static final String[] PDU_PENDING_MESSAGE_PROJECTION = {
66             "pdu",
67             "sequence",
68             "destination_port",
69             "date",
70             "reference_number",
71             "count",
72             "address",
73             "_id",
74             "message_body",
75             "display_originating_addr",
76             "sub_id"
77     };
78 
79     /** Mapping from DB COLUMN to PDU_PENDING_MESSAGE_PROJECTION index */
80     static final Map<Integer, Integer> PDU_PENDING_MESSAGE_PROJECTION_INDEX_MAPPING =
81             Map.ofEntries(
82                 entry(InboundSmsHandler.PDU_COLUMN, 0),
83                 entry(InboundSmsHandler.SEQUENCE_COLUMN, 1),
84                 entry(InboundSmsHandler.DESTINATION_PORT_COLUMN, 2),
85                 entry(InboundSmsHandler.DATE_COLUMN, 3),
86                 entry(InboundSmsHandler.REFERENCE_NUMBER_COLUMN, 4),
87                 entry(InboundSmsHandler.COUNT_COLUMN, 5),
88                 entry(InboundSmsHandler.ADDRESS_COLUMN, 6),
89                 entry(InboundSmsHandler.ID_COLUMN, 7),
90                 entry(InboundSmsHandler.MESSAGE_BODY_COLUMN, 8),
91                 entry(InboundSmsHandler.DISPLAY_ADDRESS_COLUMN, 9),
92                 entry(InboundSmsHandler.SUBID_COLUMN, 10));
93 
94 
95     private static SmsBroadcastUndelivered instance;
96 
97     /** Content resolver to use to access raw table from SmsProvider. */
98     private final ContentResolver mResolver;
99 
100     /** Broadcast receiver that processes the raw table when the user unlocks the phone for the
101      *  first time after reboot and the credential-encrypted storage is available.
102      */
103     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
104         @Override
105         public void onReceive(final Context context, Intent intent) {
106             Rlog.d(TAG, "Received broadcast " + intent.getAction());
107             if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
108                 new ScanRawTableThread(context).start();
109             }
110         }
111     };
112 
113     private class ScanRawTableThread extends Thread {
114         private final Context context;
115 
ScanRawTableThread(Context context)116         private ScanRawTableThread(Context context) {
117             this.context = context;
118         }
119 
120         @Override
run()121         public void run() {
122             scanRawTable(context,
123                     System.currentTimeMillis() - getUndeliveredSmsExpirationTime(context));
124             InboundSmsHandler.cancelNewMessageNotification(context);
125         }
126     }
127 
initialize(Context context, GsmInboundSmsHandler gsmInboundSmsHandler, CdmaInboundSmsHandler cdmaInboundSmsHandler)128     public static void initialize(Context context, GsmInboundSmsHandler gsmInboundSmsHandler,
129         CdmaInboundSmsHandler cdmaInboundSmsHandler) {
130         if (instance == null) {
131             instance = new SmsBroadcastUndelivered(context);
132         }
133 
134         // Tell handlers to start processing new messages and transit from the startup state to the
135         // idle state. This method may be called multiple times for multi-sim devices. We must make
136         // sure the state transition happen to all inbound sms handlers.
137         if (gsmInboundSmsHandler != null) {
138             gsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS);
139         }
140         if (cdmaInboundSmsHandler != null) {
141             cdmaInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS);
142         }
143     }
144 
145     @UnsupportedAppUsage
SmsBroadcastUndelivered(Context context)146     private SmsBroadcastUndelivered(Context context) {
147         mResolver = context.getContentResolver();
148 
149         UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
150 
151         if (userManager.isUserUnlocked()) {
152             new ScanRawTableThread(context).start();
153         } else {
154             IntentFilter userFilter = new IntentFilter();
155             userFilter.addAction(Intent.ACTION_USER_UNLOCKED);
156             context.registerReceiver(mBroadcastReceiver, userFilter);
157         }
158     }
159 
160     /**
161      * Scan the raw table for complete SMS messages to broadcast, and old PDUs to delete.
162      */
scanRawTable(Context context, long oldMessageTimestamp)163     static void scanRawTable(Context context, long oldMessageTimestamp) {
164         if (DBG) Rlog.d(TAG, "scanning raw table for undelivered messages");
165         long startTime = System.nanoTime();
166         ContentResolver contentResolver = context.getContentResolver();
167         HashMap<SmsReferenceKey, Integer> multiPartReceivedCount =
168                 new HashMap<SmsReferenceKey, Integer>(4);
169         HashSet<SmsReferenceKey> oldMultiPartMessages = new HashSet<SmsReferenceKey>(4);
170         Cursor cursor = null;
171         try {
172             // query only non-deleted ones
173             cursor = contentResolver.query(InboundSmsHandler.sRawUri,
174                     PDU_PENDING_MESSAGE_PROJECTION, "deleted = 0", null, null);
175             if (cursor == null) {
176                 Rlog.e(TAG, "error getting pending message cursor");
177                 return;
178             }
179 
180             boolean isCurrentFormat3gpp2 = InboundSmsHandler.isCurrentFormat3gpp2();
181             while (cursor.moveToNext()) {
182                 InboundSmsTracker tracker;
183                 try {
184                     tracker = TelephonyComponentFactory.getInstance()
185                             .inject(InboundSmsTracker.class.getName()).makeInboundSmsTracker(
186                                     context,
187                                     cursor,
188                                     isCurrentFormat3gpp2);
189                 } catch (IllegalArgumentException e) {
190                     Rlog.e(TAG, "error loading SmsTracker: " + e);
191                     continue;
192                 }
193 
194                 if (tracker.getMessageCount() == 1) {
195                     // deliver single-part message
196                     broadcastSms(tracker);
197                 } else {
198                     SmsReferenceKey reference = new SmsReferenceKey(tracker);
199                     Integer receivedCount = multiPartReceivedCount.get(reference);
200                     if (receivedCount == null) {
201                         multiPartReceivedCount.put(reference, 1);    // first segment seen
202                         if (tracker.getTimestamp() < oldMessageTimestamp) {
203                             // older than oldMessageTimestamp; delete if we don't find all the
204                             // segments
205                             oldMultiPartMessages.add(reference);
206                         }
207                     } else {
208                         int newCount = receivedCount + 1;
209                         if (newCount == tracker.getMessageCount()) {
210                             // looks like we've got all the pieces; send a single tracker
211                             // to state machine which will find the other pieces to broadcast
212                             if (DBG) Rlog.d(TAG, "found complete multi-part message");
213                             broadcastSms(tracker);
214                             // don't delete this old message until after we broadcast it
215                             oldMultiPartMessages.remove(reference);
216                         } else {
217                             multiPartReceivedCount.put(reference, newCount);
218                         }
219                     }
220                 }
221             }
222             // Retrieve the phone and phone id, required for metrics
223             // TODO don't hardcode to the first phone (phoneId = 0) but this is no worse than
224             //  earlier. Also phoneId for old messages may not be known (messages may be from an
225             //  inactive sub)
226             Phone phone = PhoneFactory.getPhone(0);
227             int phoneId = 0;
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                     if (phone != null) {
246                         phone.getSmsStats().onDroppedIncomingMultipartSms(message.mIs3gpp2, rows,
247                                 message.mMessageCount, TelephonyManager.from(context)
248                                         .isEmergencyNumber(message.mAddress));
249                         TelephonyAnalytics telephonyAnalytics = phone.getTelephonyAnalytics();
250                         if (telephonyAnalytics != null) {
251                             SmsMmsAnalytics smsMmsAnalytics =
252                                     telephonyAnalytics.getSmsMmsAnalytics();
253                             if (smsMmsAnalytics != null) {
254                                 smsMmsAnalytics.onDroppedIncomingMultipartSms();
255                             }
256                         }
257                     }
258                 }
259             }
260         } catch (SQLException e) {
261             Rlog.e(TAG, "error reading pending SMS messages", e);
262         } finally {
263             if (cursor != null) {
264                 cursor.close();
265             }
266             if (DBG) Rlog.d(TAG, "finished scanning raw table in "
267                     + ((System.nanoTime() - startTime) / 1000000) + " ms");
268         }
269     }
270 
271     /**
272      * Send tracker to appropriate (3GPP or 3GPP2) inbound SMS handler for broadcast.
273      */
broadcastSms(InboundSmsTracker tracker)274     private static void broadcastSms(InboundSmsTracker tracker) {
275         int subId = tracker.getSubId();
276         int phoneId = SubscriptionManagerService.getInstance().getPhoneId(subId);
277         if (!SubscriptionManager.isValidPhoneId(phoneId)) {
278             Rlog.e(TAG, "broadcastSms: ignoring message; no phone found for subId " + subId);
279             return;
280         }
281         Phone phone = PhoneFactory.getPhone(phoneId);
282         if (phone == null) {
283             Rlog.e(TAG, "broadcastSms: ignoring message; no phone found for subId " + subId
284                     + " phoneId " + phoneId);
285             return;
286         }
287         InboundSmsHandler handler = phone.getInboundSmsHandler(tracker.is3gpp2());
288         if (handler != null) {
289             handler.sendMessage(InboundSmsHandler.EVENT_BROADCAST_SMS, tracker);
290         } else {
291             Rlog.e(TAG, "null handler for " + tracker.getFormat() + " format, can't deliver.");
292         }
293     }
294 
getUndeliveredSmsExpirationTime(Context context)295     private long getUndeliveredSmsExpirationTime(Context context) {
296         int subId = SubscriptionManager.getDefaultSmsSubscriptionId();
297         CarrierConfigManager configManager =
298                 (CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
299         PersistableBundle bundle = configManager.getConfigForSubId(subId);
300 
301         if (bundle != null) {
302             return bundle.getLong(CarrierConfigManager.KEY_UNDELIVERED_SMS_MESSAGE_EXPIRATION_TIME,
303                     DEFAULT_PARTIAL_SEGMENT_EXPIRE_AGE);
304         } else {
305             return DEFAULT_PARTIAL_SEGMENT_EXPIRE_AGE;
306         }
307     }
308 
309     /**
310      * Used as the HashMap key for matching concatenated message segments.
311      */
312     private static class SmsReferenceKey {
313         final String mAddress;
314         final int mReferenceNumber;
315         final int mMessageCount;
316         final String mQuery;
317         final boolean mIs3gpp2;
318         final String mFormat;
319 
SmsReferenceKey(InboundSmsTracker tracker)320         SmsReferenceKey(InboundSmsTracker tracker) {
321             mAddress = tracker.getAddress();
322             mReferenceNumber = tracker.getReferenceNumber();
323             mMessageCount = tracker.getMessageCount();
324             mQuery = tracker.getQueryForSegments();
325             mIs3gpp2 = tracker.is3gpp2();
326             mFormat = tracker.getFormat();
327         }
328 
getDeleteWhereArgs()329         String[] getDeleteWhereArgs() {
330             return new String[]{mAddress, Integer.toString(mReferenceNumber),
331                     Integer.toString(mMessageCount)};
332         }
333 
getDeleteWhere()334         String getDeleteWhere() {
335             return mQuery;
336         }
337 
338         @Override
hashCode()339         public int hashCode() {
340             return ((mReferenceNumber * 31) + mMessageCount) * 31 + mAddress.hashCode();
341         }
342 
343         @Override
equals(Object o)344         public boolean equals(Object o) {
345             if (o instanceof SmsReferenceKey) {
346                 SmsReferenceKey other = (SmsReferenceKey) o;
347                 return other.mAddress.equals(mAddress)
348                         && (other.mReferenceNumber == mReferenceNumber)
349                         && (other.mMessageCount == mMessageCount);
350             }
351             return false;
352         }
353     }
354 }
355