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