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