1 /* 2 * Copyright (C) 2015 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 package com.google.android.auto.mapservice; 18 19 import android.app.Service; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothUuid; 23 import android.bluetooth.SdpMasRecord; 24 import android.bluetooth.client.map.BluetoothMapBmessage; 25 import android.bluetooth.client.map.BluetoothMasClient; 26 import android.bluetooth.client.map.BluetoothMasClient.CharsetType; 27 import android.content.BroadcastReceiver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.os.Binder; 32 import android.os.Bundle; 33 import android.os.IBinder; 34 import android.os.Handler; 35 import android.os.Message; 36 import android.os.RemoteException; 37 import android.util.Log; 38 import android.util.Pair; 39 import com.android.vcard.VCardEntry; 40 import com.android.vcard.VCardProperty; 41 import com.android.vcard.VCardConstants; 42 import com.google.android.auto.mapservice.BluetoothMapManager; 43 import com.google.android.auto.mapservice.BluetoothMapMessage; 44 import com.google.android.auto.mapservice.BluetoothMapMessagesListing; 45 import com.google.android.auto.mapservice.BluetoothMapEventReport; 46 import com.google.android.auto.mapservice.IBluetoothMapService; 47 import com.google.android.auto.mapservice.IBluetoothMapServiceCallbacks; 48 49 import java.lang.ref.WeakReference; 50 import java.util.ArrayList; 51 import java.util.LinkedList; 52 import java.util.List; 53 import java.util.Queue; 54 55 /** 56 * Service to provide a channel for SMS interaction with remote device. 57 * 58 * The service can be used to send/browse text SMS messages and also recieve notifications 59 * for new incoming messages or delivery notifications on sent messages. 60 * 61 * Connection Model 62 * ---------------- 63 * 64 * The service only cares about one device (and one external connection) at a time. Also it is 65 * reactive in nautre, i.e. it will *not* actively look if the connection between here and remote 66 * device has been dropped. What this means is that if the connection does indeed gets dropped - 67 * service will only send a connection failure on the next command that is executed. It is assumed 68 * that the caller can then take appropriate error handling decisions. A Manager can wrap execpted 69 * cases of disconnection (such as adapters on either side being turned off/on). 70 * 71 * Execution Model 72 * --------------- 73 * The service provides following types of commands: 74 * a) connect(): Connect will try to initiate a connection with remote device which 75 * it does not already have with. If the service is already connected it will refuse to do so. When 76 * the device is connected or connection gets failed, onConnect{Failed}() callbacks will be called. 77 * b) disconnect(): Disconnect is a no-callback command which is synchronously disconnect the 78 * remote device. 79 * c) pushMessage, browseMessage (etc): These are user commands which can happen within a connect() 80 * disconnect() session. They will be followed by a onX() callback where X is the user method. In 81 * case there is a snap of connection while executing these commands - the service will fire 82 * onConnectFailed(). The user of this service should appropriately handle those conditions. 83 */ 84 public class BluetoothMapService extends Service { 85 private static final String TAG = "BluetoothMapMceService"; 86 private static final boolean DBG = true; 87 88 private static final int FAIL_CALLBACK = 1; 89 90 // Connection statuses. 91 private static final int DISCONNECTING = 0; 92 private static final int DISCONNECTED = 1; 93 private static final int SDP = 2; 94 private static final int CONNECTING = 3; 95 private static final int CONNECTED = 4; 96 97 // MapServiceHandler message types. 98 private static final int MSG_MAS_SDP = 1; 99 private static final int MSG_MAS_SDP_DONE = 2; 100 private static final int MSG_MAS_CONNECT_DONE = 3; 101 private static final int MSG_ENABLE_NOTIFICATIONS = 4; 102 private static final int MSG_SET_PATH = 5; 103 private static final int MSG_PUSH_MESSAGE = 6; 104 private static final int MSG_GET_MESSAGE = 7; 105 private static final int MSG_GET_MESSAGES_LISTING = 8; 106 107 // SMS is supported via GSM or CDMA is marked by Bit 1 or Bit 2 of the supported features. 108 private static final int SMS_SUPPORT = 6; // (0110) 109 // Folder names. 110 private static final String FOLDER_TELECOM = "telecom"; 111 private static final String FOLDER_MSG = "msg"; 112 private static final String FOLDER_OUTBOX = "outbox"; 113 private static final String FOLDER_INBOX = "inbox"; 114 // By default we will be in the ROOT folder. 115 private String mFolder = ""; 116 117 // Handler to run all service methods in. We don't execute the methods in a separate 118 // thread since the BluetoothMasClient already has its thread to execute long running calls 119 // We still use Handler for synchronization and atomicity of incoming/outgoing operations. 120 private MapServiceHandler mHandler = new MapServiceHandler(this); 121 122 private ServiceBinder mBinder; 123 124 private int mMapConnectionStatus = DISCONNECTED; 125 126 // Bluetooth MAP related variables. 127 private BluetoothDevice mDevice; 128 private BluetoothMasClient mClient; 129 private SdpMasRecord mMasInstance; 130 private IBluetoothMapServiceCallbacks mCallbacks; 131 private Object mCallbacksLock = new Object(); 132 private boolean mEnableNotifications = false; 133 134 // Listen to SDP broadcasts. 135 private final BroadcastReceiver mBtReceiver = new BroadcastReceiver() { 136 @Override 137 public void onReceive(Context context, Intent intent) { 138 if (DBG) { 139 Log.d(TAG, "Received broadcast intent " + intent); 140 } 141 142 if (BluetoothDevice.ACTION_SDP_RECORD.equals(intent.getAction())) { 143 // Check if we have a valid SDP record. 144 SdpMasRecord masRecord = 145 intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD); 146 int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1); 147 if (masRecord == null) { 148 Log.w(TAG, "SDP search ended with no MAS record. Status: " + status); 149 disconnectInternal(false); 150 return; 151 } 152 synchronized (BluetoothMapService.this) { 153 // Since the discovery for MAS record is successful, connect to device. 154 mHandler.obtainMessage(MSG_MAS_SDP_DONE, masRecord).sendToTarget(); 155 } 156 } 157 } 158 }; 159 160 private static class MapServiceHandler extends Handler { 161 private WeakReference<BluetoothMapService> mBluetoothMapService; 162 MapServiceHandler(BluetoothMapService service)163 public MapServiceHandler(BluetoothMapService service) { 164 mBluetoothMapService = new WeakReference<BluetoothMapService>(service); 165 } 166 167 @Override handleMessage(Message msg)168 public void handleMessage(Message msg) { 169 BluetoothMapService service = mBluetoothMapService.get(); 170 // If the service has been GCed but we have a dangling static class left, just ignore 171 // the request. 172 if (service == null) { 173 return; 174 } 175 176 if (DBG) { 177 Log.d(TAG, "Handling " + msg); 178 } 179 180 // If this is not a connect() request, then any other message in disconnected state 181 // should be ignored. 182 int connStatus = service.getConnectionStatus(); 183 if (connStatus == DISCONNECTED || connStatus == DISCONNECTING) { 184 Log.d(TAG, 185 "Ignoring msg: " + msg + " because service not connected: " + connStatus); 186 return; 187 } 188 189 switch (msg.what) { 190 case MSG_MAS_SDP: 191 // First step to connection is to figure out the right channel to connect to. 192 BluetoothDevice device = (BluetoothDevice) msg.obj; 193 boolean ret = device.sdpSearch(BluetoothUuid.MAS); 194 if (!ret) { 195 Log.e(TAG, "SDP failed initiation."); 196 service.disconnectInternal(true); 197 } 198 break; 199 200 case MSG_MAS_SDP_DONE: 201 // Check if we have the SMS capability for the MAS record reported. 202 SdpMasRecord sdpRecord = (SdpMasRecord) msg.obj; 203 if (DBG) { 204 Log.d(TAG, "SDP record: " + sdpRecord); 205 } 206 207 if ((sdpRecord.getSupportedMessageTypes() & SMS_SUPPORT) != 0) { 208 service.connectToSdpRecord(sdpRecord); 209 } 210 break; 211 212 case MSG_MAS_CONNECT_DONE: 213 // We have connected successfully, now change into the appropriate directory. 214 this.obtainMessage(MSG_SET_PATH).sendToTarget(); 215 break; 216 217 case MSG_ENABLE_NOTIFICATIONS: 218 boolean status = (boolean) msg.obj; 219 service.enableNotifications(status); 220 break; 221 222 case MSG_SET_PATH: 223 // This case is ONLY used to transition into telecom/msg folder. 224 String currFolder = service.getFolder(); 225 if (DBG) { 226 Log.d(TAG, "Current folder: " + currFolder); 227 } 228 229 if (currFolder.equals("")) { 230 service.setPathDown(FOLDER_TELECOM); 231 } else if (currFolder.endsWith(FOLDER_TELECOM)) { 232 service.setPathDown(FOLDER_MSG); 233 } else if (currFolder.endsWith(FOLDER_MSG)) { 234 service.connectionSuccessful(); 235 } else { 236 Log.e(TAG, "Should not be here. " + currFolder); 237 } 238 break; 239 240 case MSG_PUSH_MESSAGE: 241 service.pushMessage((BluetoothMapMessage) msg.obj); 242 break; 243 244 case MSG_GET_MESSAGE: 245 service.getMessage((String) msg.obj); 246 break; 247 248 case MSG_GET_MESSAGES_LISTING: 249 service.getMessagesListing((String) msg.obj, msg.arg1, msg.arg2); 250 break; 251 252 default: 253 Log.e(TAG, "Invalid message in MapServiceHandler.handleMessage() " + msg.what); 254 break; 255 } 256 } 257 } 258 259 // Handle the callbacks from the BluetoothMasClient (see mClient). 260 private static class BluetoothMapEventHandler extends Handler { 261 private WeakReference<BluetoothMapService> mBluetoothMapService; 262 BluetoothMapEventHandler(BluetoothMapService service)263 public BluetoothMapEventHandler(BluetoothMapService service) { 264 mBluetoothMapService = new WeakReference<BluetoothMapService>(service); 265 } 266 267 @Override handleMessage(Message msg)268 public void handleMessage(Message msg) { 269 BluetoothMapService service = mBluetoothMapService.get(); 270 // If the service has been GCed but we have a dangling static class left, just ignore 271 // the request. 272 if (service == null) { 273 return; 274 } 275 276 if (DBG) { 277 Log.d(TAG, "Received message from MAP client: " + msg); 278 } 279 switch (msg.what) { 280 case BluetoothMasClient.EVENT_CONNECT: 281 if (DBG) { 282 Log.d(TAG, "Connected via OBEX with status " + msg.arg1); 283 } 284 285 if (msg.arg1 == BluetoothMasClient.STATUS_FAILED) { 286 Log.d(TAG, "Remote device disconnected."); 287 service.disconnectInternal(true); 288 return; 289 } else { 290 service.onConnectToSdpDone(); 291 } 292 break; 293 294 case BluetoothMasClient.EVENT_SET_NOTIFICATION_REGISTRATION: 295 if (DBG) { 296 Log.d(TAG, "Set notifications: " + msg.obj); 297 } 298 service.onEnableNotifications(); 299 break; 300 301 case BluetoothMasClient.EVENT_SET_PATH: 302 if (DBG) { 303 Log.d(TAG, "Set path: " + msg.obj); 304 } 305 service.onSetPath((String) msg.obj); 306 break; 307 308 case BluetoothMasClient.EVENT_PUSH_MESSAGE: 309 if (DBG) { 310 Log.d(TAG, "Push message: " + msg.obj); 311 } 312 service.onPushMessage((String) msg.obj); 313 break; 314 315 case BluetoothMasClient.EVENT_EVENT_REPORT: 316 if (DBG) { 317 Log.d(TAG, "Event report: " + msg.obj); 318 } 319 service.onEventReport( 320 (android.bluetooth.client.map.BluetoothMapEventReport) msg.obj); 321 break; 322 323 case BluetoothMasClient.EVENT_GET_MESSAGE: 324 if (DBG) { 325 Log.d(TAG, "New message: " + msg.obj); 326 } 327 service.onGetMessage((BluetoothMapBmessage) msg.obj); 328 break; 329 330 case BluetoothMasClient.EVENT_GET_MESSAGES_LISTING: 331 if (DBG) { 332 Log.d(TAG, "Messages Listing: " + msg.obj); 333 } 334 service.onGetMessagesListing( 335 (ArrayList<android.bluetooth.client.map.BluetoothMapMessage>) msg.obj); 336 break; 337 338 default: 339 Log.w(TAG, "Cannot handle map client event of type: " + msg.what); 340 } 341 } 342 } 343 344 // Interface which defines the capabilities of this service. 345 private static class ServiceBinder extends IBluetoothMapService.Stub { 346 WeakReference<BluetoothMapService> mBluetoothMapService; ServiceBinder(BluetoothMapService service)347 ServiceBinder(BluetoothMapService service) { 348 mBluetoothMapService = new WeakReference<BluetoothMapService>(service); 349 } 350 351 @Override connect( IBluetoothMapServiceCallbacks callbacks, BluetoothDevice device)352 public boolean connect( 353 IBluetoothMapServiceCallbacks callbacks, 354 BluetoothDevice device) { 355 if (callbacks == null || device == null) { 356 throw new IllegalArgumentException("Callback or device cannot be null."); 357 } 358 359 BluetoothMapService service = mBluetoothMapService.get(); 360 if (service != null) { 361 return service.connectInternal(callbacks, device); 362 } else { 363 return false; 364 } 365 } 366 367 @Override disconnect(IBluetoothMapServiceCallbacks callback)368 public void disconnect(IBluetoothMapServiceCallbacks callback) { 369 BluetoothMapService service = mBluetoothMapService.get(); 370 if (service == null) return; 371 372 if (callback == null) { 373 throw new IllegalArgumentException("Callback cannot be null."); 374 } 375 376 IBluetoothMapServiceCallbacks callbackRef = service.mCallbacks; 377 if (service.mCallbacks.asBinder() != callback.asBinder()) { 378 Log.e(TAG, "Original: " + service.mCallbacks.asBinder() + 379 " Given: " + callback.asBinder()); 380 throw new IllegalStateException(BluetoothMapManager.CALLBACK_MISMATCH); 381 } 382 383 service.disconnectInternal(false); 384 return; 385 } 386 387 @Override enableNotifications(IBluetoothMapServiceCallbacks callback, boolean enable)388 public boolean enableNotifications(IBluetoothMapServiceCallbacks callback, boolean enable) { 389 BluetoothMapService service = mBluetoothMapService.get(); 390 if (service == null) return false; 391 392 if (callback == null) { 393 throw new IllegalArgumentException("Callback cannot be null."); 394 } 395 396 synchronized (service) { 397 if (service.mMapConnectionStatus != CONNECTED) { 398 if (DBG) { 399 Log.d(TAG, "enableRegistration: Not connected."); 400 } 401 return false; 402 } else if (service.mCallbacks.asBinder() != callback.asBinder()) { 403 throw new IllegalStateException(BluetoothMapManager.CALLBACK_MISMATCH); 404 } 405 service.mHandler.obtainMessage(MSG_ENABLE_NOTIFICATIONS, enable).sendToTarget(); 406 } 407 return true; 408 } 409 410 @Override pushMessage(IBluetoothMapServiceCallbacks callback, BluetoothMapMessage message)411 public boolean pushMessage(IBluetoothMapServiceCallbacks callback, 412 BluetoothMapMessage message) { 413 BluetoothMapService service = mBluetoothMapService.get(); 414 if (service == null) return false; 415 416 if (DBG) { 417 Log.d(TAG, "pushMessage called."); 418 } 419 if (callback == null || message == null) { 420 throw new IllegalArgumentException("Callback or message cannot be null."); 421 } 422 423 synchronized (service) { 424 if (service.mMapConnectionStatus != CONNECTED) { 425 return false; 426 } else if (service.mCallbacks.asBinder() != callback.asBinder()) { 427 throw new IllegalStateException(BluetoothMapManager.CALLBACK_MISMATCH); 428 } 429 service.mHandler.obtainMessage(MSG_PUSH_MESSAGE, message).sendToTarget(); 430 } 431 return true; 432 } 433 434 @Override getMessage(IBluetoothMapServiceCallbacks callback, String handle)435 public boolean getMessage(IBluetoothMapServiceCallbacks callback, String handle) { 436 BluetoothMapService service = mBluetoothMapService.get(); 437 if (service == null) return false; 438 439 if (DBG) { 440 Log.d(TAG, "getMessge called."); 441 } 442 if (callback == null) { 443 throw new IllegalArgumentException("Callback cannot be null."); 444 } 445 446 synchronized (service) { 447 if (service.mMapConnectionStatus != CONNECTED) { 448 return false; 449 } else if (service.mCallbacks.asBinder() != callback.asBinder()) { 450 throw new IllegalStateException(BluetoothMapManager.CALLBACK_MISMATCH); 451 } 452 service.mHandler.obtainMessage(MSG_GET_MESSAGE, handle).sendToTarget(); 453 } 454 return true; 455 } 456 457 @Override getMessagesListing( IBluetoothMapServiceCallbacks callback, String folder, int count, int offset)458 public boolean getMessagesListing( 459 IBluetoothMapServiceCallbacks callback, String folder, int count, int offset) { 460 BluetoothMapService service = mBluetoothMapService.get(); 461 if (service == null) return false; 462 463 if (DBG) { 464 Log.d(TAG, "getMessgesListing called."); 465 } 466 if (callback == null) { 467 throw new IllegalArgumentException("Callback cannot be null."); 468 } 469 470 if (count < 0) { 471 throw new IllegalArgumentException("Count cannot be < 0: " + count); 472 } 473 474 if (offset < 0) { 475 throw new IllegalArgumentException("Offset cannot be < 0: " + offset); 476 } 477 478 synchronized (service) { 479 if (service.mMapConnectionStatus != CONNECTED) { 480 return false; 481 } else if (service.mCallbacks.asBinder() != callback.asBinder()) { 482 throw new IllegalStateException(BluetoothMapManager.CALLBACK_MISMATCH); 483 } 484 service.mHandler.obtainMessage( 485 MSG_GET_MESSAGES_LISTING, count, offset, folder).sendToTarget(); 486 } 487 return true; 488 } 489 } 490 491 // Death recipient to tell if the binder connection is gone. 492 private final class BinderDeath implements IBinder.DeathRecipient { 493 @Override binderDied()494 public void binderDied() { 495 if (DBG) { 496 Log.d(TAG, "Binder died, disconnecting ..."); 497 } 498 disconnectInternal(false); 499 } 500 } 501 502 @Override onCreate()503 public void onCreate() { 504 super.onCreate(); 505 506 // Initialize binder interface. 507 mBinder = new ServiceBinder(this); 508 509 IntentFilter filter = new IntentFilter(); 510 filter.addAction(BluetoothDevice.ACTION_SDP_RECORD); 511 registerReceiver(mBtReceiver, filter); 512 } 513 514 @Override onDestroy()515 public void onDestroy() { 516 if (DBG) { 517 Log.d(TAG, "Unregistering receiver and shutting down the service."); 518 } 519 disconnectInternal(false); 520 unregisterReceiver(mBtReceiver); 521 } 522 523 @Override onBind(Intent intent)524 public IBinder onBind(Intent intent) { 525 return mBinder; 526 } 527 getConnectionStatus()528 private int getConnectionStatus() { 529 return mMapConnectionStatus; 530 } 531 setConnectionStatus(int status)532 private synchronized void setConnectionStatus(int status) { 533 mMapConnectionStatus = status; 534 } 535 connectToSdpRecord(SdpMasRecord sdpRecord)536 private synchronized void connectToSdpRecord(SdpMasRecord sdpRecord) { 537 if (mMapConnectionStatus != SDP) return; 538 mMapConnectionStatus = CONNECTING; 539 mClient = 540 new BluetoothMasClient( 541 mDevice, sdpRecord, new BluetoothMapEventHandler(this)); 542 mClient.connect(); 543 } 544 onConnectToSdpDone()545 private synchronized void onConnectToSdpDone() { 546 mHandler.obtainMessage(MSG_MAS_CONNECT_DONE).sendToTarget(); 547 } 548 enableNotifications(boolean status)549 private synchronized void enableNotifications(boolean status) { 550 if (mMapConnectionStatus != CONNECTED) return; 551 mClient.setNotificationRegistration(status); 552 mEnableNotifications = status; 553 } 554 getNotificationStatus()555 private boolean getNotificationStatus() { 556 return mEnableNotifications; 557 } 558 setNotificationStatus(boolean status)559 private void setNotificationStatus(boolean status) { 560 mEnableNotifications = status; 561 } 562 setPathDown(String path)563 private synchronized void setPathDown(String path) { 564 if (mMapConnectionStatus != CONNECTING) return; 565 mClient.setFolderDown(path); 566 } 567 onEnableNotifications()568 private synchronized void onEnableNotifications() { 569 if (mMapConnectionStatus != CONNECTED) return; 570 try { 571 mCallbacks.onEnableNotifications(); 572 } catch (RemoteException ex) { 573 disconnectInternalNoLock(false); 574 } 575 } 576 onPushMessage(String handle)577 private synchronized void onPushMessage(String handle) { 578 if (mMapConnectionStatus != CONNECTED) return; 579 try { 580 mCallbacks.onPushMessage(handle); 581 } catch (RemoteException ex) { 582 disconnectInternalNoLock(false); 583 } 584 } 585 onEventReport( android.bluetooth.client.map.BluetoothMapEventReport eventReport)586 private synchronized void onEventReport( 587 android.bluetooth.client.map.BluetoothMapEventReport eventReport) { 588 // Convert the Event Report format from the one spcified by BluetoothMasClient to the one 589 // consumable by the BluetoothMapManager. 590 BluetoothMapEventReport eventReportCallback = new BluetoothMapEventReport(); 591 switch (eventReport.getType()) { 592 case NEW_MESSAGE: 593 eventReportCallback.setType(BluetoothMapEventReport.TYPE_NEW_MESSAGE); 594 eventReportCallback.setHandle(eventReport.getHandle()); 595 eventReportCallback.setFolder(eventReport.getFolder()); 596 break; 597 598 default: 599 Log.e(TAG, "onEventReport cannot understand the report: " + eventReport); 600 return; 601 } 602 603 if (mMapConnectionStatus != CONNECTED) { 604 Log.e(TAG, "onEventReport(): Returning early because not connected: " + 605 mMapConnectionStatus); 606 } 607 608 try { 609 mCallbacks.onEvent(eventReportCallback); 610 } catch (RemoteException ex) { 611 disconnectInternalNoLock(false); 612 } 613 } 614 onGetMessage(BluetoothMapBmessage msg)615 private synchronized void onGetMessage(BluetoothMapBmessage msg) { 616 // Msg encoding. 617 Log.d(TAG, "Msg encoding: " + msg.getEncoding()); 618 619 BluetoothMapMessage retMsg = new BluetoothMapMessage(); 620 621 // Source of message. 622 switch (msg.getType()) { 623 case SMS_GSM: 624 retMsg.setType(BluetoothMapMessage.TYPE_SMS_GSM); 625 break; 626 case SMS_CDMA: 627 retMsg.setType(BluetoothMapMessage.TYPE_SMS_CDMA); 628 break; 629 default: 630 retMsg.setType(BluetoothMapMessage.TYPE_UNKNOWN); 631 Log.w(TAG, "Unknown/Unsupported MAP message type: " + msg.getType()); 632 } 633 634 // Status of message. 635 switch (msg.getStatus()) { 636 case READ: 637 retMsg.setStatus(BluetoothMapMessage.STATUS_READ); 638 break; 639 case UNREAD: 640 retMsg.setStatus(BluetoothMapMessage.STATUS_UNREAD); 641 break; 642 default: 643 retMsg.setStatus(BluetoothMapMessage.STATUS_UNKNOWN); 644 } 645 646 // Folder in which it is stored on remote device. 647 retMsg.setFolder(msg.getFolder()); 648 649 // Set the sender. Since we are receiving the message we don't need to set the recipient 650 // here. We assume the first number is the primary sender. 651 boolean sendRetMsg = true; 652 VCardEntry origin = msg.getOriginator(); 653 if (origin == null) { 654 Log.e(TAG, "No originator found. " + msg); 655 // Return a null object so that the Manager can notify the client of the failure of the 656 // get message call. 657 try { 658 mCallbacks.onGetMessage(null); 659 } catch (RemoteException ex) { 660 disconnectInternalNoLock(false); 661 } 662 sendRetMsg = false; 663 } 664 665 if (origin.getPhoneList() != null && 666 origin.getPhoneList().size() > 0 && 667 origin.getPhoneList().get(0) != null && 668 origin.getPhoneList().get(0).getNumber() != null) { 669 retMsg.setSender(origin.getPhoneList().get(0).getNumber()); 670 } else { 671 sendRetMsg = false; 672 } 673 674 // Set the message. 675 retMsg.setMessage(msg.getBodyContent()); 676 677 if (mMapConnectionStatus != CONNECTED) return; 678 try { 679 if (!sendRetMsg) { 680 Log.e(TAG, "Parsing BluetoothMapBmessage failed." + msg); 681 mCallbacks.onGetMessage(null); 682 } else { 683 mCallbacks.onGetMessage(retMsg); 684 } 685 } catch (RemoteException ex) { 686 disconnectInternalNoLock(false); 687 } 688 } 689 onGetMessagesListing( ArrayList<android.bluetooth.client.map.BluetoothMapMessage> msgsListing)690 private synchronized void onGetMessagesListing( 691 ArrayList<android.bluetooth.client.map.BluetoothMapMessage> msgsListing) { 692 List<BluetoothMapMessagesListing> retMsgsListing = 693 new ArrayList<BluetoothMapMessagesListing>(); 694 695 for (android.bluetooth.client.map.BluetoothMapMessage msg : msgsListing) { 696 BluetoothMapMessagesListing listing = new BluetoothMapMessagesListing(); 697 698 // Transform the various fields to target object. 699 listing.setHandle(msg.getHandle()); 700 listing.setSubject(msg.getSubject()); 701 listing.setDate(msg.getDateTime()); 702 listing.setSender(msg.getSenderName()); 703 704 // TODO: Fill in the rest of the fields. 705 706 retMsgsListing.add(listing); 707 } 708 709 if (mMapConnectionStatus != CONNECTED) return; 710 try { 711 mCallbacks.onGetMessagesListing(retMsgsListing); 712 } catch (RemoteException ex) { 713 disconnectInternalNoLock(false); 714 } 715 } 716 onSetPath(String path)717 private synchronized void onSetPath(String path) { 718 if (path.endsWith(FOLDER_TELECOM)) { 719 mFolder = FOLDER_TELECOM; 720 } else if (path.endsWith(FOLDER_MSG)) { 721 mFolder = FOLDER_MSG; 722 } else { 723 throw new IllegalStateException(TAG + " incorrect change folder: " + path); 724 } 725 mHandler.obtainMessage(MSG_SET_PATH).sendToTarget(); 726 } 727 pushMessage(BluetoothMapMessage msg)728 private synchronized void pushMessage(BluetoothMapMessage msg) { 729 BluetoothMapBmessage bmsg = new BluetoothMapBmessage(); 730 // Set type and status. 731 bmsg.setType(BluetoothMapBmessage.Type.SMS_GSM); 732 bmsg.setStatus(BluetoothMapBmessage.Status.READ); 733 734 // Who to send the message to. 735 VCardEntry dest_entry = new VCardEntry(); 736 VCardProperty dest_entry_phone = new VCardProperty(); 737 dest_entry_phone.setName(VCardConstants.PROPERTY_TEL); 738 dest_entry_phone.addValues(msg.getRecipient()); 739 Log.d(TAG, "Recipient: " + msg.getRecipient()); 740 dest_entry.addProperty(dest_entry_phone); 741 bmsg.addRecipient(dest_entry); 742 743 // Message of the body. 744 bmsg.setBodyContent(msg.getMessage()); 745 746 boolean status = mClient.pushMessage(FOLDER_OUTBOX, bmsg, null); 747 if (status == false) { 748 try { 749 mCallbacks.onPushMessage(null); 750 } catch (RemoteException ex) { 751 disconnectInternalNoLock(false); 752 } 753 } 754 } 755 getMessage(String handle)756 private synchronized void getMessage(String handle) { 757 // Added charset to make it compile. 758 boolean status = mClient.getMessage(handle, false /* attachments */); 759 if (status == false) { 760 try { 761 mCallbacks.onGetMessage(null); 762 } catch (RemoteException ex) { 763 disconnectInternalNoLock(false); 764 } 765 } 766 } 767 getMessagesListing(String folder, int count, int offset)768 private synchronized void getMessagesListing(String folder, int count, int offset) { 769 boolean status = mClient.getMessagesListing( 770 folder, 771 0 /* all parameters */, 772 null /* no filter */, 773 (byte) 0 /* subject length */, 774 count, 775 offset); 776 if (status == false) { 777 try { 778 mCallbacks.onGetMessagesListing(null); 779 } catch (RemoteException ex) { 780 disconnectInternalNoLock(false); 781 } 782 } 783 } 784 connectInternal(IBluetoothMapServiceCallbacks callbacks, BluetoothDevice device)785 private synchronized boolean connectInternal(IBluetoothMapServiceCallbacks callbacks, 786 BluetoothDevice device) { 787 if (mMapConnectionStatus != DISCONNECTED) { 788 Log.d(TAG, "Service not in disconnected state. " + mMapConnectionStatus); 789 return false; 790 } 791 // Change the connection status here so that subsequent connect() calls would return 792 // false in the previous IF statement. 793 mMapConnectionStatus = SDP; 794 795 // Make sure we know about any deaths. 796 try { 797 callbacks.asBinder().linkToDeath(new BinderDeath(), 0); 798 } catch (RemoteException ex) { 799 Log.e(TAG, "", ex); 800 return false; 801 } 802 803 // In order to connect to device we need to do the following: 804 // a) Do a service discovery to check for available MAS instances, connect to one with 805 // SMS availability. 806 // b) On ACTION_SEARCH_INTENT use the record from (a) to connect to device. 807 // c) On callback from (b) do a registernotifications. 808 // d) On callback from (c) change directory into telecom/msg. 809 mDevice = device; 810 mCallbacks = callbacks; 811 mHandler.obtainMessage(MSG_MAS_SDP, device).sendToTarget(); 812 return true; 813 } 814 815 // Removes the connection to remote device and remotes the binder connection to client holding 816 // the manager. 817 // The disconnect call first sets the status as DISCONNECTED so that no further callbacks are 818 // sent to manager. Then it removes all messages from the handler queue. This ensures that 819 // previous (non-inflight) handler messages are discarded. 820 // NOTE: The function should only be called from within the Handler so that its call are 821 // synchornized. Calling from outside the Handler could lead to race conditions w.r.t to 822 // connection status. disconnectInternal(boolean failCallback)823 private synchronized void disconnectInternal(boolean failCallback) { 824 disconnectInternalNoLock(failCallback); 825 } disconnectInternalNoLock(boolean failCallback)826 private void disconnectInternalNoLock(boolean failCallback) { 827 mMapConnectionStatus = DISCONNECTED; 828 clearCommandQueue(); 829 if (mCallbacks != null && failCallback) { 830 try { 831 mCallbacks.onConnectFailed(); 832 } catch (RemoteException ex) { 833 Log.e(TAG, "", ex); 834 } 835 } 836 837 if (mClient != null) { 838 mClient.disconnect(); 839 } 840 841 mClient = null; 842 mDevice = null; 843 mCallbacks = null; 844 mEnableNotifications = false; 845 } 846 847 // Clears all messages from mHandler queue. clearCommandQueue()848 void clearCommandQueue() { 849 // Enumerate all the message types and remove them from the message queue. 850 mHandler.removeMessages(MSG_MAS_SDP); 851 mHandler.removeMessages(MSG_MAS_SDP_DONE); 852 mHandler.removeMessages(MSG_MAS_CONNECT_DONE); 853 mHandler.removeMessages(MSG_ENABLE_NOTIFICATIONS); 854 mHandler.removeMessages(MSG_SET_PATH); 855 mHandler.removeMessages(MSG_PUSH_MESSAGE); 856 mHandler.removeMessages(MSG_GET_MESSAGE); 857 mHandler.removeMessages(MSG_GET_MESSAGES_LISTING); 858 } 859 getCallbacks()860 private IBluetoothMapServiceCallbacks getCallbacks() { 861 return mCallbacks; 862 } 863 getFolder()864 private String getFolder() { 865 return mFolder; 866 } 867 getHandler()868 private Handler getHandler() { 869 return mHandler; 870 } 871 connectionSuccessful()872 private synchronized void connectionSuccessful() { 873 if (mMapConnectionStatus != CONNECTING) return; 874 mMapConnectionStatus = CONNECTED; 875 try { 876 mCallbacks.onConnect(); 877 } catch (RemoteException ex) { 878 Log.e(TAG, "Binder exception. " + ex); 879 disconnectInternalNoLock(false); 880 } 881 } 882 } 883