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