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 static android.telephony.PhoneNumberUtils.areSamePhoneNumber; 20 import static android.telephony.PhoneNumberUtils.extractNetworkPortion; 21 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothMapClient; 24 import android.content.ContentResolver; 25 import android.content.ContentValues; 26 import android.content.Context; 27 import android.database.ContentObserver; 28 import android.database.Cursor; 29 import android.net.Uri; 30 import android.provider.BaseColumns; 31 import android.provider.Telephony; 32 import android.provider.Telephony.Mms; 33 import android.provider.Telephony.MmsSms; 34 import android.provider.Telephony.Sms; 35 import android.provider.Telephony.Threads; 36 import android.telephony.SubscriptionInfo; 37 import android.telephony.SubscriptionManager; 38 import android.telephony.TelephonyManager; 39 import android.util.ArraySet; 40 import android.util.Log; 41 42 import com.android.bluetooth.Utils; 43 import com.android.bluetooth.map.BluetoothMapbMessageMime; 44 import com.android.bluetooth.map.BluetoothMapbMessageMime.MimePart; 45 import com.android.vcard.VCardConstants; 46 import com.android.vcard.VCardEntry; 47 import com.android.vcard.VCardProperty; 48 49 import com.google.android.mms.pdu.PduHeaders; 50 51 import java.time.Instant; 52 import java.time.ZoneId; 53 import java.time.format.DateTimeFormatter; 54 import java.util.ArrayList; 55 import java.util.Arrays; 56 import java.util.Collections; 57 import java.util.HashMap; 58 import java.util.List; 59 import java.util.Map; 60 import java.util.Objects; 61 import java.util.Set; 62 63 class MapClientContent { 64 private static final String TAG = MapClientContent.class.getSimpleName(); 65 66 private static final String INBOX_PATH = "telecom/msg/inbox"; 67 private static final int DEFAULT_CHARSET = 106; 68 private static final int ORIGINATOR_ADDRESS_TYPE = 137; 69 private static final int RECIPIENT_ADDRESS_TYPE = 151; 70 71 private static final int NUM_RECENT_MSGS_TO_DUMP = 5; 72 73 private enum Type { 74 UNKNOWN, 75 SMS, 76 MMS 77 } 78 79 private enum Folder { 80 UNKNOWN, 81 INBOX, 82 SENT 83 } 84 85 private final HashMap<String, Uri> mHandleToUriMap = new HashMap<>(); 86 private final HashMap<Uri, MessageStatus> mUriToHandleMap = new HashMap<>(); 87 88 final ContentObserver mContentObserver; 89 private final Context mContext; 90 private final BluetoothDevice mDevice; 91 private final Callbacks mCallbacks; 92 private final ContentResolver mResolver; 93 private final SubscriptionManager mSubscriptionManager; 94 private final TelephonyManager mTelephonyManager; 95 96 String mPhoneNumber = null; 97 98 private int mSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; 99 100 /** Callbacks API to notify about statusChanges as observed from the content provider */ 101 interface Callbacks { onMessageStatusChanged(String handle, int status)102 void onMessageStatusChanged(String handle, int status); 103 } 104 105 /** 106 * MapClientContent manages all interactions between Bluetooth and the messaging provider. 107 * 108 * <p>Changes to the database are mirrored between the remote and local providers, specifically 109 * new messages, changes to read status, and removal of messages. 110 * 111 * <p>Object is invalid after cleanUp() is called. 112 * 113 * <p>context: the context that all content provider interactions are conducted MceStateMachine: 114 * the interface to send outbound updates such as when a message is read locally device: the 115 * associated Bluetooth device used for associating messages with a subscription 116 */ MapClientContent(Context context, Callbacks callbacks, BluetoothDevice device)117 MapClientContent(Context context, Callbacks callbacks, BluetoothDevice device) { 118 mContext = context; 119 mDevice = device; 120 mCallbacks = callbacks; 121 mResolver = mContext.getContentResolver(); 122 mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); 123 mTelephonyManager = mContext.getSystemService(TelephonyManager.class); 124 mSubscriptionManager.addSubscriptionInfoRecord( 125 mDevice.getAddress(), 126 Utils.getName(mDevice), 127 0, 128 SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM); 129 SubscriptionInfo info = 130 mSubscriptionManager.getActiveSubscriptionInfoForIcc(mDevice.getAddress()); 131 if (info != null) { 132 mSubscriptionId = info.getSubscriptionId(); 133 } 134 135 mContentObserver = 136 new ContentObserver(null) { 137 @Override 138 public boolean deliverSelfNotifications() { 139 return false; 140 } 141 142 @Override 143 public void onChange(boolean selfChange) { 144 verbose("onChange(self=" + selfChange + ")"); 145 findChangeInDatabase(); 146 } 147 148 @Override 149 public void onChange(boolean selfChange, Uri uri) { 150 verbose("onChange(self=" + selfChange + ", uri=" + uri.toString() + ")"); 151 findChangeInDatabase(); 152 } 153 }; 154 155 clearMessages(mContext, mSubscriptionId); 156 mResolver.registerContentObserver(Sms.CONTENT_URI, true, mContentObserver); 157 mResolver.registerContentObserver(Mms.CONTENT_URI, true, mContentObserver); 158 mResolver.registerContentObserver(MmsSms.CONTENT_URI, true, mContentObserver); 159 } 160 clearAllContent(Context context)161 static void clearAllContent(Context context) { 162 SubscriptionManager subscriptionManager = 163 context.getSystemService(SubscriptionManager.class); 164 List<SubscriptionInfo> subscriptions = subscriptionManager.getActiveSubscriptionInfoList(); 165 if (subscriptions == null) { 166 Log.w(TAG, "[AllDevices] Active subscription list is missing"); 167 return; 168 } 169 for (SubscriptionInfo info : subscriptions) { 170 if (info.getSubscriptionType() == SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM) { 171 clearMessages(context, info.getSubscriptionId()); 172 try { 173 subscriptionManager.removeSubscriptionInfoRecord( 174 info.getIccId(), SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM); 175 } catch (Exception e) { 176 Log.w(TAG, "[AllDevices] cleanUp failed: " + e.toString()); 177 } 178 } 179 } 180 } 181 error(String message)182 private void error(String message) { 183 Log.e(TAG, "[" + mDevice + "] " + message); 184 } 185 warn(String message)186 private void warn(String message) { 187 Log.w(TAG, "[" + mDevice + "] " + message); 188 } 189 warn(String message, Exception e)190 private void warn(String message, Exception e) { 191 Log.w(TAG, "[" + mDevice + "] " + message, e); 192 } 193 info(String message)194 private void info(String message) { 195 Log.i(TAG, "[" + mDevice + "] " + message); 196 } 197 debug(String message)198 private void debug(String message) { 199 Log.d(TAG, "[" + mDevice + "] " + message); 200 } 201 verbose(String message)202 private void verbose(String message) { 203 Log.v(TAG, "[" + mDevice + "] " + message); 204 } 205 206 /** 207 * This number is necessary for thread_id to work properly. thread_id is needed for (group) MMS 208 * messages to be displayed/stitched correctly. 209 */ setRemoteDeviceOwnNumber(String phoneNumber)210 void setRemoteDeviceOwnNumber(String phoneNumber) { 211 mPhoneNumber = phoneNumber; 212 verbose("Remote device " + mDevice.getAddress() + " phone number set to: " + mPhoneNumber); 213 } 214 215 /** 216 * storeMessage 217 * 218 * <p>Store a message in database with the associated handle and timestamp. The handle is used 219 * to associate the local message with the remote message. 220 */ storeMessage(Bmessage message, String handle, Long timestamp, boolean seen)221 void storeMessage(Bmessage message, String handle, Long timestamp, boolean seen) { 222 info( 223 "storeMessage(time=" 224 + timestamp 225 + "[" 226 + toDatetimeString(timestamp) 227 + "]" 228 + ", handle=" 229 + handle 230 + ", type=" 231 + message.getType() 232 + ", folder=" 233 + message.getFolder()); 234 235 switch (message.getType()) { 236 case MMS: 237 storeMms(message, handle, timestamp, seen); 238 return; 239 case SMS_CDMA: 240 case SMS_GSM: 241 storeSms(message, handle, timestamp, seen); 242 return; 243 default: 244 debug("Request to store unsupported message type: " + message.getType()); 245 } 246 } 247 storeSms(Bmessage message, String handle, Long timestamp, boolean seen)248 private void storeSms(Bmessage message, String handle, Long timestamp, boolean seen) { 249 debug("storeSms"); 250 verbose(message.toString()); 251 final String recipients; 252 if (INBOX_PATH.equals(message.getFolder())) { 253 recipients = getOriginatorNumber(message); 254 } else { 255 recipients = getFirstRecipientNumber(message); 256 if (recipients == null) { 257 debug("invalid recipients"); 258 return; 259 } 260 } 261 verbose("Received SMS from Number " + recipients); 262 263 Uri contentUri = 264 INBOX_PATH.equalsIgnoreCase(message.getFolder()) 265 ? Sms.Inbox.CONTENT_URI 266 : Sms.Sent.CONTENT_URI; 267 ContentValues values = new ContentValues(); 268 long threadId = getThreadId(message); 269 int readStatus = message.getStatus() == Bmessage.Status.READ ? 1 : 0; 270 271 values.put(Sms.THREAD_ID, threadId); 272 values.put(Sms.ADDRESS, recipients); 273 values.put(Sms.BODY, message.getBodyContent()); 274 values.put(Sms.SUBSCRIPTION_ID, mSubscriptionId); 275 values.put(Sms.DATE, timestamp); 276 values.put(Sms.READ, readStatus); 277 values.put(Sms.SEEN, seen); 278 279 Uri results = mResolver.insert(contentUri, values); 280 if (results == null) { 281 error("Failed to get SMS URI, insert failed. Dropping message."); 282 return; 283 } 284 285 mHandleToUriMap.put(handle, results); 286 mUriToHandleMap.put(results, new MessageStatus(handle, readStatus)); 287 debug("Map InsertedThread" + results); 288 } 289 290 /** deleteMessage remove a message from the local provider based on a remote change */ deleteMessage(String handle)291 void deleteMessage(String handle) { 292 debug("deleting handle" + handle); 293 Uri messageToChange = mHandleToUriMap.get(handle); 294 if (messageToChange != null) { 295 mResolver.delete(messageToChange, null); 296 } 297 } 298 299 /** markRead mark a message read in the local provider based on a remote change */ markRead(String handle)300 void markRead(String handle) { 301 debug("marking read " + handle); 302 Uri messageToChange = mHandleToUriMap.get(handle); 303 if (messageToChange != null) { 304 ContentValues values = new ContentValues(); 305 values.put(Sms.READ, 1); 306 mResolver.update(messageToChange, values, null); 307 } 308 } 309 310 /** 311 * findChangeInDatabase compare the current state of the local content provider to the expected 312 * state and propagate changes to the remote. 313 */ findChangeInDatabase()314 private void findChangeInDatabase() { 315 HashMap<Uri, MessageStatus> originalUriToHandleMap; 316 HashMap<Uri, MessageStatus> duplicateUriToHandleMap; 317 318 originalUriToHandleMap = mUriToHandleMap; 319 duplicateUriToHandleMap = new HashMap<>(originalUriToHandleMap); 320 for (Uri uri : new Uri[] {Mms.CONTENT_URI, Sms.CONTENT_URI}) { 321 try (Cursor cursor = mResolver.query(uri, null, null, null, null)) { 322 while (cursor.moveToNext()) { 323 Uri index = 324 Uri.withAppendedPath( 325 uri, cursor.getString(cursor.getColumnIndex("_id"))); 326 int readStatus = cursor.getInt(cursor.getColumnIndex(Sms.READ)); 327 MessageStatus currentMessage = duplicateUriToHandleMap.remove(index); 328 if (currentMessage != null && currentMessage.mRead != readStatus) { 329 verbose(currentMessage.mHandle); 330 currentMessage.mRead = readStatus; 331 mCallbacks.onMessageStatusChanged( 332 currentMessage.mHandle, BluetoothMapClient.READ); 333 } 334 } 335 } 336 } 337 for (Map.Entry record : duplicateUriToHandleMap.entrySet()) { 338 verbose("Deleted " + ((MessageStatus) record.getValue()).mHandle); 339 originalUriToHandleMap.remove(record.getKey()); 340 mCallbacks.onMessageStatusChanged( 341 ((MessageStatus) record.getValue()).mHandle, BluetoothMapClient.DELETED); 342 } 343 } 344 storeMms(Bmessage message, String handle, Long timestamp, boolean seen)345 private void storeMms(Bmessage message, String handle, Long timestamp, boolean seen) { 346 debug("storeMms"); 347 verbose(message.toString()); 348 try { 349 ContentValues values = new ContentValues(); 350 long threadId = getThreadId(message); 351 BluetoothMapbMessageMime mmsBmessage = new BluetoothMapbMessageMime(); 352 mmsBmessage.parseMsgPart(message.getBodyContent()); 353 int read = message.getStatus() == Bmessage.Status.READ ? 1 : 0; 354 Uri contentUri; 355 int messageBox; 356 if (INBOX_PATH.equalsIgnoreCase(message.getFolder())) { 357 contentUri = Mms.Inbox.CONTENT_URI; 358 messageBox = Mms.MESSAGE_BOX_INBOX; 359 } else { 360 contentUri = Mms.Sent.CONTENT_URI; 361 messageBox = Mms.MESSAGE_BOX_SENT; 362 } 363 debug("Parsed"); 364 values.put(Mms.SUBSCRIPTION_ID, mSubscriptionId); 365 values.put(Mms.THREAD_ID, threadId); 366 values.put(Mms.DATE, timestamp / 1000L); 367 values.put(Mms.TEXT_ONLY, true); 368 values.put(Mms.MESSAGE_BOX, messageBox); 369 values.put(Mms.READ, read); 370 values.put(Mms.SEEN, seen); 371 values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ); 372 values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION); 373 values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL); 374 values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO); 375 values.put(Mms.TRANSACTION_ID, "T" + Long.toHexString(System.currentTimeMillis())); 376 values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO); 377 values.put(Mms.LOCKED, 0); 378 values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related"); 379 values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR); 380 values.put(Mms.MESSAGE_SIZE, mmsBmessage.getSize()); 381 382 Uri results = mResolver.insert(contentUri, values); 383 if (results == null) { 384 error("Failed to get MMS entry URI. Cannot store MMS parts. Dropping message."); 385 return; 386 } 387 388 mHandleToUriMap.put(handle, results); 389 mUriToHandleMap.put(results, new MessageStatus(handle, read)); 390 391 debug("Map InsertedThread" + results); 392 393 // Some Messenger Applications don't listen to address table changes and only listen 394 // for message content changes. Adding the address parts first makes it so they're 395 // already in the tables when a given app syncs due to content updates. Otherwise, we 396 // risk a race where the address content may not be ready. 397 storeAddressPart(message, results); 398 399 for (MimePart part : mmsBmessage.getMimeParts()) { 400 storeMmsPart(part, results); 401 } 402 } catch (Exception e) { 403 error("Error while storing MMS: " + e.toString()); 404 throw e; 405 } 406 } 407 storeMmsPart(MimePart messagePart, Uri messageUri)408 private Uri storeMmsPart(MimePart messagePart, Uri messageUri) { 409 ContentValues values = new ContentValues(); 410 values.put(Mms.Part.CONTENT_TYPE, "text/plain"); 411 values.put(Mms.Part.CHARSET, DEFAULT_CHARSET); 412 values.put(Mms.Part.FILENAME, "text_1.txt"); 413 values.put(Mms.Part.NAME, "text_1.txt"); 414 values.put(Mms.Part.CONTENT_ID, messagePart.mContentId); 415 values.put(Mms.Part.CONTENT_LOCATION, messagePart.mContentLocation); 416 values.put(Mms.Part.TEXT, messagePart.getDataAsString()); 417 418 Uri contentUri = Uri.parse(messageUri.toString() + "/part"); 419 Uri results = mResolver.insert(contentUri, values); 420 421 if (results == null) { 422 warn("failed to insert MMS part"); 423 return null; 424 } 425 426 debug("Inserted" + results); 427 return results; 428 } 429 storeAddressPart(Bmessage message, Uri messageUri)430 private void storeAddressPart(Bmessage message, Uri messageUri) { 431 ContentValues values = new ContentValues(); 432 Uri contentUri = Uri.parse(messageUri.toString() + "/addr"); 433 String originator = getOriginatorNumber(message); 434 values.put(Mms.Addr.CHARSET, DEFAULT_CHARSET); 435 values.put(Mms.Addr.ADDRESS, originator); 436 values.put(Mms.Addr.TYPE, ORIGINATOR_ADDRESS_TYPE); 437 438 Uri results = mResolver.insert(contentUri, values); 439 if (results == null) { 440 warn("failed to insert originator address"); 441 } 442 443 Set<String> messageContacts = new ArraySet<>(); 444 getRecipientsFromMessage(message, messageContacts); 445 for (String recipient : messageContacts) { 446 values.put(Mms.Addr.ADDRESS, recipient); 447 values.put(Mms.Addr.TYPE, RECIPIENT_ADDRESS_TYPE); 448 results = mResolver.insert(contentUri, values); 449 if (results == null) { 450 warn("failed to insert recipient address"); 451 } 452 } 453 } 454 455 /** cleanUp clear the subscription info and content on shutdown */ cleanUp()456 void cleanUp() { 457 debug( 458 "cleanUp(device=" 459 + Utils.getLoggableAddress(mDevice) 460 + "subscriptionId=" 461 + mSubscriptionId); 462 mResolver.unregisterContentObserver(mContentObserver); 463 clearMessages(mContext, mSubscriptionId); 464 try { 465 mSubscriptionManager.removeSubscriptionInfoRecord( 466 mDevice.getAddress(), SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM); 467 mSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; 468 } catch (Exception e) { 469 warn("cleanUp failed: " + e.toString()); 470 } 471 } 472 473 /** clearMessages clean up the content provider on startup */ clearMessages(Context context, int subscriptionId)474 private static void clearMessages(Context context, int subscriptionId) { 475 Log.d(TAG, "[AllDevices] clearMessages(subscriptionId=" + subscriptionId); 476 477 ContentResolver resolver = context.getContentResolver(); 478 StringBuilder threadsBuilder = new StringBuilder(); 479 480 Uri uri = Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build(); 481 try (Cursor threadCursor = resolver.query(uri, null, null, null, null)) { 482 while (threadCursor.moveToNext()) { 483 threadsBuilder 484 .append(threadCursor.getInt(threadCursor.getColumnIndex(Threads._ID))) 485 .append(", "); 486 } 487 } 488 489 resolver.delete( 490 Sms.CONTENT_URI, 491 Sms.SUBSCRIPTION_ID + " =? ", 492 new String[] {Integer.toString(subscriptionId)}); 493 resolver.delete( 494 Mms.CONTENT_URI, 495 Mms.SUBSCRIPTION_ID + " =? ", 496 new String[] {Integer.toString(subscriptionId)}); 497 if (threadsBuilder.length() > 2) { 498 String threads = threadsBuilder.substring(0, threadsBuilder.length() - 2); 499 resolver.delete(Threads.CONTENT_URI, Threads._ID + " IN (" + threads + ")", null); 500 } 501 } 502 503 /** getThreadId utilize the originator and recipients to obtain the thread id */ getThreadId(Bmessage message)504 private long getThreadId(Bmessage message) { 505 Set<String> messageContacts = new ArraySet<>(); 506 String originator = extractNetworkPortion(getOriginatorNumber(message)); 507 if (originator != null) { 508 messageContacts.add(originator); 509 } 510 getRecipientsFromMessage(message, messageContacts); 511 // If there is only one contact don't remove it. 512 if (messageContacts.isEmpty()) { 513 return Telephony.Threads.COMMON_THREAD; 514 } else if (messageContacts.size() > 1) { 515 if (mPhoneNumber == null) { 516 warn("getThreadId called, mPhoneNumber never found."); 517 } 518 final String networkCountryIso = mTelephonyManager.getNetworkCountryIso(); 519 messageContacts.removeIf( 520 number -> areSamePhoneNumber(number, mPhoneNumber, networkCountryIso)); 521 } 522 523 verbose("Contacts = " + messageContacts.toString()); 524 return Telephony.Threads.getOrCreateThreadId(mContext, messageContacts); 525 } 526 getRecipientsFromMessage(Bmessage message, Set<String> messageContacts)527 private static void getRecipientsFromMessage(Bmessage message, Set<String> messageContacts) { 528 message.getRecipients().stream() 529 .map(recipient -> recipient.getPhoneList()) 530 .filter(phoneData -> phoneData != null && !phoneData.isEmpty()) 531 .map(phoneData -> extractNetworkPortion(phoneData.get(0).getNumber())) 532 .forEach(messageContacts::add); 533 } 534 getOriginatorNumber(Bmessage message)535 private static 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 extractNetworkPortion(phoneData.get(0).getNumber()); 547 } 548 getFirstRecipientNumber(Bmessage message)549 private static 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 final Uri smsUri; 670 final Uri mmsUri; 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 List<MessageDumpElement> messages = new ArrayList<>(); 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 List<MessageDumpElement> messages = new ArrayList<>(); 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 static 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: ").append(mSubscriptionId); 811 if (mSubscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { 812 sb.append("\n SMS Messages (Inbox/Sent/Total): ") 813 .append(getStoredMessagesCount(Sms.Inbox.CONTENT_URI)) 814 .append(" / ") 815 .append(getStoredMessagesCount(Sms.Sent.CONTENT_URI)) 816 .append(" / ") 817 .append(getStoredMessagesCount(Sms.CONTENT_URI)); 818 819 sb.append("\n MMS Messages (Inbox/Sent/Total): ") 820 .append(getStoredMessagesCount(Mms.Inbox.CONTENT_URI)) 821 .append(" / ") 822 .append(getStoredMessagesCount(Mms.Sent.CONTENT_URI)) 823 .append(" / ") 824 .append(getStoredMessagesCount(Mms.CONTENT_URI)); 825 826 sb.append("\n Threads: ").append(getStoredMessagesCount(Threads.CONTENT_URI)); 827 828 sb.append("\n Most recent 'Sent' messages:"); 829 sb.append("\n ").append(MessageDumpElement.getFormattedColumnNames()); 830 for (MessageDumpElement e : getRecentMessagesFromFolder(Folder.SENT)) { 831 sb.append("\n ").append(e); 832 } 833 sb.append("\n Most recent 'Inbox' messages:"); 834 sb.append("\n ").append(MessageDumpElement.getFormattedColumnNames()); 835 for (MessageDumpElement e : getRecentMessagesFromFolder(Folder.INBOX)) { 836 sb.append("\n ").append(e); 837 } 838 } 839 sb.append("\n"); 840 } 841 842 /** 843 * MessageStatus 844 * 845 * <p>Helper class to store associations between remote and local provider based on message 846 * handle and read status 847 */ 848 static class MessageStatus { 849 850 String mHandle; 851 int mRead; 852 MessageStatus(String handle, int read)853 MessageStatus(String handle, int read) { 854 mHandle = handle; 855 mRead = read; 856 } 857 858 @Override equals(Object obj)859 public boolean equals(Object obj) { 860 if (obj == this) { 861 return true; 862 } 863 864 if (!(obj instanceof MessageStatus other)) { 865 return false; 866 } 867 868 return other.mHandle.equals(mHandle); 869 } 870 871 @Override hashCode()872 public int hashCode() { 873 return Objects.hash(mHandle); 874 } 875 } 876 877 @SuppressWarnings("GoodTime") // Use system time zone to render times for logging toDatetimeString(long epochMillis)878 private static String toDatetimeString(long epochMillis) { 879 return DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS") 880 .format( 881 Instant.ofEpochMilli(epochMillis) 882 .atZone(ZoneId.systemDefault()) 883 .toLocalDateTime()); 884 } 885 886 private record MessageDumpElement( 887 String handle, Uri uri, long timestamp, long threadId, Type type) 888 implements Comparable<MessageDumpElement> { 889 getFormattedColumnNames()890 public static String getFormattedColumnNames() { 891 return String.format( 892 "%-19s %s %-16s %s %s", "Timestamp", "ThreadId", "Handle", "Type", "Uri"); 893 } 894 895 @Override toString()896 public String toString() { 897 return String.format( 898 "%-19s %8d %-16s %-4s %s", 899 toDatetimeString(timestamp), threadId, handle, type, uri); 900 } 901 902 @Override compareTo(MessageDumpElement e)903 public int compareTo(MessageDumpElement e) { 904 // we want reverse chronological. 905 if (this.timestamp < e.timestamp) { 906 return 1; 907 } else if (this.timestamp > e.timestamp) { 908 return -1; 909 } else { 910 return 0; 911 } 912 } 913 } 914 } 915