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