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