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