1 /* 2 * Copyright (C) 2016 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 /** 18 * Bluetooth MAP MCE StateMachine 19 * (Disconnected) 20 * | ^ 21 * CONNECT | | DISCONNECTED 22 * V | 23 * (Connecting) (Disconnecting) 24 * | ^ 25 * CONNECTED | | DISCONNECT 26 * V | 27 * (Connected) 28 * 29 * Valid Transitions: State + Event -> Transition: 30 * 31 * Disconnected + CONNECT -> Connecting 32 * Connecting + CONNECTED -> Connected 33 * Connecting + TIMEOUT -> Disconnecting 34 * Connecting + DISCONNECT/CONNECT -> Defer Message 35 * Connected + DISCONNECT -> Disconnecting 36 * Connected + CONNECT -> Disconnecting + Defer Message 37 * Disconnecting + DISCONNECTED -> (Safe) Disconnected 38 * Disconnecting + TIMEOUT -> (Force) Disconnected 39 * Disconnecting + DISCONNECT/CONNECT : Defer Message 40 */ 41 package com.android.bluetooth.mapclient; 42 43 import static android.Manifest.permission.BLUETOOTH_CONNECT; 44 import static android.Manifest.permission.RECEIVE_SMS; 45 46 import android.app.Activity; 47 import android.app.PendingIntent; 48 import android.bluetooth.BluetoothDevice; 49 import android.bluetooth.BluetoothMapClient; 50 import android.bluetooth.BluetoothProfile; 51 import android.bluetooth.BluetoothUuid; 52 import android.bluetooth.SdpMasRecord; 53 import android.content.Intent; 54 import android.net.Uri; 55 import android.os.Message; 56 import android.provider.Telephony; 57 import android.telecom.PhoneAccount; 58 import android.telephony.SmsManager; 59 import android.util.Log; 60 61 import com.android.bluetooth.BluetoothMetricsProto; 62 import com.android.bluetooth.Utils; 63 import com.android.bluetooth.btservice.MetricsLogger; 64 import com.android.bluetooth.btservice.ProfileService; 65 import com.android.bluetooth.map.BluetoothMapbMessageMime; 66 import com.android.bluetooth.statemachine.IState; 67 import com.android.bluetooth.statemachine.State; 68 import com.android.bluetooth.statemachine.StateMachine; 69 import com.android.internal.annotations.VisibleForTesting; 70 import com.android.vcard.VCardConstants; 71 import com.android.vcard.VCardEntry; 72 import com.android.vcard.VCardProperty; 73 74 import java.util.ArrayList; 75 import java.util.Calendar; 76 import java.util.HashMap; 77 import java.util.HashSet; 78 import java.util.List; 79 import java.util.Set; 80 import java.util.concurrent.ConcurrentHashMap; 81 82 /* The MceStateMachine is responsible for setting up and maintaining a connection to a single 83 * specific Messaging Server Equipment endpoint. Upon connect command an SDP record is retrieved, 84 * a connection to the Message Access Server is created and a request to enable notification of new 85 * messages is sent. 86 */ 87 class MceStateMachine extends StateMachine { 88 // Messages for events handled by the StateMachine 89 static final int MSG_MAS_CONNECTED = 1001; 90 static final int MSG_MAS_DISCONNECTED = 1002; 91 static final int MSG_MAS_REQUEST_COMPLETED = 1003; 92 static final int MSG_MAS_REQUEST_FAILED = 1004; 93 static final int MSG_MAS_SDP_DONE = 1005; 94 static final int MSG_MAS_SDP_FAILED = 1006; 95 static final int MSG_OUTBOUND_MESSAGE = 2001; 96 static final int MSG_INBOUND_MESSAGE = 2002; 97 static final int MSG_NOTIFICATION = 2003; 98 static final int MSG_GET_LISTING = 2004; 99 static final int MSG_GET_MESSAGE_LISTING = 2005; 100 // Set message status to read or deleted 101 static final int MSG_SET_MESSAGE_STATUS = 2006; 102 103 private static final String TAG = "MceStateMachine"; 104 private static final Boolean DBG = MapClientService.DBG; 105 // SAVE_OUTBOUND_MESSAGES defaults to true to place the responsibility of managing content on 106 // Bluetooth, to work with the default Car Messenger. This may need to be set to false if the 107 // messaging app takes that responsibility. 108 private static final Boolean SAVE_OUTBOUND_MESSAGES = true; 109 private static final int DISCONNECT_TIMEOUT = 3000; 110 private static final int CONNECT_TIMEOUT = 10000; 111 private static final int MAX_MESSAGES = 20; 112 private static final int MSG_CONNECT = 1; 113 private static final int MSG_DISCONNECT = 2; 114 private static final int MSG_CONNECTING_TIMEOUT = 3; 115 private static final int MSG_DISCONNECTING_TIMEOUT = 4; 116 // Folder names as defined in Bluetooth.org MAP spec V10 117 private static final String FOLDER_TELECOM = "telecom"; 118 private static final String FOLDER_MSG = "msg"; 119 private static final String FOLDER_OUTBOX = "outbox"; 120 private static final String FOLDER_INBOX = "inbox"; 121 private static final String FOLDER_SENT = "sent"; 122 private static final String INBOX_PATH = "telecom/msg/inbox"; 123 124 125 // Connectivity States 126 private int mPreviousState = BluetoothProfile.STATE_DISCONNECTED; 127 private State mDisconnected; 128 private State mConnecting; 129 private State mConnected; 130 private State mDisconnecting; 131 132 private final BluetoothDevice mDevice; 133 private MapClientService mService; 134 private MasClient mMasClient; 135 private MapClientContent mDatabase; 136 private HashMap<String, Bmessage> mSentMessageLog = new HashMap<>(MAX_MESSAGES); 137 private HashMap<Bmessage, PendingIntent> mSentReceiptRequested = new HashMap<>(MAX_MESSAGES); 138 private HashMap<Bmessage, PendingIntent> mDeliveryReceiptRequested = 139 new HashMap<>(MAX_MESSAGES); 140 private Bmessage.Type mDefaultMessageType = Bmessage.Type.SMS_CDMA; 141 142 /** 143 * An object to hold the necessary meta-data for each message so we can broadcast it alongside 144 * the message content. 145 * 146 * This is necessary because the metadata is inferred or received separately from the actual 147 * message content. 148 * 149 * Note: In the future it may be best to use the entries from the MessageListing in full instead 150 * of this small subset. 151 */ 152 private class MessageMetadata { 153 private final String mHandle; 154 private final Long mTimestamp; 155 private boolean mRead; 156 MessageMetadata(String handle, Long timestamp, boolean read)157 MessageMetadata(String handle, Long timestamp, boolean read) { 158 mHandle = handle; 159 mTimestamp = timestamp; 160 mRead = read; 161 } 162 getHandle()163 public String getHandle() { 164 return mHandle; 165 } 166 getTimestamp()167 public Long getTimestamp() { 168 return mTimestamp; 169 } 170 getRead()171 public synchronized boolean getRead() { 172 return mRead; 173 } 174 setRead(boolean read)175 public synchronized void setRead(boolean read) { 176 mRead = read; 177 } 178 } 179 180 // Map each message to its metadata via the handle 181 private ConcurrentHashMap<String, MessageMetadata> mMessages = 182 new ConcurrentHashMap<String, MessageMetadata>(); 183 MceStateMachine(MapClientService service, BluetoothDevice device)184 MceStateMachine(MapClientService service, BluetoothDevice device) { 185 this(service, device, null); 186 } 187 188 @VisibleForTesting MceStateMachine(MapClientService service, BluetoothDevice device, MasClient masClient)189 MceStateMachine(MapClientService service, BluetoothDevice device, MasClient masClient) { 190 super(TAG); 191 mMasClient = masClient; 192 mService = service; 193 194 mPreviousState = BluetoothProfile.STATE_DISCONNECTED; 195 196 mDevice = device; 197 mDisconnected = new Disconnected(); 198 mConnecting = new Connecting(); 199 mDisconnecting = new Disconnecting(); 200 mConnected = new Connected(); 201 202 203 addState(mDisconnected); 204 addState(mConnecting); 205 addState(mDisconnecting); 206 addState(mConnected); 207 setInitialState(mConnecting); 208 start(); 209 } 210 doQuit()211 public void doQuit() { 212 quitNow(); 213 } 214 215 @Override onQuitting()216 protected void onQuitting() { 217 if (mService != null) { 218 mService.cleanupDevice(mDevice); 219 } 220 } 221 getDevice()222 synchronized BluetoothDevice getDevice() { 223 return mDevice; 224 } 225 onConnectionStateChanged(int prevState, int state)226 private void onConnectionStateChanged(int prevState, int state) { 227 // mDevice == null only at setInitialState 228 if (mDevice == null) { 229 return; 230 } 231 if (DBG) { 232 Log.d(TAG, "Connection state " + mDevice + ": " + prevState + "->" + state); 233 } 234 if (prevState != state && state == BluetoothProfile.STATE_CONNECTED) { 235 MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.MAP_CLIENT); 236 } 237 Intent intent = new Intent(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED); 238 intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); 239 intent.putExtra(BluetoothProfile.EXTRA_STATE, state); 240 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); 241 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 242 mService.sendBroadcast(intent, BLUETOOTH_CONNECT, Utils.getTempAllowlistBroadcastOptions()); 243 } 244 getState()245 public synchronized int getState() { 246 IState currentState = this.getCurrentState(); 247 if (currentState == null || currentState.getClass() == Disconnected.class) { 248 return BluetoothProfile.STATE_DISCONNECTED; 249 } 250 if (currentState.getClass() == Connected.class) { 251 return BluetoothProfile.STATE_CONNECTED; 252 } 253 if (currentState.getClass() == Connecting.class) { 254 return BluetoothProfile.STATE_CONNECTING; 255 } 256 if (currentState.getClass() == Disconnecting.class) { 257 return BluetoothProfile.STATE_DISCONNECTING; 258 } 259 return BluetoothProfile.STATE_DISCONNECTED; 260 } 261 disconnect()262 public boolean disconnect() { 263 if (DBG) { 264 Log.d(TAG, "Disconnect Request " + mDevice.getAddress()); 265 } 266 sendMessage(MSG_DISCONNECT, mDevice); 267 return true; 268 } 269 sendMapMessage(Uri[] contacts, String message, PendingIntent sentIntent, PendingIntent deliveredIntent)270 public synchronized boolean sendMapMessage(Uri[] contacts, String message, 271 PendingIntent sentIntent, PendingIntent deliveredIntent) { 272 if (DBG) { 273 Log.d(TAG, "Send Message " + message); 274 } 275 if (contacts == null || contacts.length <= 0) { 276 return false; 277 } 278 if (this.getCurrentState() == mConnected) { 279 Bmessage bmsg = new Bmessage(); 280 // Set type and status. 281 bmsg.setType(getDefaultMessageType()); 282 bmsg.setStatus(Bmessage.Status.READ); 283 284 for (Uri contact : contacts) { 285 // Who to send the message to. 286 if (DBG) { 287 Log.d(TAG, "Scheme " + contact.getScheme()); 288 } 289 if (PhoneAccount.SCHEME_TEL.equals(contact.getScheme())) { 290 String path = contact.getPath(); 291 if (path != null && path.contains(Telephony.Threads.CONTENT_URI.toString())) { 292 mDatabase.addThreadContactsToEntries(bmsg, contact.getLastPathSegment()); 293 } else { 294 VCardEntry destEntry = new VCardEntry(); 295 VCardProperty destEntryPhone = new VCardProperty(); 296 destEntryPhone.setName(VCardConstants.PROPERTY_TEL); 297 destEntryPhone.addValues(contact.getSchemeSpecificPart()); 298 destEntry.addProperty(destEntryPhone); 299 bmsg.addRecipient(destEntry); 300 if (DBG) { 301 Log.d(TAG, "Sending to phone numbers " + destEntryPhone.getValueList()); 302 } 303 } 304 } else { 305 if (DBG) { 306 Log.w(TAG, "Scheme " + contact.getScheme() + " not supported."); 307 } 308 return false; 309 } 310 } 311 312 // Message of the body. 313 bmsg.setBodyContent(message); 314 if (sentIntent != null) { 315 mSentReceiptRequested.put(bmsg, sentIntent); 316 } 317 if (deliveredIntent != null) { 318 mDeliveryReceiptRequested.put(bmsg, deliveredIntent); 319 } 320 sendMessage(MSG_OUTBOUND_MESSAGE, bmsg); 321 return true; 322 } 323 return false; 324 } 325 getMessage(String handle)326 synchronized boolean getMessage(String handle) { 327 if (DBG) { 328 Log.d(TAG, "getMessage" + handle); 329 } 330 if (this.getCurrentState() == mConnected) { 331 sendMessage(MSG_INBOUND_MESSAGE, handle); 332 return true; 333 } 334 return false; 335 } 336 getUnreadMessages()337 synchronized boolean getUnreadMessages() { 338 if (DBG) { 339 Log.d(TAG, "getMessage"); 340 } 341 if (this.getCurrentState() == mConnected) { 342 sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_INBOX); 343 return true; 344 } 345 return false; 346 } 347 getSupportedFeatures()348 synchronized int getSupportedFeatures() { 349 if (this.getCurrentState() == mConnected && mMasClient != null) { 350 if (DBG) Log.d(TAG, "returning getSupportedFeatures from SDP record"); 351 return mMasClient.getSdpMasRecord().getSupportedFeatures(); 352 } 353 if (DBG) Log.d(TAG, "in getSupportedFeatures, returning 0"); 354 return 0; 355 } 356 setMessageStatus(String handle, int status)357 synchronized boolean setMessageStatus(String handle, int status) { 358 if (DBG) { 359 Log.d(TAG, "setMessageStatus(" + handle + ", " + status + ")"); 360 } 361 if (this.getCurrentState() == mConnected) { 362 RequestSetMessageStatus.StatusIndicator statusIndicator; 363 byte value; 364 switch (status) { 365 case BluetoothMapClient.UNREAD: 366 statusIndicator = RequestSetMessageStatus.StatusIndicator.READ; 367 value = RequestSetMessageStatus.STATUS_NO; 368 break; 369 370 case BluetoothMapClient.READ: 371 statusIndicator = RequestSetMessageStatus.StatusIndicator.READ; 372 value = RequestSetMessageStatus.STATUS_YES; 373 break; 374 375 case BluetoothMapClient.UNDELETED: 376 statusIndicator = RequestSetMessageStatus.StatusIndicator.DELETED; 377 value = RequestSetMessageStatus.STATUS_NO; 378 break; 379 380 case BluetoothMapClient.DELETED: 381 statusIndicator = RequestSetMessageStatus.StatusIndicator.DELETED; 382 value = RequestSetMessageStatus.STATUS_YES; 383 break; 384 385 default: 386 Log.e(TAG, "Invalid parameter for status" + status); 387 return false; 388 } 389 sendMessage(MSG_SET_MESSAGE_STATUS, 0, 0, new RequestSetMessageStatus( 390 handle, statusIndicator, value)); 391 return true; 392 } 393 return false; 394 } 395 getContactURIFromPhone(String number)396 private String getContactURIFromPhone(String number) { 397 return PhoneAccount.SCHEME_TEL + ":" + number; 398 } 399 getDefaultMessageType()400 Bmessage.Type getDefaultMessageType() { 401 synchronized (mDefaultMessageType) { 402 if (Utils.isPtsTestMode()) { 403 return MapUtils.sendMessageType(); 404 } 405 return mDefaultMessageType; 406 } 407 } 408 setDefaultMessageType(SdpMasRecord sdpMasRecord)409 void setDefaultMessageType(SdpMasRecord sdpMasRecord) { 410 int supportedMessageTypes = sdpMasRecord.getSupportedMessageTypes(); 411 synchronized (mDefaultMessageType) { 412 if ((supportedMessageTypes & SdpMasRecord.MessageType.MMS) > 0) { 413 mDefaultMessageType = Bmessage.Type.MMS; 414 } else if ((supportedMessageTypes & SdpMasRecord.MessageType.SMS_CDMA) > 0) { 415 mDefaultMessageType = Bmessage.Type.SMS_CDMA; 416 } else if ((supportedMessageTypes & SdpMasRecord.MessageType.SMS_GSM) > 0) { 417 mDefaultMessageType = Bmessage.Type.SMS_GSM; 418 } 419 } 420 } 421 dump(StringBuilder sb)422 public void dump(StringBuilder sb) { 423 ProfileService.println(sb, "mCurrentDevice: " + mDevice.getAddress() + "(" 424 + Utils.getName(mDevice) + ") " + this.toString()); 425 } 426 427 class Disconnected extends State { 428 @Override enter()429 public void enter() { 430 if (DBG) { 431 Log.d(TAG, "Enter Disconnected: " + getCurrentMessage().what); 432 } 433 onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_DISCONNECTED); 434 mPreviousState = BluetoothProfile.STATE_DISCONNECTED; 435 quit(); 436 } 437 438 @Override exit()439 public void exit() { 440 mPreviousState = BluetoothProfile.STATE_DISCONNECTED; 441 } 442 } 443 444 class Connecting extends State { 445 @Override enter()446 public void enter() { 447 if (DBG) { 448 Log.d(TAG, "Enter Connecting: " + getCurrentMessage().what); 449 } 450 onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_CONNECTING); 451 452 // When commanded to connect begin SDP to find the MAS server. 453 mDevice.sdpSearch(BluetoothUuid.MAS); 454 sendMessageDelayed(MSG_CONNECTING_TIMEOUT, CONNECT_TIMEOUT); 455 } 456 457 @Override processMessage(Message message)458 public boolean processMessage(Message message) { 459 if (DBG) { 460 Log.d(TAG, "processMessage" + this.getName() + message.what); 461 } 462 463 switch (message.what) { 464 case MSG_MAS_SDP_DONE: 465 if (DBG) { 466 Log.d(TAG, "SDP Complete"); 467 } 468 if (mMasClient == null) { 469 SdpMasRecord record = (SdpMasRecord) message.obj; 470 if (record == null) { 471 Log.e(TAG, "Unexpected: SDP record is null for device " 472 + Utils.getName(mDevice)); 473 return NOT_HANDLED; 474 } 475 mMasClient = new MasClient(mDevice, MceStateMachine.this, record); 476 setDefaultMessageType(record); 477 } 478 break; 479 480 case MSG_MAS_CONNECTED: 481 transitionTo(mConnected); 482 break; 483 484 case MSG_MAS_DISCONNECTED: 485 if (mMasClient != null) { 486 mMasClient.shutdown(); 487 } 488 transitionTo(mDisconnected); 489 break; 490 491 case MSG_CONNECTING_TIMEOUT: 492 transitionTo(mDisconnecting); 493 break; 494 495 case MSG_CONNECT: 496 case MSG_DISCONNECT: 497 deferMessage(message); 498 break; 499 500 default: 501 Log.w(TAG, "Unexpected message: " + message.what + " from state:" 502 + this.getName()); 503 return NOT_HANDLED; 504 } 505 return HANDLED; 506 } 507 508 @Override exit()509 public void exit() { 510 mPreviousState = BluetoothProfile.STATE_CONNECTING; 511 removeMessages(MSG_CONNECTING_TIMEOUT); 512 } 513 } 514 515 class Connected extends State { 516 @Override enter()517 public void enter() { 518 if (DBG) { 519 Log.d(TAG, "Enter Connected: " + getCurrentMessage().what); 520 } 521 522 MapClientContent.Callbacks callbacks = new MapClientContent.Callbacks(){ 523 @Override 524 public void onMessageStatusChanged(String handle, int status) { 525 setMessageStatus(handle, status); 526 } 527 }; 528 mDatabase = new MapClientContent(mService, callbacks, mDevice); 529 onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_CONNECTED); 530 if (Utils.isPtsTestMode()) return; 531 532 mMasClient.makeRequest(new RequestSetPath(FOLDER_TELECOM)); 533 mMasClient.makeRequest(new RequestSetPath(FOLDER_MSG)); 534 mMasClient.makeRequest(new RequestSetPath(FOLDER_INBOX)); 535 mMasClient.makeRequest(new RequestGetFolderListing(0, 0)); 536 mMasClient.makeRequest(new RequestSetPath(false)); 537 mMasClient.makeRequest(new RequestSetNotificationRegistration(true)); 538 sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_SENT); 539 sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_INBOX); 540 } 541 542 @Override processMessage(Message message)543 public boolean processMessage(Message message) { 544 switch (message.what) { 545 case MSG_DISCONNECT: 546 if (mDevice.equals(message.obj)) { 547 transitionTo(mDisconnecting); 548 } 549 break; 550 551 case MSG_MAS_DISCONNECTED: 552 deferMessage(message); 553 transitionTo(mDisconnecting); 554 break; 555 556 case MSG_OUTBOUND_MESSAGE: 557 mMasClient.makeRequest( 558 new RequestPushMessage(FOLDER_OUTBOX, (Bmessage) message.obj, null, 559 false, false)); 560 break; 561 562 case MSG_INBOUND_MESSAGE: 563 mMasClient.makeRequest( 564 new RequestGetMessage((String) message.obj, MasClient.CharsetType.UTF_8, 565 false)); 566 break; 567 568 case MSG_NOTIFICATION: 569 processNotification(message); 570 break; 571 572 case MSG_GET_LISTING: 573 mMasClient.makeRequest(new RequestGetFolderListing(0, 0)); 574 break; 575 576 case MSG_GET_MESSAGE_LISTING: 577 // Get latest 50 Unread messages in the last week 578 MessagesFilter filter = new MessagesFilter(); 579 filter.setMessageType(MapUtils.fetchMessageType()); 580 Calendar calendar = Calendar.getInstance(); 581 calendar.add(Calendar.DATE, -7); 582 filter.setPeriod(calendar.getTime(), null); 583 mMasClient.makeRequest(new RequestGetMessagesListing( 584 (String) message.obj, 0, filter, 0, 50, 0)); 585 break; 586 587 case MSG_SET_MESSAGE_STATUS: 588 if (message.obj instanceof RequestSetMessageStatus) { 589 mMasClient.makeRequest((RequestSetMessageStatus) message.obj); 590 } 591 break; 592 593 case MSG_MAS_REQUEST_COMPLETED: 594 if (DBG) { 595 Log.d(TAG, "Completed request"); 596 } 597 if (message.obj instanceof RequestGetMessage) { 598 processInboundMessage((RequestGetMessage) message.obj); 599 } else if (message.obj instanceof RequestPushMessage) { 600 RequestPushMessage requestPushMessage = (RequestPushMessage) message.obj; 601 String messageHandle = requestPushMessage.getMsgHandle(); 602 if (DBG) { 603 Log.d(TAG, "Message Sent......." + messageHandle); 604 } 605 // ignore the top-order byte (converted to string) in the handle for now 606 // some test devices don't populate messageHandle field. 607 // in such cases, no need to wait up for response for such messages. 608 if (messageHandle != null && messageHandle.length() > 2) { 609 if (SAVE_OUTBOUND_MESSAGES) { 610 mDatabase.storeMessage(requestPushMessage.getBMsg(), messageHandle, 611 System.currentTimeMillis()); 612 } 613 mSentMessageLog.put(messageHandle.substring(2), 614 requestPushMessage.getBMsg()); 615 } 616 } else if (message.obj instanceof RequestGetMessagesListing) { 617 processMessageListing((RequestGetMessagesListing) message.obj); 618 } else if (message.obj instanceof RequestSetMessageStatus) { 619 processSetMessageStatus((RequestSetMessageStatus) message.obj); 620 } 621 break; 622 623 case MSG_CONNECT: 624 if (!mDevice.equals(message.obj)) { 625 deferMessage(message); 626 transitionTo(mDisconnecting); 627 } 628 break; 629 630 default: 631 Log.w(TAG, "Unexpected message: " + message.what + " from state:" 632 + this.getName()); 633 return NOT_HANDLED; 634 } 635 return HANDLED; 636 } 637 638 @Override exit()639 public void exit() { 640 mDatabase.cleanUp(); 641 mPreviousState = BluetoothProfile.STATE_CONNECTED; 642 } 643 644 /** 645 * Given a message notification event, will ensure message caching and updating and update 646 * interested applications. 647 * 648 * Message notifications arrive for both remote message reception and Message-Listing object 649 * updates that are triggered by the server side. 650 * 651 * @param msg - A Message object containing a EventReport object describing the remote event 652 */ processNotification(Message msg)653 private void processNotification(Message msg) { 654 if (DBG) { 655 Log.d(TAG, "Handler: msg: " + msg.what); 656 } 657 658 switch (msg.what) { 659 case MSG_NOTIFICATION: 660 EventReport ev = (EventReport) msg.obj; 661 if (ev == null) { 662 Log.w(TAG, "MSG_NOTIFICATION event is null"); 663 return; 664 } 665 if (DBG) { 666 Log.d(TAG, "Message Type = " + ev.getType() 667 + ", Message handle = " + ev.getHandle()); 668 } 669 switch (ev.getType()) { 670 case NEW_MESSAGE: 671 // Infer the timestamp for this message as 'now' and read status false 672 // instead of getting the message listing data for it 673 if (!mMessages.contains(ev.getHandle())) { 674 Calendar calendar = Calendar.getInstance(); 675 MessageMetadata metadata = new MessageMetadata(ev.getHandle(), 676 calendar.getTime().getTime(), false); 677 mMessages.put(ev.getHandle(), metadata); 678 } 679 mMasClient.makeRequest(new RequestGetMessage(ev.getHandle(), 680 MasClient.CharsetType.UTF_8, false)); 681 break; 682 case DELIVERY_SUCCESS: 683 case SENDING_SUCCESS: 684 notifySentMessageStatus(ev.getHandle(), ev.getType()); 685 break; 686 case READ_STATUS_CHANGED: 687 mDatabase.markRead(ev.getHandle()); 688 break; 689 case MESSAGE_DELETED: 690 mDatabase.deleteMessage(ev.getHandle()); 691 break; 692 } 693 } 694 } 695 696 // Sets the specified message status to "read" (from "unread" status, mostly) markMessageRead(RequestGetMessage request)697 private void markMessageRead(RequestGetMessage request) { 698 if (DBG) Log.d(TAG, "markMessageRead" + request.getHandle()); 699 MessageMetadata metadata = mMessages.get(request.getHandle()); 700 metadata.setRead(true); 701 mMasClient.makeRequest(new RequestSetMessageStatus(request.getHandle(), 702 RequestSetMessageStatus.StatusIndicator.READ, RequestSetMessageStatus.STATUS_YES)); 703 } 704 705 // Sets the specified message status to "deleted" markMessageDeleted(RequestGetMessage request)706 private void markMessageDeleted(RequestGetMessage request) { 707 if (DBG) Log.d(TAG, "markMessageDeleted"); 708 mMasClient.makeRequest(new RequestSetMessageStatus(request.getHandle(), 709 RequestSetMessageStatus.StatusIndicator.DELETED, RequestSetMessageStatus.STATUS_YES)); 710 } 711 712 /** 713 * Given the result of a Message Listing request, will cache the contents of each Message in 714 * the Message Listing Object and kick off requests to retrieve message contents from the 715 * remote device. 716 * 717 * @param request - A request object that has been resolved and returned with a message list 718 */ processMessageListing(RequestGetMessagesListing request)719 private void processMessageListing(RequestGetMessagesListing request) { 720 if (DBG) { 721 Log.d(TAG, "processMessageListing"); 722 } 723 ArrayList<com.android.bluetooth.mapclient.Message> messageListing = request.getList(); 724 if (messageListing != null) { 725 // Message listings by spec arrive ordered newest first but we wish to broadcast as 726 // oldest first. Iterate in reverse order so we initiate requests oldest first. 727 for (int i = messageListing.size() - 1; i >= 0; i--) { 728 com.android.bluetooth.mapclient.Message msg = messageListing.get(i); 729 if (DBG) { 730 Log.d(TAG, "getting message for handle " + msg.getHandle()); 731 } 732 // A message listing coming from the server should always have up to date data 733 mMessages.put(msg.getHandle(), new MessageMetadata(msg.getHandle(), 734 msg.getDateTime().getTime(), msg.isRead())); 735 getMessage(msg.getHandle()); 736 } 737 } 738 } 739 processSetMessageStatus(RequestSetMessageStatus request)740 private void processSetMessageStatus(RequestSetMessageStatus request) { 741 if (DBG) { 742 Log.d(TAG, "processSetMessageStatus"); 743 } 744 int result = BluetoothMapClient.RESULT_SUCCESS; 745 if (!request.isSuccess()) { 746 Log.e(TAG, "Set message status failed"); 747 result = BluetoothMapClient.RESULT_FAILURE; 748 } 749 RequestSetMessageStatus.StatusIndicator status = request.getStatusIndicator(); 750 switch (status) { 751 case READ: { 752 Intent intent = new Intent( 753 BluetoothMapClient.ACTION_MESSAGE_READ_STATUS_CHANGED); 754 intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_READ_STATUS, 755 request.getValue() == RequestSetMessageStatus.STATUS_YES ? true : false); 756 intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_HANDLE, request.getHandle()); 757 intent.putExtra(BluetoothMapClient.EXTRA_RESULT_CODE, result); 758 mService.sendBroadcast(intent, BLUETOOTH_CONNECT); 759 break; 760 } 761 case DELETED: { 762 Intent intent = new Intent( 763 BluetoothMapClient.ACTION_MESSAGE_DELETED_STATUS_CHANGED); 764 intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_DELETED_STATUS, 765 request.getValue() == RequestSetMessageStatus.STATUS_YES ? true : false); 766 intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_HANDLE, request.getHandle()); 767 intent.putExtra(BluetoothMapClient.EXTRA_RESULT_CODE, result); 768 mService.sendBroadcast(intent, BLUETOOTH_CONNECT); 769 break; 770 } 771 default: 772 Log.e(TAG, "Unknown status indicator " + status); 773 return; 774 } 775 } 776 777 /** 778 * Given the response of a GetMessage request, will broadcast the bMessage contents on to 779 * all registered applications. 780 * 781 * Inbound messages arrive as bMessage objects following a GetMessage request. GetMessage 782 * uses a message handle that can arrive from both a GetMessageListing request or a Message 783 * Notification event. 784 * 785 * @param request - A request object that has been resolved and returned with message data 786 */ processInboundMessage(RequestGetMessage request)787 private void processInboundMessage(RequestGetMessage request) { 788 Bmessage message = request.getMessage(); 789 if (DBG) { 790 Log.d(TAG, "Notify inbound Message" + message); 791 } 792 793 if (message == null) { 794 return; 795 } 796 mDatabase.storeMessage(message, request.getHandle(), 797 mMessages.get(request.getHandle()).getTimestamp()); 798 if (!INBOX_PATH.equalsIgnoreCase(message.getFolder())) { 799 if (DBG) { 800 Log.d(TAG, "Ignoring message received in " + message.getFolder() + "."); 801 } 802 return; 803 } 804 switch (message.getType()) { 805 case SMS_CDMA: 806 case SMS_GSM: 807 case MMS: 808 if (DBG) { 809 Log.d(TAG, "Body: " + message.getBodyContent()); 810 } 811 if (DBG) { 812 Log.d(TAG, message.toString()); 813 } 814 if (DBG) { 815 Log.d(TAG, "Recipients" + message.getRecipients().toString()); 816 } 817 818 // Grab the message metadata and update the cached read status from the bMessage 819 MessageMetadata metadata = mMessages.get(request.getHandle()); 820 metadata.setRead(request.getMessage().getStatus() == Bmessage.Status.READ); 821 822 Intent intent = new Intent(); 823 intent.setAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED); 824 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); 825 intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_HANDLE, request.getHandle()); 826 intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_TIMESTAMP, 827 metadata.getTimestamp()); 828 intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_READ_STATUS, 829 metadata.getRead()); 830 intent.putExtra(android.content.Intent.EXTRA_TEXT, message.getBodyContent()); 831 VCardEntry originator = message.getOriginator(); 832 if (originator != null) { 833 if (DBG) { 834 Log.d(TAG, originator.toString()); 835 } 836 List<VCardEntry.PhoneData> phoneData = originator.getPhoneList(); 837 if (phoneData != null && phoneData.size() > 0) { 838 String phoneNumber = phoneData.get(0).getNumber(); 839 if (DBG) { 840 Log.d(TAG, "Originator number: " + phoneNumber); 841 } 842 intent.putExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_URI, 843 getContactURIFromPhone(phoneNumber)); 844 } 845 intent.putExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_NAME, 846 originator.getDisplayName()); 847 } 848 if (message.getType() == Bmessage.Type.MMS) { 849 BluetoothMapbMessageMime mmsBmessage = new BluetoothMapbMessageMime(); 850 mmsBmessage.parseMsgPart(message.getBodyContent()); 851 intent.putExtra(android.content.Intent.EXTRA_TEXT, 852 mmsBmessage.getMessageAsText()); 853 ArrayList<VCardEntry> recipients = message.getRecipients(); 854 if (recipients != null && !recipients.isEmpty()) { 855 intent.putExtra(android.content.Intent.EXTRA_CC, 856 getRecipientsUri(recipients)); 857 } 858 } 859 // Only send to the current default SMS app if one exists 860 String defaultMessagingPackage = Telephony.Sms.getDefaultSmsPackage(mService); 861 if (defaultMessagingPackage != null) { 862 intent.setPackage(defaultMessagingPackage); 863 } 864 mService.sendBroadcast(intent, RECEIVE_SMS); 865 break; 866 case EMAIL: 867 default: 868 Log.e(TAG, "Received unhandled type" + message.getType().toString()); 869 break; 870 } 871 } 872 873 /** 874 * Retrieves the URIs of all the participants of a group conversation, besides the sender 875 * of the message. 876 * @param recipients 877 * @return 878 */ getRecipientsUri(ArrayList<VCardEntry> recipients)879 private String[] getRecipientsUri(ArrayList<VCardEntry> recipients) { 880 Set<String> uris = new HashSet<>(); 881 882 for (VCardEntry recipient : recipients) { 883 List<VCardEntry.PhoneData> phoneData = recipient.getPhoneList(); 884 if (phoneData != null && phoneData.size() > 0) { 885 String phoneNumber = phoneData.get(0).getNumber(); 886 if (DBG) { 887 Log.d(TAG, "CC Recipient number: " + phoneNumber); 888 } 889 uris.add(getContactURIFromPhone(phoneNumber)); 890 } 891 } 892 String[] stringUris = new String[uris.size()]; 893 return uris.toArray(stringUris); 894 } 895 notifySentMessageStatus(String handle, EventReport.Type status)896 private void notifySentMessageStatus(String handle, EventReport.Type status) { 897 if (DBG) { 898 Log.d(TAG, "got a status for " + handle + " Status = " + status); 899 } 900 // some test devices don't populate messageHandle field. 901 // in such cases, ignore such messages. 902 if (handle == null || handle.length() <= 2) return; 903 PendingIntent intentToSend = null; 904 // ignore the top-order byte (converted to string) in the handle for now 905 String shortHandle = handle.substring(2); 906 if (status == EventReport.Type.SENDING_FAILURE 907 || status == EventReport.Type.SENDING_SUCCESS) { 908 intentToSend = mSentReceiptRequested.remove(mSentMessageLog.get(shortHandle)); 909 } else if (status == EventReport.Type.DELIVERY_SUCCESS 910 || status == EventReport.Type.DELIVERY_FAILURE) { 911 intentToSend = mDeliveryReceiptRequested.remove(mSentMessageLog.get(shortHandle)); 912 } 913 914 if (intentToSend != null) { 915 try { 916 if (DBG) { 917 Log.d(TAG, "*******Sending " + intentToSend); 918 } 919 int result = Activity.RESULT_OK; 920 if (status == EventReport.Type.SENDING_FAILURE 921 || status == EventReport.Type.DELIVERY_FAILURE) { 922 result = SmsManager.RESULT_ERROR_GENERIC_FAILURE; 923 } 924 intentToSend.send(result); 925 } catch (PendingIntent.CanceledException e) { 926 Log.w(TAG, "Notification Request Canceled" + e); 927 } 928 } else { 929 Log.e(TAG, "Received a notification on message with handle = " 930 + handle + ", but it is NOT found in mSentMessageLog! where did it go?"); 931 } 932 } 933 } 934 935 class Disconnecting extends State { 936 @Override enter()937 public void enter() { 938 if (DBG) { 939 Log.d(TAG, "Enter Disconnecting: " + getCurrentMessage().what); 940 } 941 onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_DISCONNECTING); 942 943 if (mMasClient != null) { 944 mMasClient.makeRequest(new RequestSetNotificationRegistration(false)); 945 mMasClient.shutdown(); 946 sendMessageDelayed(MSG_DISCONNECTING_TIMEOUT, DISCONNECT_TIMEOUT); 947 } else { 948 // MAP was never connected 949 transitionTo(mDisconnected); 950 } 951 } 952 953 @Override processMessage(Message message)954 public boolean processMessage(Message message) { 955 switch (message.what) { 956 case MSG_DISCONNECTING_TIMEOUT: 957 case MSG_MAS_DISCONNECTED: 958 mMasClient = null; 959 transitionTo(mDisconnected); 960 break; 961 962 case MSG_CONNECT: 963 case MSG_DISCONNECT: 964 deferMessage(message); 965 break; 966 967 default: 968 Log.w(TAG, "Unexpected message: " + message.what + " from state:" 969 + this.getName()); 970 return NOT_HANDLED; 971 } 972 return HANDLED; 973 } 974 975 @Override exit()976 public void exit() { 977 mPreviousState = BluetoothProfile.STATE_DISCONNECTING; 978 removeMessages(MSG_DISCONNECTING_TIMEOUT); 979 } 980 } 981 receiveEvent(EventReport ev)982 void receiveEvent(EventReport ev) { 983 if (DBG) { 984 Log.d(TAG, "Message Type = " + ev.getType() 985 + ", Message handle = " + ev.getHandle()); 986 } 987 sendMessage(MSG_NOTIFICATION, ev); 988 } 989 } 990