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