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