• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 /**
18  * Bluetooth MAP MCE StateMachine
19  *         (Disconnected)
20  *             |    ^
21  *     CONNECT |    | DISCONNECTED
22  *             V    |
23  *    (Connecting) (Disconnecting)
24  *             |    ^
25  *   CONNECTED |    | DISCONNECT
26  *             V    |
27  *           (Connected)
28  *
29  * Valid Transitions: State + Event -> Transition:
30  *
31  * Disconnected + CONNECT -> Connecting
32  * Connecting + CONNECTED -> Connected
33  * Connecting + TIMEOUT -> Disconnecting
34  * Connecting + DISCONNECT/CONNECT -> Defer Message
35  * Connected + DISCONNECT -> Disconnecting
36  * Connected + CONNECT -> Disconnecting + Defer Message
37  * Disconnecting + DISCONNECTED -> (Safe) Disconnected
38  * Disconnecting + TIMEOUT -> (Force) Disconnected
39  * Disconnecting + DISCONNECT/CONNECT : Defer Message
40  */
41 package com.android.bluetooth.mapclient;
42 
43 import static android.Manifest.permission.BLUETOOTH_CONNECT;
44 import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
45 import static android.Manifest.permission.RECEIVE_SMS;
46 
47 import android.app.Activity;
48 import android.app.PendingIntent;
49 import android.bluetooth.BluetoothDevice;
50 import android.bluetooth.BluetoothMapClient;
51 import android.bluetooth.BluetoothProfile;
52 import android.bluetooth.BluetoothUuid;
53 import android.bluetooth.SdpMasRecord;
54 import android.content.Intent;
55 import android.net.Uri;
56 import android.os.Message;
57 import android.os.SystemProperties;
58 import android.provider.Telephony;
59 import android.telecom.PhoneAccount;
60 import android.telephony.SmsManager;
61 import android.util.Log;
62 
63 import com.android.bluetooth.BluetoothMetricsProto;
64 import com.android.bluetooth.Utils;
65 import com.android.bluetooth.btservice.MetricsLogger;
66 import com.android.bluetooth.btservice.ProfileService;
67 import com.android.bluetooth.map.BluetoothMapbMessageMime;
68 import com.android.internal.annotations.VisibleForTesting;
69 import com.android.internal.util.State;
70 import com.android.internal.util.StateMachine;
71 import com.android.vcard.VCardConstants;
72 import com.android.vcard.VCardEntry;
73 import com.android.vcard.VCardProperty;
74 
75 import java.time.Instant;
76 import java.util.ArrayList;
77 import java.util.Calendar;
78 import java.util.HashMap;
79 import java.util.HashSet;
80 import java.util.List;
81 import java.util.Set;
82 import java.util.concurrent.ConcurrentHashMap;
83 
84 /* The MceStateMachine is responsible for setting up and maintaining a connection to a single
85  * specific Messaging Server Equipment endpoint.  Upon connect command an SDP record is retrieved,
86  * a connection to the Message Access Server is created and a request to enable notification of new
87  * messages is sent.
88  */
89 class MceStateMachine extends StateMachine {
90     // Messages for events handled by the StateMachine
91     static final int MSG_MAS_CONNECTED = 1001;
92     static final int MSG_MAS_DISCONNECTED = 1002;
93     static final int MSG_MAS_REQUEST_COMPLETED = 1003;
94     static final int MSG_MAS_REQUEST_FAILED = 1004;
95     static final int MSG_MAS_SDP_DONE = 1005;
96     static final int MSG_MAS_SDP_FAILED = 1006;
97     static final int MSG_OUTBOUND_MESSAGE = 2001;
98     static final int MSG_INBOUND_MESSAGE = 2002;
99     static final int MSG_NOTIFICATION = 2003;
100     static final int MSG_GET_LISTING = 2004;
101     static final int MSG_GET_MESSAGE_LISTING = 2005;
102     // Set message status to read or deleted
103     static final int MSG_SET_MESSAGE_STATUS = 2006;
104     static final int MSG_SEARCH_OWN_NUMBER_TIMEOUT = 2007;
105 
106     private static final String TAG = "MceStateMachine";
107     private static final Boolean DBG = MapClientService.DBG;
108     private static final Boolean VDBG = MapClientService.VDBG;
109     // SAVE_OUTBOUND_MESSAGES defaults to true to place the responsibility of managing content on
110     // Bluetooth, to work with the default Car Messenger.  This may need to be set to false if the
111     // messaging app takes that responsibility.
112     private static final Boolean SAVE_OUTBOUND_MESSAGES = true;
113     private static final int DISCONNECT_TIMEOUT = 3000;
114     private static final int CONNECT_TIMEOUT = 10000;
115     private static final int MAX_MESSAGES = 20;
116     private static final int MSG_CONNECT = 1;
117     private static final int MSG_DISCONNECT = 2;
118     private static final int MSG_CONNECTING_TIMEOUT = 3;
119     private static final int MSG_DISCONNECTING_TIMEOUT = 4;
120 
121     private static final boolean MESSAGE_SEEN = true;
122     private static final boolean MESSAGE_NOT_SEEN = false;
123 
124     // Folder names as defined in Bluetooth.org MAP spec V10
125     private static final String FOLDER_TELECOM = "telecom";
126     private static final String FOLDER_MSG = "msg";
127     private static final String FOLDER_OUTBOX = "outbox";
128     static final String FOLDER_INBOX = "inbox";
129     static final String FOLDER_SENT = "sent";
130     private static final String INBOX_PATH = "telecom/msg/inbox";
131 
132     // URI Scheme for messages with email contact
133     private static final String SCHEME_MAILTO = "mailto";
134 
135     // Connectivity States
136     private int mPreviousState = BluetoothProfile.STATE_DISCONNECTED;
137     private int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
138     private State mDisconnected;
139     private State mConnecting;
140     private State mConnected;
141     private State mDisconnecting;
142 
143     private final BluetoothDevice mDevice;
144     private MapClientService mService;
145     private MasClient mMasClient;
146     private MapClientContent mDatabase;
147     private HashMap<String, Bmessage> mSentMessageLog = new HashMap<>(MAX_MESSAGES);
148     private HashMap<Bmessage, PendingIntent> mSentReceiptRequested = new HashMap<>(MAX_MESSAGES);
149     private HashMap<Bmessage, PendingIntent> mDeliveryReceiptRequested =
150             new HashMap<>(MAX_MESSAGES);
151     private Bmessage.Type mDefaultMessageType = Bmessage.Type.SMS_CDMA;
152 
153     // The amount of time for MCE to search for remote device's own phone number before:
154     // (1) MCE registering itself for being notified of the arrival of new messages; and
155     // (2) MCE start downloading existing messages off of MSE.
156     // NOTE: the value is not "final" so that it can be modified in the unit tests
157     @VisibleForTesting
158     static int sOwnNumberSearchTimeoutMs = 3_000;
159 
160     /**
161      * An object to hold the necessary meta-data for each message so we can broadcast it alongside
162      * the message content.
163      *
164      * This is necessary because the metadata is inferred or received separately from the actual
165      * message content.
166      *
167      * Note: In the future it may be best to use the entries from the MessageListing in full instead
168      * of this small subset.
169      */
170     @VisibleForTesting
171     static class MessageMetadata {
172         private final String mHandle;
173         private final Long mTimestamp;
174         private boolean mRead;
175         private boolean mSeen;
176 
MessageMetadata(String handle, Long timestamp, boolean read, boolean seen)177         MessageMetadata(String handle, Long timestamp, boolean read, boolean seen) {
178             mHandle = handle;
179             mTimestamp = timestamp;
180             mRead = read;
181             mSeen = seen;
182         }
183 
getHandle()184         public String getHandle() {
185             return mHandle;
186         }
187 
getTimestamp()188         public Long getTimestamp() {
189             return mTimestamp;
190         }
191 
getRead()192         public synchronized boolean getRead() {
193             return mRead;
194         }
195 
setRead(boolean read)196         public synchronized void setRead(boolean read) {
197             mRead = read;
198         }
199 
getSeen()200         public synchronized boolean getSeen() {
201             return mSeen;
202         }
203 
204     }
205 
206     // Map each message to its metadata via the handle
207     @VisibleForTesting
208     ConcurrentHashMap<String, MessageMetadata> mMessages =
209             new ConcurrentHashMap<String, MessageMetadata>();
210 
MceStateMachine(MapClientService service, BluetoothDevice device)211     MceStateMachine(MapClientService service, BluetoothDevice device) {
212         this(service, device, null, null);
213     }
214 
215     @VisibleForTesting
MceStateMachine(MapClientService service, BluetoothDevice device, MasClient masClient, MapClientContent database)216     MceStateMachine(MapClientService service, BluetoothDevice device, MasClient masClient,
217             MapClientContent database) {
218         super(TAG);
219         mMasClient = masClient;
220         mService = service;
221         mDatabase = database;
222 
223         mPreviousState = BluetoothProfile.STATE_DISCONNECTED;
224 
225         mDevice = device;
226         mDisconnected = new Disconnected();
227         mConnecting = new Connecting();
228         mDisconnecting = new Disconnecting();
229         mConnected = new Connected();
230 
231         addState(mDisconnected);
232         addState(mConnecting);
233         addState(mDisconnecting);
234         addState(mConnected);
235         setInitialState(mConnecting);
236         start();
237     }
238 
doQuit()239     public void doQuit() {
240         quitNow();
241     }
242 
243     @Override
onQuitting()244     protected void onQuitting() {
245         if (mService != null) {
246             mService.cleanupDevice(mDevice);
247         }
248     }
249 
getDevice()250     synchronized BluetoothDevice getDevice() {
251         return mDevice;
252     }
253 
onConnectionStateChanged(int prevState, int state)254     private void onConnectionStateChanged(int prevState, int state) {
255         if (mMostRecentState == state) {
256             return;
257         }
258         // mDevice == null only at setInitialState
259         if (mDevice == null) {
260             return;
261         }
262         if (DBG) {
263             Log.d(TAG, Utils.getLoggableAddress(mDevice) + ": Connection state changed, prev="
264                     + prevState + ", new=" + state);
265         }
266         if (prevState != state && state == BluetoothProfile.STATE_CONNECTED) {
267             MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.MAP_CLIENT);
268         }
269         setState(state);
270         Intent intent = new Intent(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
271         intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
272         intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
273         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
274         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
275         mService.sendBroadcastMultiplePermissions(intent,
276                 new String[] {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED},
277                 Utils.getTempBroadcastOptions());
278     }
279 
setState(int state)280     private synchronized void setState(int state) {
281         mMostRecentState = state;
282     }
283 
getState()284     public synchronized int getState() {
285         return mMostRecentState;
286     }
287 
disconnect()288     public boolean disconnect() {
289         if (DBG) {
290             Log.d(TAG, "Disconnect Request " + mDevice);
291         }
292         sendMessage(MSG_DISCONNECT, mDevice);
293         return true;
294     }
295 
sendMapMessage(Uri[] contacts, String message, PendingIntent sentIntent, PendingIntent deliveredIntent)296     public synchronized boolean sendMapMessage(Uri[] contacts, String message,
297             PendingIntent sentIntent, PendingIntent deliveredIntent) {
298         if (DBG) {
299             Log.d(TAG, Utils.getLoggableAddress(mDevice) + ": Send, message=" + message);
300         }
301         if (contacts == null || contacts.length <= 0) {
302             return false;
303         }
304         if (mMostRecentState == BluetoothProfile.STATE_CONNECTED) {
305             Bmessage bmsg = new Bmessage();
306             // Set type and status.
307             bmsg.setType(getDefaultMessageType());
308             bmsg.setStatus(Bmessage.Status.READ);
309 
310             for (Uri contact : contacts) {
311                 // Who to send the message to.
312                 if (VDBG) {
313                     Log.d(TAG, "Scheme " + contact.getScheme());
314                 }
315                 if (PhoneAccount.SCHEME_TEL.equals(contact.getScheme())) {
316                     String path = contact.getPath();
317                     if (path != null && path.contains(Telephony.Threads.CONTENT_URI.toString())) {
318                         mDatabase.addThreadContactsToEntries(bmsg, contact.getLastPathSegment());
319                     } else {
320                         VCardEntry destEntry = new VCardEntry();
321                         VCardProperty destEntryPhone = new VCardProperty();
322                         destEntryPhone.setName(VCardConstants.PROPERTY_TEL);
323                         destEntryPhone.addValues(contact.getSchemeSpecificPart());
324                         destEntry.addProperty(destEntryPhone);
325                         bmsg.addRecipient(destEntry);
326                         if (VDBG) {
327                             Log.d(TAG, "Sending to phone numbers " + destEntryPhone.getValueList());
328                         }
329                     }
330                 } else if (SCHEME_MAILTO.equals(contact.getScheme())) {
331                     VCardEntry destEntry = new VCardEntry();
332                     VCardProperty destEntryContact = new VCardProperty();
333                     destEntryContact.setName(VCardConstants.PROPERTY_EMAIL);
334                     destEntryContact.addValues(contact.getSchemeSpecificPart());
335                     destEntry.addProperty(destEntryContact);
336                     bmsg.addRecipient(destEntry);
337                     Log.d(TAG, "SPECIFIC: " + contact.getSchemeSpecificPart());
338                     if (DBG) {
339                         Log.d(TAG, "Sending to emails "
340                                 + destEntryContact.getValueList());
341                     }
342                 } else {
343                     Log.w(TAG, "Scheme " + contact.getScheme() + " not supported.");
344                     return false;
345                 }
346             }
347 
348             // Message of the body.
349             bmsg.setBodyContent(message);
350             if (sentIntent != null) {
351                 mSentReceiptRequested.put(bmsg, sentIntent);
352             }
353             if (deliveredIntent != null) {
354                 mDeliveryReceiptRequested.put(bmsg, deliveredIntent);
355             }
356             sendMessage(MSG_OUTBOUND_MESSAGE, bmsg);
357             return true;
358         }
359         return false;
360     }
361 
getMessage(String handle)362     synchronized boolean getMessage(String handle) {
363         if (DBG) {
364             Log.d(TAG, "getMessage" + handle);
365         }
366         if (mMostRecentState == BluetoothProfile.STATE_CONNECTED) {
367             sendMessage(MSG_INBOUND_MESSAGE, handle);
368             return true;
369         }
370         return false;
371     }
372 
getUnreadMessages()373     synchronized boolean getUnreadMessages() {
374         if (DBG) {
375             Log.d(TAG, "getMessage");
376         }
377         if (mMostRecentState == BluetoothProfile.STATE_CONNECTED) {
378             sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_INBOX);
379             return true;
380         }
381         return false;
382     }
383 
getSupportedFeatures()384     synchronized int getSupportedFeatures() {
385         if (mMostRecentState == BluetoothProfile.STATE_CONNECTED && mMasClient != null) {
386             if (DBG) Log.d(TAG, "returning getSupportedFeatures from SDP record");
387             return mMasClient.getSdpMasRecord().getSupportedFeatures();
388         }
389         if (DBG) Log.d(TAG, "in getSupportedFeatures, returning 0");
390         return 0;
391     }
392 
setMessageStatus(String handle, int status)393     synchronized boolean setMessageStatus(String handle, int status) {
394         if (DBG) {
395             Log.d(TAG, "setMessageStatus(" + handle + ", " + status + ")");
396         }
397         if (mMostRecentState == BluetoothProfile.STATE_CONNECTED) {
398             RequestSetMessageStatus.StatusIndicator statusIndicator;
399             byte value;
400             switch (status) {
401                 case BluetoothMapClient.UNREAD:
402                     statusIndicator = RequestSetMessageStatus.StatusIndicator.READ;
403                     value = RequestSetMessageStatus.STATUS_NO;
404                     break;
405 
406                 case BluetoothMapClient.READ:
407                     statusIndicator = RequestSetMessageStatus.StatusIndicator.READ;
408                     value = RequestSetMessageStatus.STATUS_YES;
409                     break;
410 
411                 case BluetoothMapClient.UNDELETED:
412                     statusIndicator = RequestSetMessageStatus.StatusIndicator.DELETED;
413                     value = RequestSetMessageStatus.STATUS_NO;
414                     break;
415 
416                 case BluetoothMapClient.DELETED:
417                     statusIndicator = RequestSetMessageStatus.StatusIndicator.DELETED;
418                     value = RequestSetMessageStatus.STATUS_YES;
419                     break;
420 
421                 default:
422                     Log.e(TAG, "Invalid parameter for status" + status);
423                     return false;
424             }
425             sendMessage(MSG_SET_MESSAGE_STATUS, 0, 0, new RequestSetMessageStatus(
426                     handle, statusIndicator, value));
427             return true;
428         }
429         return false;
430     }
431 
getContactURIFromPhone(String number)432     private String getContactURIFromPhone(String number) {
433         return PhoneAccount.SCHEME_TEL + ":" + number;
434     }
435 
getContactURIFromEmail(String email)436     private String getContactURIFromEmail(String email) {
437         return SCHEME_MAILTO + "://" + email;
438     }
439 
getDefaultMessageType()440     Bmessage.Type getDefaultMessageType() {
441         synchronized (mDefaultMessageType) {
442             if (Utils.isPtsTestMode()) {
443                 return MapUtils.sendMessageType();
444             }
445             return mDefaultMessageType;
446         }
447     }
448 
setDefaultMessageType(SdpMasRecord sdpMasRecord)449     void setDefaultMessageType(SdpMasRecord sdpMasRecord) {
450         int supportedMessageTypes = sdpMasRecord.getSupportedMessageTypes();
451         synchronized (mDefaultMessageType) {
452             if ((supportedMessageTypes & SdpMasRecord.MessageType.MMS) > 0) {
453                 mDefaultMessageType = Bmessage.Type.MMS;
454             } else if ((supportedMessageTypes & SdpMasRecord.MessageType.SMS_CDMA) > 0) {
455                 mDefaultMessageType = Bmessage.Type.SMS_CDMA;
456             } else if ((supportedMessageTypes & SdpMasRecord.MessageType.SMS_GSM) > 0) {
457                 mDefaultMessageType = Bmessage.Type.SMS_GSM;
458             }
459         }
460     }
461 
dump(StringBuilder sb)462     public void dump(StringBuilder sb) {
463         ProfileService.println(sb, "mCurrentDevice: " + mDevice + "("
464                 + Utils.getName(mDevice) + ") " + this.toString());
465         if (mDatabase != null) {
466             mDatabase.dump(sb);
467         } else {
468             ProfileService.println(sb, "  Device Message DB: null");
469         }
470         sb.append("\n");
471     }
472 
473     class Disconnected extends State {
474         @Override
enter()475         public void enter() {
476             if (DBG) {
477                 Log.d(TAG, Utils.getLoggableAddress(mDevice) + " [Disconnected]: Entered, message="
478                         + getMessageName(getCurrentMessage().what));
479             }
480             onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_DISCONNECTED);
481             mPreviousState = BluetoothProfile.STATE_DISCONNECTED;
482             quit();
483         }
484 
485         @Override
exit()486         public void exit() {
487             mPreviousState = BluetoothProfile.STATE_DISCONNECTED;
488         }
489     }
490 
491     class Connecting extends State {
492         @Override
enter()493         public void enter() {
494             if (DBG) {
495                 Log.d(TAG, Utils.getLoggableAddress(mDevice) + " [Connecting]: Entered, message="
496                         + getMessageName(getCurrentMessage().what));
497             }
498             onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_CONNECTING);
499 
500             // When commanded to connect begin SDP to find the MAS server.
501             mDevice.sdpSearch(BluetoothUuid.MAS);
502             sendMessageDelayed(MSG_CONNECTING_TIMEOUT, CONNECT_TIMEOUT);
503             Log.i(TAG, Utils.getLoggableAddress(mDevice) + " [Connecting]: Await SDP results");
504         }
505 
506         @Override
processMessage(Message message)507         public boolean processMessage(Message message) {
508             if (DBG) {
509                 Log.d(TAG, Utils.getLoggableAddress(mDevice) + " [Connecting]: Received "
510                         + getMessageName(message.what));
511             }
512 
513             switch (message.what) {
514                 case MSG_MAS_SDP_DONE:
515                     Log.i(TAG, Utils.getLoggableAddress(mDevice) + " [Connecting]: SDP Complete");
516                     if (mMasClient == null) {
517                         SdpMasRecord record = (SdpMasRecord) message.obj;
518                         if (record == null) {
519                             Log.e(TAG, Utils.getLoggableAddress(mDevice)
520                                     + " [Connecting]: SDP record is null");
521                             return NOT_HANDLED;
522                         }
523                         mMasClient = new MasClient(mDevice, MceStateMachine.this, record);
524                         setDefaultMessageType(record);
525                     }
526                     break;
527 
528                 case MSG_MAS_CONNECTED:
529                     transitionTo(mConnected);
530                     break;
531 
532                 case MSG_MAS_DISCONNECTED:
533                     if (mMasClient != null) {
534                         mMasClient.shutdown();
535                     }
536                     transitionTo(mDisconnected);
537                     break;
538 
539                 case MSG_CONNECTING_TIMEOUT:
540                     transitionTo(mDisconnecting);
541                     break;
542 
543                 case MSG_CONNECT:
544                 case MSG_DISCONNECT:
545                     deferMessage(message);
546                     break;
547 
548                 default:
549                     Log.w(TAG, Utils.getLoggableAddress(mDevice)
550                             + " [Connecting]: Unexpected message: " + getMessageName(message.what));
551                     return NOT_HANDLED;
552             }
553             return HANDLED;
554         }
555 
556         @Override
exit()557         public void exit() {
558             mPreviousState = BluetoothProfile.STATE_CONNECTING;
559             removeMessages(MSG_CONNECTING_TIMEOUT);
560         }
561     }
562 
563     class Connected extends State {
564         @Override
enter()565         public void enter() {
566             if (DBG) {
567                 Log.d(TAG, Utils.getLoggableAddress(mDevice) + " [Connected]: Entered, message="
568                         + getMessageName(getCurrentMessage().what));
569             }
570 
571             MapClientContent.Callbacks callbacks = new MapClientContent.Callbacks(){
572                 @Override
573                 public void onMessageStatusChanged(String handle, int status) {
574                     setMessageStatus(handle, status);
575                 }
576             };
577             // Keeps mock database from being overwritten in tests
578             if (mDatabase == null) {
579                 mDatabase = new MapClientContent(mService, callbacks, mDevice);
580             }
581             onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_CONNECTED);
582             if (Utils.isPtsTestMode()) return;
583 
584             mMasClient.makeRequest(new RequestSetPath(FOLDER_TELECOM));
585             mMasClient.makeRequest(new RequestSetPath(FOLDER_MSG));
586             mMasClient.makeRequest(new RequestSetPath(FOLDER_INBOX));
587             mMasClient.makeRequest(new RequestGetFolderListing(0, 0));
588             mMasClient.makeRequest(new RequestSetPath(false));
589             // Start searching for remote device's own phone number. Only until either:
590             //   (a) the search completes (with or without finding the number), or
591             //   (b) the timeout expires,
592             // does the MCE:
593             //   (a) register itself for being notified of the arrival of new messages, and
594             //   (b) start downloading existing messages off of MSE.
595             // In other words, the MCE shouldn't handle any messages (new or existing) until after
596             // it has tried obtaining the remote's own phone number.
597             RequestGetMessagesListingForOwnNumber requestForOwnNumber =
598                     new RequestGetMessagesListingForOwnNumber();
599             mMasClient.makeRequest(requestForOwnNumber);
600             sendMessageDelayed(MSG_SEARCH_OWN_NUMBER_TIMEOUT, requestForOwnNumber,
601                     sOwnNumberSearchTimeoutMs);
602             Log.i(TAG, Utils.getLoggableAddress(mDevice) + "[Connected]: Find phone number");
603         }
604 
605         @Override
processMessage(Message message)606         public boolean processMessage(Message message) {
607             if (DBG) {
608                 Log.d(TAG, Utils.getLoggableAddress(mDevice) + " [Connected]: Received "
609                         + getMessageName(message.what));
610             }
611             switch (message.what) {
612                 case MSG_DISCONNECT:
613                     if (mDevice.equals(message.obj)) {
614                         transitionTo(mDisconnecting);
615                     }
616                     break;
617 
618                 case MSG_MAS_DISCONNECTED:
619                     deferMessage(message);
620                     transitionTo(mDisconnecting);
621                     break;
622 
623                 case MSG_OUTBOUND_MESSAGE:
624                     mMasClient.makeRequest(
625                             new RequestPushMessage(FOLDER_OUTBOX, (Bmessage) message.obj, null,
626                                     false, false));
627                     break;
628 
629                 case MSG_INBOUND_MESSAGE:
630                     mMasClient.makeRequest(
631                             new RequestGetMessage((String) message.obj, MasClient.CharsetType.UTF_8,
632                                     false));
633                     break;
634 
635                 case MSG_NOTIFICATION:
636                     EventReport notification = (EventReport) message.obj;
637                     processNotification(notification);
638                     break;
639 
640                 case MSG_GET_LISTING:
641                     mMasClient.makeRequest(new RequestGetFolderListing(0, 0));
642                     break;
643 
644                 case MSG_GET_MESSAGE_LISTING:
645                     // Get latest 50 Unread messages in the last week
646                     MessagesFilter filter = new MessagesFilter();
647                     filter.setMessageType(MapUtils.fetchMessageType());
648                     Calendar calendar = Calendar.getInstance();
649                     calendar.add(Calendar.DATE, -7);
650                     filter.setPeriod(calendar.getTime(), null);
651                     mMasClient.makeRequest(new RequestGetMessagesListing(
652                             (String) message.obj, 0, filter, 0, 50, 0));
653                     break;
654 
655                 case MSG_SET_MESSAGE_STATUS:
656                     if (message.obj instanceof RequestSetMessageStatus) {
657                         mMasClient.makeRequest((RequestSetMessageStatus) message.obj);
658                     }
659                     break;
660 
661                 case MSG_MAS_REQUEST_COMPLETED:
662                     if (message.obj instanceof RequestGetMessage) {
663                         processInboundMessage((RequestGetMessage) message.obj);
664                     } else if (message.obj instanceof RequestPushMessage) {
665                         RequestPushMessage requestPushMessage = (RequestPushMessage) message.obj;
666                         String messageHandle = requestPushMessage.getMsgHandle();
667                         Log.i(TAG, Utils.getLoggableAddress(mDevice)
668                                 + " [Connected]: Message Sent, handle=" + messageHandle);
669                         // ignore the top-order byte (converted to string) in the handle for now
670                         // some test devices don't populate messageHandle field.
671                         // in such cases, no need to wait up for response for such messages.
672                         if (messageHandle != null && messageHandle.length() > 2) {
673                             if (SAVE_OUTBOUND_MESSAGES) {
674                                 mDatabase.storeMessage(requestPushMessage.getBMsg(), messageHandle,
675                                         System.currentTimeMillis(), MESSAGE_SEEN);
676                             }
677                             mSentMessageLog.put(messageHandle.substring(2),
678                                     requestPushMessage.getBMsg());
679                         }
680                     } else if (message.obj instanceof RequestGetMessagesListing) {
681                         processMessageListing((RequestGetMessagesListing) message.obj);
682                     } else if (message.obj instanceof RequestSetMessageStatus) {
683                         processSetMessageStatus((RequestSetMessageStatus) message.obj);
684                     } else if (message.obj instanceof RequestGetMessagesListingForOwnNumber) {
685                         processMessageListingForOwnNumber(
686                                 (RequestGetMessagesListingForOwnNumber) message.obj);
687                     }
688                     break;
689 
690                 case MSG_CONNECT:
691                     if (!mDevice.equals(message.obj)) {
692                         deferMessage(message);
693                         transitionTo(mDisconnecting);
694                     }
695                     break;
696 
697                 case MSG_SEARCH_OWN_NUMBER_TIMEOUT:
698                     Log.w(TAG, "Timeout while searching for own phone number.");
699                     // Abort any outstanding Request so it doesn't execute on MasClient
700                     RequestGetMessagesListingForOwnNumber request =
701                             (RequestGetMessagesListingForOwnNumber) message.obj;
702                     mMasClient.abortRequest(request);
703                     // Remove any executed/completed Request that MasClient has passed back to
704                     // state machine. Note: {@link StateMachine} doesn't provide a {@code
705                     // removeMessages(int what, Object obj)}, nor direct access to {@link
706                     // mSmHandler}, so this will remove *all* {@code MSG_MAS_REQUEST_COMPLETED}
707                     // messages. However, {@link RequestGetMessagesListingForOwnNumber} should be
708                     // the only MAS Request enqueued at this point, since none of the other MAS
709                     // Requests should trigger/start until after getOwnNumber has completed.
710                     removeMessages(MSG_MAS_REQUEST_COMPLETED);
711                     // If failed to complete search for remote device's own phone number,
712                     // proceed without it (i.e., register MCE for MNS and start download
713                     // of existing messages from MSE).
714                     notificationRegistrationAndStartDownloadMessages();
715                     break;
716 
717                 default:
718                     Log.w(TAG, Utils.getLoggableAddress(mDevice)
719                             + " [Connected]: Unexpected message: " + getMessageName(message.what));
720                     return NOT_HANDLED;
721             }
722             return HANDLED;
723         }
724 
725         @Override
exit()726         public void exit() {
727             mDatabase.cleanUp();
728             mDatabase = null;
729             mPreviousState = BluetoothProfile.STATE_CONNECTED;
730         }
731 
732         /**
733          * Given a message notification event, will ensure message caching and updating and update
734          * interested applications.
735          *
736          * Message notifications arrive for both remote message reception and Message-Listing object
737          * updates that are triggered by the server side.
738          *
739          * @param msg - A Message object containing a EventReport object describing the remote event
740          */
processNotification(EventReport event)741         private void processNotification(EventReport event) {
742             Log.i(TAG, Utils.getLoggableAddress(mDevice)
743                     + " [Connected]: Received Notification, event=" + event);
744 
745             if (event == null) {
746                 Log.w(TAG, Utils.getLoggableAddress(mDevice)
747                         + "[Connected]: Notification event is null");
748                 return;
749             }
750 
751             switch (event.getType()) {
752                 case NEW_MESSAGE:
753                     if (!mMessages.containsKey(event.getHandle())) {
754                         Long timestamp = event.getTimestamp();
755                         if (timestamp == null) {
756                             // Infer the timestamp for this message as 'now' and read status
757                             // false instead of getting the message listing data for it
758                             timestamp = new Long(Instant.now().toEpochMilli());
759                         }
760                         MessageMetadata metadata = new MessageMetadata(event.getHandle(),
761                                 timestamp, false, MESSAGE_NOT_SEEN);
762                         mMessages.put(event.getHandle(), metadata);
763                     }
764                     mMasClient.makeRequest(new RequestGetMessage(event.getHandle(),
765                             MasClient.CharsetType.UTF_8, false));
766                     break;
767                 case DELIVERY_SUCCESS:
768                 case SENDING_SUCCESS:
769                     notifySentMessageStatus(event.getHandle(), event.getType());
770                     break;
771                 case READ_STATUS_CHANGED:
772                     mDatabase.markRead(event.getHandle());
773                     break;
774                 case MESSAGE_DELETED:
775                     mDatabase.deleteMessage(event.getHandle());
776                     break;
777             }
778         }
779 
780         // Sets the specified message status to "read" (from "unread" status, mostly)
markMessageRead(RequestGetMessage request)781         private void markMessageRead(RequestGetMessage request) {
782             if (DBG) Log.d(TAG, "markMessageRead" + request.getHandle());
783             MessageMetadata metadata = mMessages.get(request.getHandle());
784             metadata.setRead(true);
785             mMasClient.makeRequest(new RequestSetMessageStatus(request.getHandle(),
786                     RequestSetMessageStatus.StatusIndicator.READ, RequestSetMessageStatus.STATUS_YES));
787         }
788 
789         // Sets the specified message status to "deleted"
markMessageDeleted(RequestGetMessage request)790         private void markMessageDeleted(RequestGetMessage request) {
791             if (DBG) Log.d(TAG, "markMessageDeleted");
792             mMasClient.makeRequest(new RequestSetMessageStatus(request.getHandle(),
793                     RequestSetMessageStatus.StatusIndicator.DELETED, RequestSetMessageStatus.STATUS_YES));
794         }
795 
796         /**
797          * Given the result of a Message Listing request, will cache the contents of each Message in
798          * the Message Listing Object and kick off requests to retrieve message contents from the
799          * remote device.
800          *
801          * @param request - A request object that has been resolved and returned with a message list
802          */
processMessageListing(RequestGetMessagesListing request)803         private void processMessageListing(RequestGetMessagesListing request) {
804             Log.i(TAG, Utils.getLoggableAddress(mDevice)
805                     + " [Connected]: Received Message Listing, listing="
806                     + (request != null ? (request.getList() != null
807                         ? String.valueOf(request.getList().size())
808                         : "null list") : "null request"));
809 
810             ArrayList<com.android.bluetooth.mapclient.Message> messageListing = request.getList();
811             if (messageListing != null) {
812                 // Message listings by spec arrive ordered newest first but we wish to broadcast as
813                 // oldest first. Iterate in reverse order so we initiate requests oldest first.
814                 for (int i = messageListing.size() - 1; i >= 0; i--) {
815                     com.android.bluetooth.mapclient.Message msg = messageListing.get(i);
816                     if (DBG) {
817                         Log.d(TAG, Utils.getLoggableAddress(mDevice)
818                                 + " [Connected]: fetch message content, handle=" + msg.getHandle());
819                     }
820                     // A message listing coming from the server should always have up to date data
821                     if (msg.getDateTime() == null) {
822                         Log.w(TAG, "message with handle " + msg.getHandle()
823                                 + " has a null datetime, ignoring");
824                         continue;
825                     }
826                     mMessages.put(msg.getHandle(), new MessageMetadata(msg.getHandle(),
827                             msg.getDateTime().getTime(), msg.isRead(), MESSAGE_SEEN));
828                     getMessage(msg.getHandle());
829                 }
830             }
831         }
832 
833         /**
834          * Process the result of a MessageListing request that was made specifically to obtain
835          * the remote device's own phone number.
836          *
837          * @param request - A request object that has been resolved and returned with:
838          *   - a phone number (possibly null if a number wasn't found)
839          *   - a flag indicating whether there are still messages that can be searched/requested.
840          *   - the request will automatically update itself if a number wasn't found and there are
841          *     still messages that can be searched.
842          */
processMessageListingForOwnNumber( RequestGetMessagesListingForOwnNumber request)843         private void processMessageListingForOwnNumber(
844                 RequestGetMessagesListingForOwnNumber request) {
845 
846             if (request.isSearchCompleted()) {
847                 if (DBG) {
848                     Log.d(TAG, "processMessageListingForOwnNumber: search completed");
849                 }
850                 if (request.getOwnNumber() != null) {
851                     // A phone number was found (should be the remote device's).
852                     if (DBG) {
853                         Log.d(TAG, "processMessageListingForOwnNumber: number found = "
854                                 + request.getOwnNumber());
855                     }
856                     mDatabase.setRemoteDeviceOwnNumber(request.getOwnNumber());
857                 }
858                 // Remove any outstanding timeouts from state machine queue
859                 removeDeferredMessages(MSG_SEARCH_OWN_NUMBER_TIMEOUT);
860                 removeMessages(MSG_SEARCH_OWN_NUMBER_TIMEOUT);
861                 // Move on to next stage of connection process
862                 notificationRegistrationAndStartDownloadMessages();
863             } else {
864                 // A phone number wasn't found, but there are still additional messages that can
865                 // be requested and searched.
866                 if (DBG) {
867                     Log.d(TAG, "processMessageListingForOwnNumber: continuing search");
868                 }
869                 mMasClient.makeRequest(request);
870             }
871         }
872 
873         /**
874          * (1) MCE registering itself for being notified of the arrival of new messages; and
875          * (2) MCE downloading existing messages of off MSE.
876          */
notificationRegistrationAndStartDownloadMessages()877         private void notificationRegistrationAndStartDownloadMessages() {
878             Log.i(TAG, Utils.getLoggableAddress(mDevice) + "[Connected]: Queue Message downloads");
879             mMasClient.makeRequest(new RequestSetNotificationRegistration(true));
880             sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_SENT);
881             sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_INBOX);
882         }
883 
processSetMessageStatus(RequestSetMessageStatus request)884         private void processSetMessageStatus(RequestSetMessageStatus request) {
885             if (DBG) {
886                 Log.d(TAG, "processSetMessageStatus");
887             }
888             int result = BluetoothMapClient.RESULT_SUCCESS;
889             if (!request.isSuccess()) {
890                 Log.e(TAG, "Set message status failed");
891                 result = BluetoothMapClient.RESULT_FAILURE;
892             }
893             RequestSetMessageStatus.StatusIndicator status = request.getStatusIndicator();
894             switch (status) {
895                 case READ: {
896                     Intent intent = new Intent(
897                             BluetoothMapClient.ACTION_MESSAGE_READ_STATUS_CHANGED);
898                     intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_READ_STATUS,
899                             request.getValue() == RequestSetMessageStatus.STATUS_YES ? true : false);
900                     intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_HANDLE, request.getHandle());
901                     intent.putExtra(BluetoothMapClient.EXTRA_RESULT_CODE, result);
902                     mService.sendBroadcast(intent, BLUETOOTH_CONNECT);
903                     break;
904                 }
905                 case DELETED: {
906                     Intent intent = new Intent(
907                             BluetoothMapClient.ACTION_MESSAGE_DELETED_STATUS_CHANGED);
908                     intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_DELETED_STATUS,
909                             request.getValue() == RequestSetMessageStatus.STATUS_YES ? true : false);
910                     intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_HANDLE, request.getHandle());
911                     intent.putExtra(BluetoothMapClient.EXTRA_RESULT_CODE, result);
912                     mService.sendBroadcast(intent, BLUETOOTH_CONNECT);
913                     break;
914                 }
915                 default:
916                     Log.e(TAG, "Unknown status indicator " + status);
917                     return;
918             }
919         }
920 
921         /**
922          * Given the response of a GetMessage request, will broadcast the bMessage contents on to
923          * all registered applications.
924          *
925          * Inbound messages arrive as bMessage objects following a GetMessage request. GetMessage
926          * uses a message handle that can arrive from both a GetMessageListing request or a Message
927          * Notification event.
928          *
929          * @param request - A request object that has been resolved and returned with message data
930          */
processInboundMessage(RequestGetMessage request)931         private void processInboundMessage(RequestGetMessage request) {
932             Bmessage message = request.getMessage();
933             if (DBG) {
934                 Log.d(TAG, "Notify inbound Message" + message);
935             }
936 
937             if (message == null) {
938                 return;
939             }
940             mDatabase.storeMessage(message, request.getHandle(),
941                     mMessages.get(request.getHandle()).getTimestamp(),
942                     mMessages.get(request.getHandle()).getSeen());
943             if (!INBOX_PATH.equalsIgnoreCase(message.getFolder())) {
944                 if (DBG) {
945                     Log.d(TAG, "Ignoring message received in " + message.getFolder() + ".");
946                 }
947                 return;
948             }
949             switch (message.getType()) {
950                 case SMS_CDMA:
951                 case SMS_GSM:
952                 case MMS:
953                     if (DBG) {
954                         Log.d(TAG, "Body: " + message.getBodyContent());
955                         Log.d(TAG, message.toString());
956                         Log.d(TAG, "Recipients" + message.getRecipients().toString());
957                     }
958 
959                     // Grab the message metadata and update the cached read status from the bMessage
960                     MessageMetadata metadata = mMessages.get(request.getHandle());
961                     metadata.setRead(request.getMessage().getStatus() == Bmessage.Status.READ);
962 
963                     Intent intent = new Intent();
964                     intent.setAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED);
965                     intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
966                     intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_HANDLE, request.getHandle());
967                     intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_TIMESTAMP,
968                             metadata.getTimestamp());
969                     intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_READ_STATUS,
970                             metadata.getRead());
971                     intent.putExtra(android.content.Intent.EXTRA_TEXT, message.getBodyContent());
972                     VCardEntry originator = message.getOriginator();
973                     if (originator != null) {
974                         if (DBG) {
975                             Log.d(TAG, originator.toString());
976                         }
977                         List<VCardEntry.PhoneData> phoneData = originator.getPhoneList();
978                         List<VCardEntry.EmailData> emailData = originator.getEmailList();
979                         if (phoneData != null && phoneData.size() > 0) {
980                             String phoneNumber = phoneData.get(0).getNumber();
981                             if (DBG) {
982                                 Log.d(TAG, "Originator number: " + phoneNumber);
983                             }
984                             intent.putExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_URI,
985                                     getContactURIFromPhone(phoneNumber));
986                         } else if (emailData != null && emailData.size() > 0) {
987                             String email = emailData.get(0).getAddress();
988                             if (DBG) {
989                                 Log.d(TAG, "Originator email: " + email);
990                             }
991                             intent.putExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_URI,
992                                     getContactURIFromEmail(email));
993                         }
994                         intent.putExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_NAME,
995                                 originator.getDisplayName());
996                     }
997                     if (message.getType() == Bmessage.Type.MMS) {
998                         BluetoothMapbMessageMime mmsBmessage = new BluetoothMapbMessageMime();
999                         mmsBmessage.parseMsgPart(message.getBodyContent());
1000                         intent.putExtra(android.content.Intent.EXTRA_TEXT,
1001                                 mmsBmessage.getMessageAsText());
1002                         ArrayList<VCardEntry> recipients = message.getRecipients();
1003                         if (recipients != null && !recipients.isEmpty()) {
1004                             intent.putExtra(android.content.Intent.EXTRA_CC,
1005                                     getRecipientsUri(recipients));
1006                         }
1007                     }
1008                     String defaultMessagingPackage = Telephony.Sms.getDefaultSmsPackage(mService);
1009                     if (defaultMessagingPackage == null) {
1010                         // Broadcast to all RECEIVE_SMS recipients, including the SMS receiver
1011                         // package defined in system properties if one exists
1012                         mService.sendBroadcast(intent, RECEIVE_SMS);
1013                     } else {
1014                         String smsReceiverPackageName =
1015                                 SystemProperties.get(
1016                                         "bluetooth.profile.map_client.sms_receiver_package",
1017                                         null
1018                                 );
1019                         if (smsReceiverPackageName != null && !smsReceiverPackageName.isEmpty()) {
1020                             // Clone intent and broadcast to SMS receiver package if one exists
1021                             Intent messageNotificationIntent = (Intent) intent.clone();
1022                             messageNotificationIntent.setPackage(smsReceiverPackageName);
1023                             mService.sendBroadcast(messageNotificationIntent, RECEIVE_SMS);
1024                         }
1025                         // Broadcast to default messaging package
1026                         intent.setPackage(defaultMessagingPackage);
1027                         mService.sendBroadcast(intent, RECEIVE_SMS);
1028                     }
1029                     break;
1030                 case EMAIL:
1031                 default:
1032                     Log.e(TAG, "Received unhandled type" + message.getType().toString());
1033                     break;
1034             }
1035         }
1036 
1037         /**
1038          * Retrieves the URIs of all the participants of a group conversation, besides the sender
1039          * of the message.
1040          * @param recipients
1041          * @return
1042          */
getRecipientsUri(ArrayList<VCardEntry> recipients)1043         private String[] getRecipientsUri(ArrayList<VCardEntry> recipients) {
1044             Set<String> uris = new HashSet<>();
1045 
1046             for (VCardEntry recipient : recipients) {
1047                 List<VCardEntry.PhoneData> phoneData = recipient.getPhoneList();
1048                 if (phoneData != null && phoneData.size() > 0) {
1049                     String phoneNumber = phoneData.get(0).getNumber();
1050                     if (DBG) {
1051                         Log.d(TAG, "CC Recipient number: " + phoneNumber);
1052                     }
1053                     uris.add(getContactURIFromPhone(phoneNumber));
1054                 }
1055             }
1056             String[] stringUris = new String[uris.size()];
1057             return uris.toArray(stringUris);
1058         }
1059 
notifySentMessageStatus(String handle, EventReport.Type status)1060         private void notifySentMessageStatus(String handle, EventReport.Type status) {
1061             if (DBG) {
1062                 Log.d(TAG, "got a status for " + handle + " Status = " + status);
1063             }
1064             // some test devices don't populate messageHandle field.
1065             // in such cases, ignore such messages.
1066             if (handle == null || handle.length() <= 2) return;
1067             PendingIntent intentToSend = null;
1068             // ignore the top-order byte (converted to string) in the handle for now
1069             String shortHandle = handle.substring(2);
1070             if (status == EventReport.Type.SENDING_FAILURE
1071                     || status == EventReport.Type.SENDING_SUCCESS) {
1072                 intentToSend = mSentReceiptRequested.remove(mSentMessageLog.get(shortHandle));
1073             } else if (status == EventReport.Type.DELIVERY_SUCCESS
1074                     || status == EventReport.Type.DELIVERY_FAILURE) {
1075                 intentToSend = mDeliveryReceiptRequested.remove(mSentMessageLog.get(shortHandle));
1076             }
1077 
1078             if (intentToSend != null) {
1079                 try {
1080                     if (DBG) {
1081                         Log.d(TAG, "*******Sending " + intentToSend);
1082                     }
1083                     int result = Activity.RESULT_OK;
1084                     if (status == EventReport.Type.SENDING_FAILURE
1085                             || status == EventReport.Type.DELIVERY_FAILURE) {
1086                         result = SmsManager.RESULT_ERROR_GENERIC_FAILURE;
1087                     }
1088                     intentToSend.send(result);
1089                 } catch (PendingIntent.CanceledException e) {
1090                     Log.w(TAG, "Notification Request Canceled" + e);
1091                 }
1092             } else {
1093                 Log.e(TAG, "Received a notification on message with handle = "
1094                         + handle + ", but it is NOT found in mSentMessageLog! where did it go?");
1095             }
1096         }
1097     }
1098 
1099     class Disconnecting extends State {
1100         @Override
enter()1101         public void enter() {
1102             if (DBG) {
1103                 Log.d(TAG, Utils.getLoggableAddress(mDevice) + " [Disconnecting]: Entered, message="
1104                         + getMessageName(getCurrentMessage().what));
1105             }
1106 
1107             onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_DISCONNECTING);
1108 
1109             if (mMasClient != null) {
1110                 mMasClient.makeRequest(new RequestSetNotificationRegistration(false));
1111                 mMasClient.shutdown();
1112                 sendMessageDelayed(MSG_DISCONNECTING_TIMEOUT, DISCONNECT_TIMEOUT);
1113             } else {
1114                 // MAP was never connected
1115                 transitionTo(mDisconnected);
1116             }
1117         }
1118 
1119         @Override
processMessage(Message message)1120         public boolean processMessage(Message message) {
1121             if (DBG) {
1122                 Log.d(TAG, Utils.getLoggableAddress(mDevice) + " [Disconnecting]: Received "
1123                         + getMessageName(message.what));
1124             }
1125             switch (message.what) {
1126                 case MSG_DISCONNECTING_TIMEOUT:
1127                 case MSG_MAS_DISCONNECTED:
1128                     mMasClient = null;
1129                     transitionTo(mDisconnected);
1130                     break;
1131 
1132                 case MSG_CONNECT:
1133                 case MSG_DISCONNECT:
1134                     deferMessage(message);
1135                     break;
1136 
1137                 default:
1138                     Log.w(TAG, Utils.getLoggableAddress(mDevice)
1139                             + " [Disconnecting]: Unexpected message: "
1140                             + getMessageName(message.what));
1141                     return NOT_HANDLED;
1142             }
1143             return HANDLED;
1144         }
1145 
1146         @Override
exit()1147         public void exit() {
1148             mPreviousState = BluetoothProfile.STATE_DISCONNECTING;
1149             removeMessages(MSG_DISCONNECTING_TIMEOUT);
1150         }
1151     }
1152 
receiveEvent(EventReport ev)1153     void receiveEvent(EventReport ev) {
1154         if (DBG) {
1155             Log.d(TAG, "Message Type = " + ev.getType()
1156                     + ", Message handle = " + ev.getHandle());
1157         }
1158         sendMessage(MSG_NOTIFICATION, ev);
1159     }
1160 
getMessageName(int what)1161     private String getMessageName(int what) {
1162         switch (what) {
1163             case MSG_MAS_CONNECTED:
1164                 return "MSG_MAS_CONNECTED";
1165             case MSG_MAS_DISCONNECTED:
1166                 return "MSG_MAS_DISCONNECTED";
1167             case MSG_MAS_REQUEST_COMPLETED:
1168                 return "MSG_MAS_REQUEST_COMPLETED";
1169             case MSG_MAS_REQUEST_FAILED:
1170                 return "MSG_MAS_REQUEST_FAILED";
1171             case MSG_MAS_SDP_DONE:
1172                 return "MSG_MAS_SDP_DONE";
1173             case MSG_MAS_SDP_FAILED:
1174                 return "MSG_MAS_SDP_FAILED";
1175             case MSG_OUTBOUND_MESSAGE:
1176                 return "MSG_OUTBOUND_MESSAGE";
1177             case MSG_INBOUND_MESSAGE:
1178                 return "MSG_INBOUND_MESSAGE";
1179             case MSG_NOTIFICATION:
1180                 return "MSG_NOTIFICATION";
1181             case MSG_GET_LISTING:
1182                 return "MSG_GET_LISTING";
1183             case MSG_GET_MESSAGE_LISTING:
1184                 return "MSG_GET_MESSAGE_LISTING";
1185             case MSG_SET_MESSAGE_STATUS:
1186                 return "MSG_SET_MESSAGE_STATUS";
1187             case DISCONNECT_TIMEOUT:
1188                 return "DISCONNECT_TIMEOUT";
1189             case CONNECT_TIMEOUT:
1190                 return "CONNECT_TIMEOUT";
1191             case MSG_CONNECT:
1192                 return "MSG_CONNECT";
1193             case MSG_DISCONNECT:
1194                 return "MSG_DISCONNECT";
1195             case MSG_CONNECTING_TIMEOUT:
1196                 return "MSG_CONNECTING_TIMEOUT";
1197             case MSG_DISCONNECTING_TIMEOUT:
1198                 return "MSG_DISCONNECTING_TIMEOUT";
1199         }
1200         return "UNKNOWN";
1201     }
1202 }
1203