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