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