Home | History | Annotate | Download | only in mapservice
      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 
    163         public MapServiceHandler(BluetoothMapService service) {
    164             mBluetoothMapService = new WeakReference<BluetoothMapService>(service);
    165         }
    166 
    167         @Override
    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 
    263         public BluetoothMapEventHandler(BluetoothMapService service) {
    264             mBluetoothMapService = new WeakReference<BluetoothMapService>(service);
    265         }
    266 
    267         @Override
    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;
    347         ServiceBinder(BluetoothMapService service) {
    348             mBluetoothMapService = new WeakReference<BluetoothMapService>(service);
    349         }
    350 
    351         @Override
    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
    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
    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
    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
    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
    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
    494         public void binderDied() {
    495             if (DBG) {
    496                 Log.d(TAG, "Binder died, disconnecting ...");
    497             }
    498             disconnectInternal(false);
    499         }
    500     }
    501 
    502     @Override
    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
    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
    524     public IBinder onBind(Intent intent) {
    525         return mBinder;
    526     }
    527 
    528     private int getConnectionStatus() {
    529         return mMapConnectionStatus;
    530     }
    531 
    532     private synchronized void setConnectionStatus(int status) {
    533         mMapConnectionStatus = status;
    534     }
    535 
    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 
    545     private synchronized void onConnectToSdpDone() {
    546         mHandler.obtainMessage(MSG_MAS_CONNECT_DONE).sendToTarget();
    547     }
    548 
    549     private synchronized void enableNotifications(boolean status) {
    550         if (mMapConnectionStatus != CONNECTED) return;
    551         mClient.setNotificationRegistration(status);
    552         mEnableNotifications = status;
    553     }
    554 
    555     private boolean getNotificationStatus() {
    556         return mEnableNotifications;
    557     }
    558 
    559     private void setNotificationStatus(boolean status) {
    560         mEnableNotifications = status;
    561     }
    562 
    563     private synchronized void setPathDown(String path) {
    564         if (mMapConnectionStatus != CONNECTING) return;
    565         mClient.setFolderDown(path);
    566     }
    567 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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.
    823     private synchronized void disconnectInternal(boolean failCallback) {
    824         disconnectInternalNoLock(failCallback);
    825     }
    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.
    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 
    860     private IBluetoothMapServiceCallbacks getCallbacks() {
    861         return mCallbacks;
    862     }
    863 
    864     private String getFolder() {
    865         return mFolder;
    866     }
    867 
    868     private Handler getHandler() {
    869         return mHandler;
    870     }
    871 
    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