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