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