1 /*
2  * Copyright (C) 2020 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.bluetooth.mapclient;
18 
19 import android.bluetooth.BluetoothDevice;
20 import android.bluetooth.BluetoothMapClient;
21 import android.content.ContentResolver;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.database.ContentObserver;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.provider.BaseColumns;
28 import android.provider.Telephony;
29 import android.provider.Telephony.Mms;
30 import android.provider.Telephony.MmsSms;
31 import android.provider.Telephony.Sms;
32 import android.provider.Telephony.Threads;
33 import android.telephony.PhoneNumberUtils;
34 import android.telephony.SubscriptionInfo;
35 import android.telephony.SubscriptionManager;
36 import android.telephony.TelephonyManager;
37 import android.util.ArraySet;
38 import android.util.Log;
39 
40 import com.android.bluetooth.Utils;
41 import com.android.bluetooth.map.BluetoothMapbMessageMime;
42 import com.android.bluetooth.map.BluetoothMapbMessageMime.MimePart;
43 import com.android.vcard.VCardConstants;
44 import com.android.vcard.VCardEntry;
45 import com.android.vcard.VCardProperty;
46 
47 import com.google.android.mms.pdu.PduHeaders;
48 
49 import java.time.Instant;
50 import java.time.ZoneId;
51 import java.time.format.DateTimeFormatter;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.Collections;
55 import java.util.HashMap;
56 import java.util.List;
57 import java.util.Map;
58 import java.util.Set;
59 
60 class MapClientContent {
61     private static final String TAG = MapClientContent.class.getSimpleName();
62 
63     private static final String INBOX_PATH = "telecom/msg/inbox";
64     private static final int DEFAULT_CHARSET = 106;
65     private static final int ORIGINATOR_ADDRESS_TYPE = 137;
66     private static final int RECIPIENT_ADDRESS_TYPE = 151;
67 
68     private static final int NUM_RECENT_MSGS_TO_DUMP = 5;
69 
70     private enum Type {
71         UNKNOWN,
72         SMS,
73         MMS
74     }
75 
76     private enum Folder {
77         UNKNOWN,
78         INBOX,
79         SENT
80     }
81 
82     final BluetoothDevice mDevice;
83     private final Context mContext;
84     private final Callbacks mCallbacks;
85     private final ContentResolver mResolver;
86     ContentObserver mContentObserver;
87     String mPhoneNumber = null;
88     private int mSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
89     private SubscriptionManager mSubscriptionManager;
90     private TelephonyManager mTelephonyManager;
91     private HashMap<String, Uri> mHandleToUriMap = new HashMap<>();
92     private HashMap<Uri, MessageStatus> mUriToHandleMap = new HashMap<>();
93 
94     /** Callbacks API to notify about statusChanges as observed from the content provider */
95     interface Callbacks {
onMessageStatusChanged(String handle, int status)96         void onMessageStatusChanged(String handle, int status);
97     }
98 
99     /**
100      * MapClientContent manages all interactions between Bluetooth and the messaging provider.
101      *
102      * <p>Changes to the database are mirrored between the remote and local providers, specifically
103      * new messages, changes to read status, and removal of messages.
104      *
105      * <p>Object is invalid after cleanUp() is called.
106      *
107      * <p>context: the context that all content provider interactions are conducted MceStateMachine:
108      * the interface to send outbound updates such as when a message is read locally device: the
109      * associated Bluetooth device used for associating messages with a subscription
110      */
MapClientContent(Context context, Callbacks callbacks, BluetoothDevice device)111     MapClientContent(Context context, Callbacks callbacks, BluetoothDevice device) {
112         mContext = context;
113         mDevice = device;
114         mCallbacks = callbacks;
115         mResolver = mContext.getContentResolver();
116 
117         mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
118         mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
119         mSubscriptionManager.addSubscriptionInfoRecord(
120                 mDevice.getAddress(),
121                 Utils.getName(mDevice),
122                 0,
123                 SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM);
124         SubscriptionInfo info =
125                 mSubscriptionManager.getActiveSubscriptionInfoForIcc(mDevice.getAddress());
126         if (info != null) {
127             mSubscriptionId = info.getSubscriptionId();
128         }
129 
130         mContentObserver =
131                 new ContentObserver(null) {
132                     @Override
133                     public boolean deliverSelfNotifications() {
134                         return false;
135                     }
136 
137                     @Override
138                     public void onChange(boolean selfChange) {
139                         verbose("onChange(self=" + selfChange + ")");
140                         findChangeInDatabase();
141                     }
142 
143                     @Override
144                     public void onChange(boolean selfChange, Uri uri) {
145                         verbose("onChange(self=" + selfChange + ", uri=" + uri.toString() + ")");
146                         findChangeInDatabase();
147                     }
148                 };
149 
150         clearMessages(mContext, mSubscriptionId);
151         mResolver.registerContentObserver(Sms.CONTENT_URI, true, mContentObserver);
152         mResolver.registerContentObserver(Mms.CONTENT_URI, true, mContentObserver);
153         mResolver.registerContentObserver(MmsSms.CONTENT_URI, true, mContentObserver);
154     }
155 
clearAllContent(Context context)156     static void clearAllContent(Context context) {
157         SubscriptionManager subscriptionManager =
158                 context.getSystemService(SubscriptionManager.class);
159         List<SubscriptionInfo> subscriptions = subscriptionManager.getActiveSubscriptionInfoList();
160         if (subscriptions == null) {
161             Log.w(TAG, "[AllDevices] Active subscription list is missing");
162             return;
163         }
164         for (SubscriptionInfo info : subscriptions) {
165             if (info.getSubscriptionType() == SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM) {
166                 clearMessages(context, info.getSubscriptionId());
167                 try {
168                     subscriptionManager.removeSubscriptionInfoRecord(
169                             info.getIccId(), SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM);
170                 } catch (Exception e) {
171                     Log.w(TAG, "[AllDevices] cleanUp failed: " + e.toString());
172                 }
173             }
174         }
175     }
176 
error(String message)177     private void error(String message) {
178         Log.e(TAG, "[" + mDevice + "] " + message);
179     }
180 
warn(String message)181     private void warn(String message) {
182         Log.w(TAG, "[" + mDevice + "] " + message);
183     }
184 
warn(String message, Exception e)185     private void warn(String message, Exception e) {
186         Log.w(TAG, "[" + mDevice + "] " + message, e);
187     }
188 
info(String message)189     private void info(String message) {
190         Log.i(TAG, "[" + mDevice + "] " + message);
191     }
192 
debug(String message)193     private void debug(String message) {
194         Log.d(TAG, "[" + mDevice + "] " + message);
195     }
196 
verbose(String message)197     private void verbose(String message) {
198         Log.v(TAG, "[" + mDevice + "] " + message);
199     }
200 
201     /**
202      * This number is necessary for thread_id to work properly. thread_id is needed for (group) MMS
203      * messages to be displayed/stitched correctly.
204      */
setRemoteDeviceOwnNumber(String phoneNumber)205     void setRemoteDeviceOwnNumber(String phoneNumber) {
206         mPhoneNumber = phoneNumber;
207         verbose("Remote device " + mDevice.getAddress() + " phone number set to: " + mPhoneNumber);
208     }
209 
210     /**
211      * storeMessage
212      *
213      * <p>Store a message in database with the associated handle and timestamp. The handle is used
214      * to associate the local message with the remote message.
215      */
storeMessage(Bmessage message, String handle, Long timestamp, boolean seen)216     void storeMessage(Bmessage message, String handle, Long timestamp, boolean seen) {
217         info(
218                 "storeMessage(time="
219                         + timestamp
220                         + "["
221                         + toDatetimeString(timestamp)
222                         + "]"
223                         + ", handle="
224                         + handle
225                         + ", type="
226                         + message.getType()
227                         + ", folder="
228                         + message.getFolder());
229 
230         switch (message.getType()) {
231             case MMS:
232                 storeMms(message, handle, timestamp, seen);
233                 return;
234             case SMS_CDMA:
235             case SMS_GSM:
236                 storeSms(message, handle, timestamp, seen);
237                 return;
238             default:
239                 debug("Request to store unsupported message type: " + message.getType());
240         }
241     }
242 
storeSms(Bmessage message, String handle, Long timestamp, boolean seen)243     private void storeSms(Bmessage message, String handle, Long timestamp, boolean seen) {
244         debug("storeSms");
245         verbose(message.toString());
246         String recipients;
247         if (INBOX_PATH.equals(message.getFolder())) {
248             recipients = getOriginatorNumber(message);
249         } else {
250             recipients = getFirstRecipientNumber(message);
251             if (recipients == null) {
252                 debug("invalid recipients");
253                 return;
254             }
255         }
256         verbose("Received SMS from Number " + recipients);
257 
258         Uri contentUri =
259                 INBOX_PATH.equalsIgnoreCase(message.getFolder())
260                         ? Sms.Inbox.CONTENT_URI
261                         : Sms.Sent.CONTENT_URI;
262         ContentValues values = new ContentValues();
263         long threadId = getThreadId(message);
264         int readStatus = message.getStatus() == Bmessage.Status.READ ? 1 : 0;
265 
266         values.put(Sms.THREAD_ID, threadId);
267         values.put(Sms.ADDRESS, recipients);
268         values.put(Sms.BODY, message.getBodyContent());
269         values.put(Sms.SUBSCRIPTION_ID, mSubscriptionId);
270         values.put(Sms.DATE, timestamp);
271         values.put(Sms.READ, readStatus);
272         values.put(Sms.SEEN, seen);
273 
274         Uri results = mResolver.insert(contentUri, values);
275         if (results == null) {
276             error("Failed to get SMS URI, insert failed. Dropping message.");
277             return;
278         }
279 
280         mHandleToUriMap.put(handle, results);
281         mUriToHandleMap.put(results, new MessageStatus(handle, readStatus));
282         debug("Map InsertedThread" + results);
283     }
284 
285     /** deleteMessage remove a message from the local provider based on a remote change */
deleteMessage(String handle)286     void deleteMessage(String handle) {
287         debug("deleting handle" + handle);
288         Uri messageToChange = mHandleToUriMap.get(handle);
289         if (messageToChange != null) {
290             mResolver.delete(messageToChange, null);
291         }
292     }
293 
294     /** markRead mark a message read in the local provider based on a remote change */
markRead(String handle)295     void markRead(String handle) {
296         debug("marking read " + handle);
297         Uri messageToChange = mHandleToUriMap.get(handle);
298         if (messageToChange != null) {
299             ContentValues values = new ContentValues();
300             values.put(Sms.READ, 1);
301             mResolver.update(messageToChange, values, null);
302         }
303     }
304 
305     /**
306      * findChangeInDatabase compare the current state of the local content provider to the expected
307      * state and propagate changes to the remote.
308      */
findChangeInDatabase()309     private void findChangeInDatabase() {
310         HashMap<Uri, MessageStatus> originalUriToHandleMap;
311         HashMap<Uri, MessageStatus> duplicateUriToHandleMap;
312 
313         originalUriToHandleMap = mUriToHandleMap;
314         duplicateUriToHandleMap = new HashMap<>(originalUriToHandleMap);
315         for (Uri uri : new Uri[] {Mms.CONTENT_URI, Sms.CONTENT_URI}) {
316             try (Cursor cursor = mResolver.query(uri, null, null, null, null)) {
317                 while (cursor.moveToNext()) {
318                     Uri index =
319                             Uri.withAppendedPath(
320                                     uri, cursor.getString(cursor.getColumnIndex("_id")));
321                     int readStatus = cursor.getInt(cursor.getColumnIndex(Sms.READ));
322                     MessageStatus currentMessage = duplicateUriToHandleMap.remove(index);
323                     if (currentMessage != null && currentMessage.mRead != readStatus) {
324                         verbose(currentMessage.mHandle);
325                         currentMessage.mRead = readStatus;
326                         mCallbacks.onMessageStatusChanged(
327                                 currentMessage.mHandle, BluetoothMapClient.READ);
328                     }
329                 }
330             }
331         }
332         for (Map.Entry record : duplicateUriToHandleMap.entrySet()) {
333             verbose("Deleted " + ((MessageStatus) record.getValue()).mHandle);
334             originalUriToHandleMap.remove(record.getKey());
335             mCallbacks.onMessageStatusChanged(
336                     ((MessageStatus) record.getValue()).mHandle, BluetoothMapClient.DELETED);
337         }
338     }
339 
storeMms(Bmessage message, String handle, Long timestamp, boolean seen)340     private void storeMms(Bmessage message, String handle, Long timestamp, boolean seen) {
341         debug("storeMms");
342         verbose(message.toString());
343         try {
344             ContentValues values = new ContentValues();
345             long threadId = getThreadId(message);
346             BluetoothMapbMessageMime mmsBmessage = new BluetoothMapbMessageMime();
347             mmsBmessage.parseMsgPart(message.getBodyContent());
348             int read = message.getStatus() == Bmessage.Status.READ ? 1 : 0;
349             Uri contentUri;
350             int messageBox;
351             if (INBOX_PATH.equalsIgnoreCase(message.getFolder())) {
352                 contentUri = Mms.Inbox.CONTENT_URI;
353                 messageBox = Mms.MESSAGE_BOX_INBOX;
354             } else {
355                 contentUri = Mms.Sent.CONTENT_URI;
356                 messageBox = Mms.MESSAGE_BOX_SENT;
357             }
358             debug("Parsed");
359             values.put(Mms.SUBSCRIPTION_ID, mSubscriptionId);
360             values.put(Mms.THREAD_ID, threadId);
361             values.put(Mms.DATE, timestamp / 1000L);
362             values.put(Mms.TEXT_ONLY, true);
363             values.put(Mms.MESSAGE_BOX, messageBox);
364             values.put(Mms.READ, read);
365             values.put(Mms.SEEN, seen);
366             values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
367             values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION);
368             values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL);
369             values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO);
370             values.put(Mms.TRANSACTION_ID, "T" + Long.toHexString(System.currentTimeMillis()));
371             values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO);
372             values.put(Mms.LOCKED, 0);
373             values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related");
374             values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR);
375             values.put(Mms.MESSAGE_SIZE, mmsBmessage.getSize());
376 
377             Uri results = mResolver.insert(contentUri, values);
378             if (results == null) {
379                 error("Failed to get MMS entry URI. Cannot store MMS parts. Dropping message.");
380                 return;
381             }
382 
383             mHandleToUriMap.put(handle, results);
384             mUriToHandleMap.put(results, new MessageStatus(handle, read));
385 
386             debug("Map InsertedThread" + results);
387 
388             // Some Messenger Applications don't listen to address table changes and only listen
389             // for message content changes. Adding the address parts first makes it so they're
390             // already in the tables when a given app syncs due to content updates. Otherwise, we
391             // risk a race where the address content may not be ready.
392             storeAddressPart(message, results);
393 
394             for (MimePart part : mmsBmessage.getMimeParts()) {
395                 storeMmsPart(part, results);
396             }
397         } catch (Exception e) {
398             error("Error while storing MMS: " + e.toString());
399             throw e;
400         }
401     }
402 
storeMmsPart(MimePart messagePart, Uri messageUri)403     private Uri storeMmsPart(MimePart messagePart, Uri messageUri) {
404         ContentValues values = new ContentValues();
405         values.put(Mms.Part.CONTENT_TYPE, "text/plain");
406         values.put(Mms.Part.CHARSET, DEFAULT_CHARSET);
407         values.put(Mms.Part.FILENAME, "text_1.txt");
408         values.put(Mms.Part.NAME, "text_1.txt");
409         values.put(Mms.Part.CONTENT_ID, messagePart.mContentId);
410         values.put(Mms.Part.CONTENT_LOCATION, messagePart.mContentLocation);
411         values.put(Mms.Part.TEXT, messagePart.getDataAsString());
412 
413         Uri contentUri = Uri.parse(messageUri.toString() + "/part");
414         Uri results = mResolver.insert(contentUri, values);
415 
416         if (results == null) {
417             warn("failed to insert MMS part");
418             return null;
419         }
420 
421         debug("Inserted" + results);
422         return results;
423     }
424 
storeAddressPart(Bmessage message, Uri messageUri)425     private void storeAddressPart(Bmessage message, Uri messageUri) {
426         ContentValues values = new ContentValues();
427         Uri contentUri = Uri.parse(messageUri.toString() + "/addr");
428         String originator = getOriginatorNumber(message);
429         values.put(Mms.Addr.CHARSET, DEFAULT_CHARSET);
430         values.put(Mms.Addr.ADDRESS, originator);
431         values.put(Mms.Addr.TYPE, ORIGINATOR_ADDRESS_TYPE);
432 
433         Uri results = mResolver.insert(contentUri, values);
434         if (results == null) {
435             warn("failed to insert originator address");
436         }
437 
438         Set<String> messageContacts = new ArraySet<>();
439         getRecipientsFromMessage(message, messageContacts);
440         for (String recipient : messageContacts) {
441             values.put(Mms.Addr.ADDRESS, recipient);
442             values.put(Mms.Addr.TYPE, RECIPIENT_ADDRESS_TYPE);
443             results = mResolver.insert(contentUri, values);
444             if (results == null) {
445                 warn("failed to insert recipient address");
446             }
447         }
448     }
449 
450     /** cleanUp clear the subscription info and content on shutdown */
cleanUp()451     void cleanUp() {
452         debug(
453                 "cleanUp(device="
454                         + Utils.getLoggableAddress(mDevice)
455                         + "subscriptionId="
456                         + mSubscriptionId);
457         mResolver.unregisterContentObserver(mContentObserver);
458         clearMessages(mContext, mSubscriptionId);
459         try {
460             mSubscriptionManager.removeSubscriptionInfoRecord(
461                     mDevice.getAddress(), SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM);
462             mSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
463         } catch (Exception e) {
464             warn("cleanUp failed: " + e.toString());
465         }
466     }
467 
468     /** clearMessages clean up the content provider on startup */
clearMessages(Context context, int subscriptionId)469     private static void clearMessages(Context context, int subscriptionId) {
470         Log.d(TAG, "[AllDevices] clearMessages(subscriptionId=" + subscriptionId);
471 
472         ContentResolver resolver = context.getContentResolver();
473         String threads = new String();
474 
475         Uri uri = Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();
476         try (Cursor threadCursor = resolver.query(uri, null, null, null, null)) {
477             while (threadCursor.moveToNext()) {
478                 threads += threadCursor.getInt(threadCursor.getColumnIndex(Threads._ID)) + ", ";
479             }
480         }
481 
482         resolver.delete(
483                 Sms.CONTENT_URI,
484                 Sms.SUBSCRIPTION_ID + " =? ",
485                 new String[] {Integer.toString(subscriptionId)});
486         resolver.delete(
487                 Mms.CONTENT_URI,
488                 Mms.SUBSCRIPTION_ID + " =? ",
489                 new String[] {Integer.toString(subscriptionId)});
490         if (threads.length() > 2) {
491             threads = threads.substring(0, threads.length() - 2);
492             resolver.delete(Threads.CONTENT_URI, Threads._ID + " IN (" + threads + ")", null);
493         }
494     }
495 
496     /** getThreadId utilize the originator and recipients to obtain the thread id */
getThreadId(Bmessage message)497     private long getThreadId(Bmessage message) {
498 
499         Set<String> messageContacts = new ArraySet<>();
500         String originator = PhoneNumberUtils.extractNetworkPortion(getOriginatorNumber(message));
501         if (originator != null) {
502             messageContacts.add(originator);
503         }
504         getRecipientsFromMessage(message, messageContacts);
505         // If there is only one contact don't remove it.
506         if (messageContacts.isEmpty()) {
507             return Telephony.Threads.COMMON_THREAD;
508         } else if (messageContacts.size() > 1) {
509             if (mPhoneNumber == null) {
510                 warn("getThreadId called, mPhoneNumber never found.");
511             }
512             messageContacts.removeIf(
513                     number ->
514                             (PhoneNumberUtils.areSamePhoneNumber(
515                                     number,
516                                     mPhoneNumber,
517                                     mTelephonyManager.getNetworkCountryIso())));
518         }
519 
520         verbose("Contacts = " + messageContacts.toString());
521         return Telephony.Threads.getOrCreateThreadId(mContext, messageContacts);
522     }
523 
getRecipientsFromMessage(Bmessage message, Set<String> messageContacts)524     private void getRecipientsFromMessage(Bmessage message, Set<String> messageContacts) {
525         List<VCardEntry> recipients = message.getRecipients();
526         for (VCardEntry recipient : recipients) {
527             List<VCardEntry.PhoneData> phoneData = recipient.getPhoneList();
528             if (phoneData != null && !phoneData.isEmpty()) {
529                 messageContacts.add(
530                         PhoneNumberUtils.extractNetworkPortion(phoneData.get(0).getNumber()));
531             }
532         }
533     }
534 
getOriginatorNumber(Bmessage message)535     private String getOriginatorNumber(Bmessage message) {
536         VCardEntry originator = message.getOriginator();
537         if (originator == null) {
538             return null;
539         }
540 
541         List<VCardEntry.PhoneData> phoneData = originator.getPhoneList();
542         if (phoneData == null || phoneData.isEmpty()) {
543             return null;
544         }
545 
546         return PhoneNumberUtils.extractNetworkPortion(phoneData.get(0).getNumber());
547     }
548 
getFirstRecipientNumber(Bmessage message)549     private String getFirstRecipientNumber(Bmessage message) {
550         List<VCardEntry> recipients = message.getRecipients();
551         if (recipients == null || recipients.isEmpty()) {
552             return null;
553         }
554 
555         List<VCardEntry.PhoneData> phoneData = recipients.get(0).getPhoneList();
556         if (phoneData == null || phoneData.isEmpty()) {
557             return null;
558         }
559 
560         return phoneData.get(0).getNumber();
561     }
562 
563     /**
564      * addThreadContactToEntries utilizing the thread id fill in the appropriate fields of bmsg with
565      * the intended recipients
566      */
addThreadContactsToEntries(Bmessage bmsg, String thread)567     boolean addThreadContactsToEntries(Bmessage bmsg, String thread) {
568         String threadId = Uri.parse(thread).getLastPathSegment();
569 
570         debug("MATCHING THREAD" + threadId);
571         debug(MmsSms.CONTENT_CONVERSATIONS_URI + threadId + "/recipients");
572 
573         try (Cursor cursor =
574                 mResolver.query(
575                         Uri.withAppendedPath(
576                                 MmsSms.CONTENT_CONVERSATIONS_URI, threadId + "/recipients"),
577                         null,
578                         null,
579                         null,
580                         null)) {
581 
582             if (cursor.moveToNext()) {
583                 debug("Columns" + Arrays.toString(cursor.getColumnNames()));
584                 verbose(
585                         "CONTACT LIST: "
586                                 + cursor.getString(cursor.getColumnIndex("recipient_ids")));
587                 addRecipientsToEntries(
588                         bmsg, cursor.getString(cursor.getColumnIndex("recipient_ids")).split(" "));
589                 return true;
590             } else {
591                 warn("Thread Not Found");
592                 return false;
593             }
594         }
595     }
596 
addRecipientsToEntries(Bmessage bmsg, String[] recipients)597     private void addRecipientsToEntries(Bmessage bmsg, String[] recipients) {
598         verbose("CONTACT LIST: " + Arrays.toString(recipients));
599         for (String recipient : recipients) {
600             try (Cursor cursor =
601                     mResolver.query(
602                             Uri.parse("content://mms-sms/canonical-address/" + recipient),
603                             null,
604                             null,
605                             null,
606                             null)) {
607                 while (cursor.moveToNext()) {
608                     String number = cursor.getString(cursor.getColumnIndex(Mms.Addr.ADDRESS));
609                     verbose("CONTACT number: " + number);
610                     VCardEntry destEntry = new VCardEntry();
611                     VCardProperty destEntryPhone = new VCardProperty();
612                     destEntryPhone.setName(VCardConstants.PROPERTY_TEL);
613                     destEntryPhone.addValues(number);
614                     destEntry.addProperty(destEntryPhone);
615                     bmsg.addRecipient(destEntry);
616                 }
617             }
618         }
619     }
620 
621     /**
622      * Get the total number of messages we've stored under this device's subscription ID, for a
623      * given message source, provided by the "uri" parameter.
624      */
getStoredMessagesCount(Uri uri)625     private int getStoredMessagesCount(Uri uri) {
626         if (mSubscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
627             verbose("getStoredMessagesCount(uri=" + uri + "): Failed, no subscription ID");
628             return 0;
629         }
630 
631         Cursor cursor = null;
632         if (Sms.CONTENT_URI.equals(uri)
633                 || Sms.Inbox.CONTENT_URI.equals(uri)
634                 || Sms.Sent.CONTENT_URI.equals(uri)) {
635             cursor =
636                     mResolver.query(
637                             uri,
638                             new String[] {"count(*)"},
639                             Sms.SUBSCRIPTION_ID + " =? ",
640                             new String[] {Integer.toString(mSubscriptionId)},
641                             null);
642         } else if (Mms.CONTENT_URI.equals(uri)
643                 || Mms.Inbox.CONTENT_URI.equals(uri)
644                 || Mms.Sent.CONTENT_URI.equals(uri)) {
645             cursor =
646                     mResolver.query(
647                             uri,
648                             new String[] {"count(*)"},
649                             Mms.SUBSCRIPTION_ID + " =? ",
650                             new String[] {Integer.toString(mSubscriptionId)},
651                             null);
652         } else if (Threads.CONTENT_URI.equals(uri)) {
653             uri = Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();
654             cursor = mResolver.query(uri, new String[] {"count(*)"}, null, null, null);
655         }
656 
657         if (cursor == null) {
658             return 0;
659         }
660 
661         cursor.moveToFirst();
662         int count = cursor.getInt(0);
663         cursor.close();
664 
665         return count;
666     }
667 
getRecentMessagesFromFolder(Folder folder)668     private List<MessageDumpElement> getRecentMessagesFromFolder(Folder folder) {
669         Uri smsUri = null;
670         Uri mmsUri = null;
671         if (folder == Folder.INBOX) {
672             smsUri = Sms.Inbox.CONTENT_URI;
673             mmsUri = Mms.Inbox.CONTENT_URI;
674         } else if (folder == Folder.SENT) {
675             smsUri = Sms.Sent.CONTENT_URI;
676             mmsUri = Mms.Sent.CONTENT_URI;
677         } else {
678             warn("getRecentMessagesFromFolder: Failed, unsupported folder=" + folder);
679             return null;
680         }
681 
682         ArrayList<MessageDumpElement> messages = new ArrayList<MessageDumpElement>();
683         for (Uri uri : new Uri[] {smsUri, mmsUri}) {
684             messages.addAll(getMessagesFromUri(uri));
685         }
686         verbose(
687                 "getRecentMessagesFromFolder: "
688                         + folder
689                         + ", "
690                         + messages.size()
691                         + " messages found.");
692 
693         Collections.sort(messages);
694         if (messages.size() > NUM_RECENT_MSGS_TO_DUMP) {
695             return messages.subList(0, NUM_RECENT_MSGS_TO_DUMP);
696         }
697         return messages;
698     }
699 
getMessagesFromUri(Uri uri)700     private List<MessageDumpElement> getMessagesFromUri(Uri uri) {
701         debug("getMessagesFromUri: uri=" + uri);
702         ArrayList<MessageDumpElement> messages = new ArrayList<MessageDumpElement>();
703 
704         if (mSubscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
705             warn("getMessagesFromUri: Failed, no subscription ID");
706             return messages;
707         }
708 
709         Type type = getMessageTypeFromUri(uri);
710         if (type == Type.UNKNOWN) {
711             warn("getMessagesFromUri: unknown message type");
712             return messages;
713         }
714 
715         String[] selectionArgs = new String[] {Integer.toString(mSubscriptionId)};
716         String limit = " LIMIT " + NUM_RECENT_MSGS_TO_DUMP;
717         String[] projection = null;
718         String selectionClause = null;
719         String threadIdColumnName = null;
720         String timestampColumnName = null;
721 
722         if (type == Type.SMS) {
723             projection = new String[] {BaseColumns._ID, Sms.THREAD_ID, Sms.DATE};
724             selectionClause = Sms.SUBSCRIPTION_ID + " =? ";
725             threadIdColumnName = Sms.THREAD_ID;
726             timestampColumnName = Sms.DATE;
727         } else if (type == Type.MMS) {
728             projection = new String[] {BaseColumns._ID, Mms.THREAD_ID, Mms.DATE};
729             selectionClause = Mms.SUBSCRIPTION_ID + " =? ";
730             threadIdColumnName = Mms.THREAD_ID;
731             timestampColumnName = Mms.DATE;
732         }
733 
734         Cursor cursor =
735                 mResolver.query(
736                         uri,
737                         projection,
738                         selectionClause,
739                         selectionArgs,
740                         timestampColumnName + " DESC" + limit);
741 
742         try {
743             if (cursor == null) {
744                 warn("getMessagesFromUri: null cursor for uri=" + uri);
745                 return messages;
746             }
747             verbose("Number of rows in cursor = " + cursor.getCount() + ", for uri=" + uri);
748 
749             cursor.moveToPosition(-1);
750             while (cursor.moveToNext()) {
751                 // Even though {@link storeSms} and {@link storeMms} use Uris that contain the
752                 // folder name (e.g., {@code Sms.Inbox.CONTENT_URI}), the Uri returned by
753                 // {@link ContentResolver#insert} does not (e.g., {@code Sms.CONTENT_URI}).
754                 // Therefore, the Uris in the keyset of {@code mUriToHandleMap} do not contain
755                 // the folder name, but unfortunately, the Uri passed in to query the database
756                 // does contains the folder name, so we can't simply append messageId to the
757                 // passed-in Uri.
758                 String messageId = cursor.getString(cursor.getColumnIndex(BaseColumns._ID));
759                 Uri messageUri =
760                         Uri.withAppendedPath(
761                                 type == Type.SMS ? Sms.CONTENT_URI : Mms.CONTENT_URI, messageId);
762 
763                 MessageStatus handleAndStatus = mUriToHandleMap.get(messageUri);
764                 String messageHandle = "<unknown>";
765                 if (handleAndStatus == null) {
766                     warn("getMessagesFromUri: no entry for message uri=" + messageUri);
767                 } else {
768                     messageHandle = handleAndStatus.mHandle;
769                 }
770 
771                 long timestamp = cursor.getLong(cursor.getColumnIndex(timestampColumnName));
772                 // TODO: why does `storeMms` truncate down to the seconds instead of keeping it
773                 // millisec, like `storeSms`?
774                 if (type == Type.MMS) {
775                     timestamp *= 1000L;
776                 }
777 
778                 messages.add(
779                         new MessageDumpElement(
780                                 messageHandle,
781                                 messageUri,
782                                 timestamp,
783                                 cursor.getLong(cursor.getColumnIndex(threadIdColumnName)),
784                                 type));
785             }
786         } catch (Exception e) {
787             warn("Exception when querying db for dumpsys", e);
788         } finally {
789             cursor.close();
790         }
791         return messages;
792     }
793 
getMessageTypeFromUri(Uri uri)794     private Type getMessageTypeFromUri(Uri uri) {
795         if (Sms.CONTENT_URI.equals(uri)
796                 || Sms.Inbox.CONTENT_URI.equals(uri)
797                 || Sms.Sent.CONTENT_URI.equals(uri)) {
798             return Type.SMS;
799         } else if (Mms.CONTENT_URI.equals(uri)
800                 || Mms.Inbox.CONTENT_URI.equals(uri)
801                 || Mms.Sent.CONTENT_URI.equals(uri)) {
802             return Type.MMS;
803         } else {
804             return Type.UNKNOWN;
805         }
806     }
807 
dump(StringBuilder sb)808     public void dump(StringBuilder sb) {
809         sb.append("    Device Message DB:");
810         sb.append("\n      Subscription ID: " + mSubscriptionId);
811         if (mSubscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
812             sb.append(
813                     "\n      SMS Messages (Inbox/Sent/Total): "
814                             + getStoredMessagesCount(Sms.Inbox.CONTENT_URI)
815                             + " / "
816                             + getStoredMessagesCount(Sms.Sent.CONTENT_URI)
817                             + " / "
818                             + getStoredMessagesCount(Sms.CONTENT_URI));
819 
820             sb.append(
821                     "\n      MMS Messages (Inbox/Sent/Total): "
822                             + getStoredMessagesCount(Mms.Inbox.CONTENT_URI)
823                             + " / "
824                             + getStoredMessagesCount(Mms.Sent.CONTENT_URI)
825                             + " / "
826                             + getStoredMessagesCount(Mms.CONTENT_URI));
827 
828             sb.append("\n      Threads: " + getStoredMessagesCount(Threads.CONTENT_URI));
829 
830             sb.append("\n      Most recent 'Sent' messages:");
831             sb.append("\n        " + MessageDumpElement.getFormattedColumnNames());
832             for (MessageDumpElement e : getRecentMessagesFromFolder(Folder.SENT)) {
833                 sb.append("\n        " + e);
834             }
835             sb.append("\n      Most recent 'Inbox' messages:");
836             sb.append("\n        " + MessageDumpElement.getFormattedColumnNames());
837             for (MessageDumpElement e : getRecentMessagesFromFolder(Folder.INBOX)) {
838                 sb.append("\n        " + e);
839             }
840         }
841         sb.append("\n");
842     }
843 
844     /**
845      * MessageStatus
846      *
847      * <p>Helper class to store associations between remote and local provider based on message
848      * handle and read status
849      */
850     static class MessageStatus {
851 
852         String mHandle;
853         int mRead;
854 
MessageStatus(String handle, int read)855         MessageStatus(String handle, int read) {
856             mHandle = handle;
857             mRead = read;
858         }
859 
860         @Override
equals(Object other)861         public boolean equals(Object other) {
862             return ((other instanceof MessageStatus)
863                     && ((MessageStatus) other).mHandle.equals(mHandle));
864         }
865     }
866 
867     @SuppressWarnings("GoodTime") // Use system time zone to render times for logging
toDatetimeString(long epochMillis)868     private static String toDatetimeString(long epochMillis) {
869         return DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS")
870                 .format(
871                         Instant.ofEpochMilli(epochMillis)
872                                 .atZone(ZoneId.systemDefault())
873                                 .toLocalDateTime());
874     }
875 
876     private static class MessageDumpElement implements Comparable<MessageDumpElement> {
877         private String mMessageHandle;
878         private long mTimestamp;
879         private Type mType;
880         private long mThreadId;
881         private Uri mUri;
882 
MessageDumpElement(String handle, Uri uri, long timestamp, long threadId, Type type)883         MessageDumpElement(String handle, Uri uri, long timestamp, long threadId, Type type) {
884             mMessageHandle = handle;
885             mTimestamp = timestamp;
886             mUri = uri;
887             mThreadId = threadId;
888             mType = type;
889         }
890 
getFormattedColumnNames()891         public static String getFormattedColumnNames() {
892             return String.format(
893                     "%-19s %s %-16s %s %s", "Timestamp", "ThreadId", "Handle", "Type", "Uri");
894         }
895 
896         @Override
toString()897         public String toString() {
898             return String.format(
899                     "%-19s %8d %-16s %-4s %s",
900                     toDatetimeString(mTimestamp), mThreadId, mMessageHandle, mType, mUri);
901         }
902 
903         @Override
compareTo(MessageDumpElement e)904         public int compareTo(MessageDumpElement e) {
905             // we want reverse chronological.
906             if (this.mTimestamp < e.mTimestamp) {
907                 return 1;
908             } else if (this.mTimestamp > e.mTimestamp) {
909                 return -1;
910             } else {
911                 return 0;
912             }
913         }
914     }
915 }
916