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.Telephony; 28 import android.provider.Telephony.Mms; 29 import android.provider.Telephony.MmsSms; 30 import android.provider.Telephony.Sms; 31 import android.provider.Telephony.Threads; 32 import android.telephony.PhoneNumberUtils; 33 import android.telephony.SubscriptionInfo; 34 import android.telephony.SubscriptionManager; 35 import android.telephony.TelephonyManager; 36 import android.util.ArraySet; 37 import android.util.Log; 38 39 import com.android.bluetooth.Utils; 40 import com.android.bluetooth.map.BluetoothMapbMessageMime; 41 import com.android.bluetooth.map.BluetoothMapbMessageMime.MimePart; 42 import com.android.vcard.VCardConstants; 43 import com.android.vcard.VCardEntry; 44 import com.android.vcard.VCardProperty; 45 46 import com.google.android.mms.pdu.PduHeaders; 47 48 import java.util.ArrayList; 49 import java.util.Arrays; 50 import java.util.HashMap; 51 import java.util.List; 52 import java.util.Set; 53 54 class MapClientContent { 55 56 private static final String INBOX_PATH = "telecom/msg/inbox"; 57 private static final String TAG = "MapClientContent"; 58 private static final int DEFAULT_CHARSET = 106; 59 private static final int ORIGINATOR_ADDRESS_TYPE = 137; 60 private static final int RECIPIENT_ADDRESS_TYPE = 151; 61 62 final BluetoothDevice mDevice; 63 private final Context mContext; 64 private final Callbacks mCallbacks; 65 private final ContentResolver mResolver; 66 ContentObserver mContentObserver; 67 String mPhoneNumber = null; 68 private int mSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; 69 private SubscriptionManager mSubscriptionManager; 70 private TelephonyManager mTelephonyManager; 71 private HashMap<String, Uri> mHandleToUriMap = new HashMap<>(); 72 private HashMap<Uri, MessageStatus> mUriToHandleMap = new HashMap<>(); 73 74 /** 75 * Callbacks 76 * API to notify about statusChanges as observed from the content provider 77 */ 78 interface Callbacks { onMessageStatusChanged(String handle, int status)79 void onMessageStatusChanged(String handle, int status); 80 } 81 82 /** 83 * MapClientContent manages all interactions between Bluetooth and the messaging provider. 84 * 85 * Changes to the database are mirrored between the remote and local providers, specifically new 86 * messages, changes to read status, and removal of messages. 87 * 88 * context: the context that all content provider interactions are conducted 89 * MceStateMachine: the interface to send outbound updates such as when a message is read 90 * locally 91 * device: the associated Bluetooth device used for associating messages with a subscription 92 */ MapClientContent(Context context, Callbacks callbacks, BluetoothDevice device)93 MapClientContent(Context context, Callbacks callbacks, 94 BluetoothDevice device) { 95 mContext = context; 96 mDevice = device; 97 mCallbacks = callbacks; 98 mResolver = mContext.getContentResolver(); 99 100 mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); 101 mTelephonyManager = mContext.getSystemService(TelephonyManager.class); 102 mSubscriptionManager 103 .addSubscriptionInfoRecord(mDevice.getAddress(), Utils.getName(mDevice), 0, 104 SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM); 105 SubscriptionInfo info = mSubscriptionManager 106 .getActiveSubscriptionInfoForIcc(mDevice.getAddress()); 107 if (info != null) { 108 mSubscriptionId = info.getSubscriptionId(); 109 } 110 111 mContentObserver = new ContentObserver(null) { 112 @Override 113 public boolean deliverSelfNotifications() { 114 return false; 115 } 116 117 @Override 118 public void onChange(boolean selfChange) { 119 logV("onChange"); 120 findChangeInDatabase(); 121 } 122 123 @Override 124 public void onChange(boolean selfChange, Uri uri) { 125 logV("onChange" + uri.toString()); 126 findChangeInDatabase(); 127 } 128 }; 129 130 clearMessages(mContext, mSubscriptionId); 131 mResolver.registerContentObserver(Sms.CONTENT_URI, true, mContentObserver); 132 mResolver.registerContentObserver(Mms.CONTENT_URI, true, mContentObserver); 133 mResolver.registerContentObserver(MmsSms.CONTENT_URI, true, mContentObserver); 134 } 135 clearAllContent(Context context)136 static void clearAllContent(Context context) { 137 SubscriptionManager subscriptionManager = 138 context.getSystemService(SubscriptionManager.class); 139 List<SubscriptionInfo> subscriptions = subscriptionManager.getActiveSubscriptionInfoList(); 140 if (subscriptions == null) { 141 Log.w(TAG, "Active subscription list is missing"); 142 return; 143 } 144 for (SubscriptionInfo info : subscriptions) { 145 if (info.getSubscriptionType() == SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM) { 146 clearMessages(context, info.getSubscriptionId()); 147 try { 148 subscriptionManager.removeSubscriptionInfoRecord(info.getIccId(), 149 SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM); 150 } catch (Exception e) { 151 Log.w(TAG, "cleanUp failed: " + e.toString()); 152 } 153 } 154 } 155 } 156 logD(String message)157 private static void logD(String message) { 158 if (MapClientService.DBG) { 159 Log.d(TAG, message); 160 } 161 } 162 logV(String message)163 private static void logV(String message) { 164 if (MapClientService.VDBG) { 165 Log.v(TAG, message); 166 } 167 } 168 169 /** 170 * parseLocalNumber 171 * 172 * Determine the connected phone's number by extracting it from an inbound or outbound mms 173 * message. This number is necessary such that group messages can be displayed correctly. 174 */ parseLocalNumber(Bmessage message)175 void parseLocalNumber(Bmessage message) { 176 if (mPhoneNumber != null) { 177 return; 178 } 179 if (INBOX_PATH.equals(message.getFolder())) { 180 ArrayList<VCardEntry> recipients = message.getRecipients(); 181 if (recipients != null && !recipients.isEmpty()) { 182 mPhoneNumber = PhoneNumberUtils.extractNetworkPortion( 183 getFirstRecipientNumber(message)); 184 } 185 } else { 186 mPhoneNumber = PhoneNumberUtils.extractNetworkPortion(getOriginatorNumber(message)); 187 } 188 189 logV("Found phone number: " + mPhoneNumber); 190 } 191 192 /** 193 * storeMessage 194 * 195 * Store a message in database with the associated handle and timestamp. 196 * The handle is used to associate the local message with the remote message. 197 */ storeMessage(Bmessage message, String handle, Long timestamp)198 void storeMessage(Bmessage message, String handle, Long timestamp) { 199 switch (message.getType()) { 200 case MMS: 201 storeMms(message, handle, timestamp); 202 return; 203 case SMS_CDMA: 204 case SMS_GSM: 205 storeSms(message, handle, timestamp); 206 return; 207 default: 208 logD("Request to store unsupported message type: " + message.getType()); 209 } 210 } 211 storeSms(Bmessage message, String handle, Long timestamp)212 private void storeSms(Bmessage message, String handle, Long timestamp) { 213 logD("storeSms"); 214 logV(message.toString()); 215 VCardEntry originator = message.getOriginator(); 216 String recipients; 217 if (INBOX_PATH.equals(message.getFolder())) { 218 recipients = getOriginatorNumber(message); 219 } else { 220 recipients = getFirstRecipientNumber(message); 221 if (recipients == null) { 222 logD("invalid recipients"); 223 return; 224 } 225 } 226 logV("Received SMS from Number " + recipients); 227 String messageContent; 228 229 Uri contentUri = INBOX_PATH.equalsIgnoreCase(message.getFolder()) ? Sms.Inbox.CONTENT_URI 230 : Sms.Sent.CONTENT_URI; 231 ContentValues values = new ContentValues(); 232 long threadId = getThreadId(message); 233 int readStatus = message.getStatus() == Bmessage.Status.READ ? 1 : 0; 234 235 values.put(Sms.THREAD_ID, threadId); 236 values.put(Sms.ADDRESS, recipients); 237 values.put(Sms.BODY, message.getBodyContent()); 238 values.put(Sms.SUBSCRIPTION_ID, mSubscriptionId); 239 values.put(Sms.DATE, timestamp); 240 values.put(Sms.READ, readStatus); 241 242 Uri results = mResolver.insert(contentUri, values); 243 mHandleToUriMap.put(handle, results); 244 mUriToHandleMap.put(results, new MessageStatus(handle, readStatus)); 245 logD("Map InsertedThread" + results); 246 } 247 248 /** 249 * deleteMessage 250 * remove a message from the local provider based on a remote change 251 */ deleteMessage(String handle)252 void deleteMessage(String handle) { 253 logD("deleting handle" + handle); 254 Uri messageToChange = mHandleToUriMap.get(handle); 255 if (messageToChange != null) { 256 mResolver.delete(messageToChange, null); 257 } 258 } 259 260 261 /** 262 * markRead 263 * mark a message read in the local provider based on a remote change 264 */ markRead(String handle)265 void markRead(String handle) { 266 logD("marking read " + handle); 267 Uri messageToChange = mHandleToUriMap.get(handle); 268 if (messageToChange != null) { 269 ContentValues values = new ContentValues(); 270 values.put(Sms.READ, 1); 271 mResolver.update(messageToChange, values, null); 272 } 273 } 274 275 /** 276 * findChangeInDatabase 277 * compare the current state of the local content provider to the expected state and propagate 278 * changes to the remote. 279 */ findChangeInDatabase()280 private void findChangeInDatabase() { 281 HashMap<Uri, MessageStatus> originalUriToHandleMap; 282 HashMap<Uri, MessageStatus> duplicateUriToHandleMap; 283 284 originalUriToHandleMap = mUriToHandleMap; 285 duplicateUriToHandleMap = new HashMap<>(originalUriToHandleMap); 286 for (Uri uri : new Uri[]{Mms.CONTENT_URI, Sms.CONTENT_URI}) { 287 Cursor cursor = mResolver.query(uri, null, null, null, null); 288 while (cursor.moveToNext()) { 289 Uri index = Uri 290 .withAppendedPath(uri, cursor.getString(cursor.getColumnIndex("_id"))); 291 int readStatus = cursor.getInt(cursor.getColumnIndex(Sms.READ)); 292 MessageStatus currentMessage = duplicateUriToHandleMap.remove(index); 293 if (currentMessage != null && currentMessage.mRead != readStatus) { 294 logV(currentMessage.mHandle); 295 currentMessage.mRead = readStatus; 296 mCallbacks.onMessageStatusChanged(currentMessage.mHandle, 297 BluetoothMapClient.READ); 298 } 299 } 300 } 301 for (HashMap.Entry record : duplicateUriToHandleMap.entrySet()) { 302 logV("Deleted " + ((MessageStatus) record.getValue()).mHandle); 303 originalUriToHandleMap.remove(record.getKey()); 304 mCallbacks.onMessageStatusChanged(((MessageStatus) record.getValue()).mHandle, 305 BluetoothMapClient.DELETED); 306 } 307 } 308 storeMms(Bmessage message, String handle, Long timestamp)309 private void storeMms(Bmessage message, String handle, Long timestamp) { 310 logD("storeMms"); 311 logV(message.toString()); 312 try { 313 parseLocalNumber(message); 314 ContentValues values = new ContentValues(); 315 long threadId = getThreadId(message); 316 BluetoothMapbMessageMime mmsBmessage = new BluetoothMapbMessageMime(); 317 mmsBmessage.parseMsgPart(message.getBodyContent()); 318 int read = message.getStatus() == Bmessage.Status.READ ? 1 : 0; 319 Uri contentUri; 320 int messageBox; 321 if (INBOX_PATH.equalsIgnoreCase(message.getFolder())) { 322 contentUri = Mms.Inbox.CONTENT_URI; 323 messageBox = Mms.MESSAGE_BOX_INBOX; 324 } else { 325 contentUri = Mms.Sent.CONTENT_URI; 326 messageBox = Mms.MESSAGE_BOX_SENT; 327 } 328 logD("Parsed"); 329 values.put(Mms.SUBSCRIPTION_ID, mSubscriptionId); 330 values.put(Mms.THREAD_ID, threadId); 331 values.put(Mms.DATE, timestamp / 1000L); 332 values.put(Mms.TEXT_ONLY, true); 333 values.put(Mms.MESSAGE_BOX, messageBox); 334 values.put(Mms.READ, read); 335 values.put(Mms.SEEN, 0); 336 values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ); 337 values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION); 338 values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL); 339 values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO); 340 values.put(Mms.TRANSACTION_ID, "T" + Long.toHexString(System.currentTimeMillis())); 341 values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO); 342 values.put(Mms.LOCKED, 0); 343 values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related"); 344 values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR); 345 values.put(Mms.MESSAGE_SIZE, mmsBmessage.getSize()); 346 347 Uri results = mResolver.insert(contentUri, values); 348 mHandleToUriMap.put(handle, results); 349 mUriToHandleMap.put(results, new MessageStatus(handle, read)); 350 351 logD("Map InsertedThread" + results); 352 353 for (MimePart part : mmsBmessage.getMimeParts()) { 354 storeMmsPart(part, results); 355 } 356 357 storeAddressPart(message, results); 358 359 String messageContent = mmsBmessage.getMessageAsText(); 360 361 values.put(Mms.Part.CONTENT_TYPE, "plain/text"); 362 values.put(Mms.SUBSCRIPTION_ID, mSubscriptionId); 363 } catch (Exception e) { 364 Log.e(TAG, e.toString()); 365 throw e; 366 } 367 } 368 storeMmsPart(MimePart messagePart, Uri messageUri)369 private Uri storeMmsPart(MimePart messagePart, Uri messageUri) { 370 ContentValues values = new ContentValues(); 371 values.put(Mms.Part.CONTENT_TYPE, "text/plain"); 372 values.put(Mms.Part.CHARSET, DEFAULT_CHARSET); 373 values.put(Mms.Part.FILENAME, "text_1.txt"); 374 values.put(Mms.Part.NAME, "text_1.txt"); 375 values.put(Mms.Part.CONTENT_ID, messagePart.mContentId); 376 values.put(Mms.Part.CONTENT_LOCATION, messagePart.mContentLocation); 377 values.put(Mms.Part.TEXT, messagePart.getDataAsString()); 378 379 Uri contentUri = Uri.parse(messageUri.toString() + "/part"); 380 Uri results = mResolver.insert(contentUri, values); 381 logD("Inserted" + results); 382 return results; 383 } 384 storeAddressPart(Bmessage message, Uri messageUri)385 private void storeAddressPart(Bmessage message, Uri messageUri) { 386 ContentValues values = new ContentValues(); 387 Uri contentUri = Uri.parse(messageUri.toString() + "/addr"); 388 String originator = getOriginatorNumber(message); 389 values.put(Mms.Addr.CHARSET, DEFAULT_CHARSET); 390 391 values.put(Mms.Addr.ADDRESS, originator); 392 values.put(Mms.Addr.TYPE, ORIGINATOR_ADDRESS_TYPE); 393 mResolver.insert(contentUri, values); 394 395 Set<String> messageContacts = new ArraySet<>(); 396 getRecipientsFromMessage(message, messageContacts); 397 for (String recipient : messageContacts) { 398 values.put(Mms.Addr.ADDRESS, recipient); 399 values.put(Mms.Addr.TYPE, RECIPIENT_ADDRESS_TYPE); 400 mResolver.insert(contentUri, values); 401 } 402 } 403 insertIntoMmsTable(String subject)404 private Uri insertIntoMmsTable(String subject) { 405 ContentValues mmsValues = new ContentValues(); 406 mmsValues.put(Mms.TEXT_ONLY, 1); 407 mmsValues.put(Mms.MESSAGE_TYPE, 128); 408 mmsValues.put(Mms.SUBJECT, subject); 409 return mResolver.insert(Mms.CONTENT_URI, mmsValues); 410 } 411 412 /** 413 * cleanUp 414 * clear the subscription info and content on shutdown 415 */ cleanUp()416 void cleanUp() { 417 mResolver.unregisterContentObserver(mContentObserver); 418 clearMessages(mContext, mSubscriptionId); 419 try { 420 mSubscriptionManager.removeSubscriptionInfoRecord(mDevice.getAddress(), 421 SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM); 422 } catch (Exception e) { 423 Log.w(TAG, "cleanUp failed: " + e.toString()); 424 } 425 } 426 427 /** 428 * clearMessages 429 * clean up the content provider on startup 430 */ clearMessages(Context context, int subscriptionId)431 private static void clearMessages(Context context, int subscriptionId) { 432 ContentResolver resolver = context.getContentResolver(); 433 String threads = new String(); 434 435 Uri uri = Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build(); 436 Cursor threadCursor = resolver.query(uri, null, null, null, null); 437 while (threadCursor.moveToNext()) { 438 threads += threadCursor.getInt(threadCursor.getColumnIndex(Threads._ID)) + ", "; 439 } 440 441 resolver.delete(Sms.CONTENT_URI, Sms.SUBSCRIPTION_ID + " =? ", 442 new String[]{Integer.toString(subscriptionId)}); 443 resolver.delete(Mms.CONTENT_URI, Mms.SUBSCRIPTION_ID + " =? ", 444 new String[]{Integer.toString(subscriptionId)}); 445 if (threads.length() > 2) { 446 threads = threads.substring(0, threads.length() - 2); 447 resolver.delete(Threads.CONTENT_URI, Threads._ID + " IN (" + threads + ")", null); 448 } 449 } 450 451 /** 452 * getThreadId 453 * utilize the originator and recipients to obtain the thread id 454 */ getThreadId(Bmessage message)455 private long getThreadId(Bmessage message) { 456 457 Set<String> messageContacts = new ArraySet<>(); 458 String originator = PhoneNumberUtils.extractNetworkPortion(getOriginatorNumber(message)); 459 if (originator != null) { 460 messageContacts.add(originator); 461 } 462 getRecipientsFromMessage(message, messageContacts); 463 // If there is only one contact don't remove it. 464 if (messageContacts.isEmpty()) { 465 return Telephony.Threads.COMMON_THREAD; 466 } else if (messageContacts.size() > 1) { 467 messageContacts.removeIf(number -> (PhoneNumberUtils.areSamePhoneNumber(number, 468 mPhoneNumber, mTelephonyManager.getNetworkCountryIso()))); 469 } 470 471 logV("Contacts = " + messageContacts.toString()); 472 return Telephony.Threads.getOrCreateThreadId(mContext, messageContacts); 473 } 474 getRecipientsFromMessage(Bmessage message, Set<String> messageContacts)475 private void getRecipientsFromMessage(Bmessage message, Set<String> messageContacts) { 476 List<VCardEntry> recipients = message.getRecipients(); 477 for (VCardEntry recipient : recipients) { 478 List<VCardEntry.PhoneData> phoneData = recipient.getPhoneList(); 479 if (phoneData != null && !phoneData.isEmpty()) { 480 messageContacts 481 .add(PhoneNumberUtils.extractNetworkPortion(phoneData.get(0).getNumber())); 482 } 483 } 484 } 485 getOriginatorNumber(Bmessage message)486 private String getOriginatorNumber(Bmessage message) { 487 VCardEntry originator = message.getOriginator(); 488 if (originator == null) { 489 return null; 490 } 491 492 List<VCardEntry.PhoneData> phoneData = originator.getPhoneList(); 493 if (phoneData == null || phoneData.isEmpty()) { 494 return null; 495 } 496 497 return PhoneNumberUtils.extractNetworkPortion(phoneData.get(0).getNumber()); 498 } 499 getFirstRecipientNumber(Bmessage message)500 private String getFirstRecipientNumber(Bmessage message) { 501 List<VCardEntry> recipients = message.getRecipients(); 502 if (recipients == null || recipients.isEmpty()) { 503 return null; 504 } 505 506 List<VCardEntry.PhoneData> phoneData = recipients.get(0).getPhoneList(); 507 if (phoneData == null || phoneData.isEmpty()) { 508 return null; 509 } 510 511 return phoneData.get(0).getNumber(); 512 } 513 514 /** 515 * addThreadContactToEntries 516 * utilizing the thread id fill in the appropriate fields of bmsg with the intended recipients 517 */ addThreadContactsToEntries(Bmessage bmsg, String thread)518 boolean addThreadContactsToEntries(Bmessage bmsg, String thread) { 519 String threadId = Uri.parse(thread).getLastPathSegment(); 520 521 logD("MATCHING THREAD" + threadId); 522 logD(MmsSms.CONTENT_CONVERSATIONS_URI + threadId + "/recipients"); 523 524 Cursor cursor = mResolver 525 .query(Uri.withAppendedPath(MmsSms.CONTENT_CONVERSATIONS_URI, 526 threadId + "/recipients"), 527 null, null, 528 null, null); 529 530 if (cursor.moveToNext()) { 531 logD("Columns" + Arrays.toString(cursor.getColumnNames())); 532 logV("CONTACT LIST: " + cursor.getString(cursor.getColumnIndex("recipient_ids"))); 533 addRecipientsToEntries(bmsg, 534 cursor.getString(cursor.getColumnIndex("recipient_ids")).split(" ")); 535 return true; 536 } else { 537 Log.w(TAG, "Thread Not Found"); 538 return false; 539 } 540 } 541 542 addRecipientsToEntries(Bmessage bmsg, String[] recipients)543 private void addRecipientsToEntries(Bmessage bmsg, String[] recipients) { 544 logV("CONTACT LIST: " + Arrays.toString(recipients)); 545 for (String recipient : recipients) { 546 Cursor cursor = mResolver 547 .query(Uri.parse("content://mms-sms/canonical-address/" + recipient), null, 548 null, null, 549 null); 550 while (cursor.moveToNext()) { 551 String number = cursor.getString(cursor.getColumnIndex(Mms.Addr.ADDRESS)); 552 logV("CONTACT number: " + number); 553 VCardEntry destEntry = new VCardEntry(); 554 VCardProperty destEntryPhone = new VCardProperty(); 555 destEntryPhone.setName(VCardConstants.PROPERTY_TEL); 556 destEntryPhone.addValues(number); 557 destEntry.addProperty(destEntryPhone); 558 bmsg.addRecipient(destEntry); 559 } 560 } 561 } 562 563 /** 564 * MessageStatus 565 * 566 * Helper class to store associations between remote and local provider based on message handle 567 * and read status 568 */ 569 class MessageStatus { 570 571 String mHandle; 572 int mRead; 573 MessageStatus(String handle, int read)574 MessageStatus(String handle, int read) { 575 mHandle = handle; 576 mRead = read; 577 } 578 579 @Override equals(Object other)580 public boolean equals(Object other) { 581 return ((other instanceof MessageStatus) && ((MessageStatus) other).mHandle 582 .equals(mHandle)); 583 } 584 } 585 } 586