1 /*
2 * Copyright (C) 2014 Samsung System LSI
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *      http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15 package com.android.bluetooth.map;
16 
17 import android.bluetooth.BluetoothDevice;
18 import android.bluetooth.BluetoothSocket;
19 import android.bluetooth.SdpMnsRecord;
20 import android.os.Handler;
21 import android.os.HandlerThread;
22 import android.os.Looper;
23 import android.os.Message;
24 import android.os.ParcelUuid;
25 import android.util.Log;
26 import android.util.SparseBooleanArray;
27 
28 import com.android.bluetooth.BluetoothObexTransport;
29 
30 import java.io.IOException;
31 import java.io.OutputStream;
32 
33 import javax.obex.ClientOperation;
34 import javax.obex.ClientSession;
35 import javax.obex.HeaderSet;
36 import javax.obex.ObexTransport;
37 import javax.obex.ResponseCodes;
38 
39 /**
40  * The Message Notification Service class runs its own message handler thread,
41  * to avoid executing long operations on the MAP service Thread.
42  * This handler context is passed to the content observers,
43  * hence all call-backs (and thereby transmission of data) is executed
44  * from this thread.
45  */
46 public class BluetoothMnsObexClient {
47 
48     private static final String TAG = "BluetoothMnsObexClient";
49     private static final boolean D = BluetoothMapService.DEBUG;
50     private static final boolean V = BluetoothMapService.VERBOSE;
51 
52     private ObexTransport mTransport;
53     public Handler mHandler = null;
54     private volatile boolean mWaitingForRemote;
55     private static final String TYPE_EVENT = "x-bt/MAP-event-report";
56     private ClientSession mClientSession;
57     private boolean mConnected = false;
58     BluetoothDevice mRemoteDevice;
59     private SparseBooleanArray mRegisteredMasIds = new SparseBooleanArray(1);
60 
61     private HeaderSet mHsConnect = null;
62     private Handler mCallback = null;
63     private SdpMnsRecord mMnsRecord;
64     // Used by the MAS to forward notification registrations
65     public static final int MSG_MNS_NOTIFICATION_REGISTRATION = 1;
66     public static final int MSG_MNS_SEND_EVENT = 2;
67     public static final int MSG_MNS_SDP_SEARCH_REGISTRATION = 3;
68 
69     //Copy SdpManager.SDP_INTENT_DELAY - The timeout to wait for reply from native.
70     private static final int MNS_SDP_SEARCH_DELAY = 6000;
71     public MnsSdpSearchInfo mMnsLstRegRqst = null;
72     private static final int MNS_NOTIFICATION_DELAY = 10;
73     public static final ParcelUuid BLUETOOTH_UUID_OBEX_MNS =
74             ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB");
75 
76 
BluetoothMnsObexClient(BluetoothDevice remoteDevice, SdpMnsRecord mnsRecord, Handler callback)77     public BluetoothMnsObexClient(BluetoothDevice remoteDevice, SdpMnsRecord mnsRecord,
78             Handler callback) {
79         if (remoteDevice == null) {
80             throw new NullPointerException("Obex transport is null");
81         }
82         mRemoteDevice = remoteDevice;
83         HandlerThread thread = new HandlerThread("BluetoothMnsObexClient");
84         thread.start();
85         /* This will block until the looper have started, hence it will be safe to use it,
86            when the constructor completes */
87         Looper looper = thread.getLooper();
88         mHandler = new MnsObexClientHandler(looper);
89         mCallback = callback;
90         mMnsRecord = mnsRecord;
91     }
92 
getMessageHandler()93     public Handler getMessageHandler() {
94         return mHandler;
95     }
96 
97     class MnsSdpSearchInfo {
98         private boolean mIsSearchInProgress;
99         public int lastMasId;
100         public int lastNotificationStatus;
101 
MnsSdpSearchInfo(boolean isSearchON, int masId, int notification)102         MnsSdpSearchInfo(boolean isSearchON, int masId, int notification) {
103             mIsSearchInProgress = isSearchON;
104             lastMasId = masId;
105             lastNotificationStatus = notification;
106         }
107 
isSearchInProgress()108         public boolean isSearchInProgress() {
109             return mIsSearchInProgress;
110         }
111 
setIsSearchInProgress(boolean isSearchON)112         public void setIsSearchInProgress(boolean isSearchON) {
113             mIsSearchInProgress = isSearchON;
114         }
115     }
116 
117     private final class MnsObexClientHandler extends Handler {
MnsObexClientHandler(Looper looper)118         private MnsObexClientHandler(Looper looper) {
119             super(looper);
120         }
121 
122         @Override
handleMessage(Message msg)123         public void handleMessage(Message msg) {
124             switch (msg.what) {
125                 case MSG_MNS_NOTIFICATION_REGISTRATION:
126                     if (V) {
127                         Log.v(TAG, "Reg  masId:  " + msg.arg1 + " notfStatus: " + msg.arg2);
128                     }
129                     if (isValidMnsRecord()) {
130                         handleRegistration(msg.arg1 /*masId*/, msg.arg2 /*status*/);
131                     } else {
132                         //Should not happen
133                         if (D) {
134                             Log.d(TAG, "MNS SDP info not available yet - Cannot Connect.");
135                         }
136                     }
137                     break;
138                 case MSG_MNS_SEND_EVENT:
139                     sendEventHandler((byte[]) msg.obj/*byte[]*/, msg.arg1 /*masId*/);
140                     break;
141                 case MSG_MNS_SDP_SEARCH_REGISTRATION:
142                     //Initiate SDP Search
143                     notifyMnsSdpSearch();
144                     //Save the mns search info
145                     mMnsLstRegRqst = new MnsSdpSearchInfo(true, msg.arg1, msg.arg2);
146                     //Handle notification registration.
147                     Message msgReg =
148                             mHandler.obtainMessage(MSG_MNS_NOTIFICATION_REGISTRATION, msg.arg1,
149                                     msg.arg2);
150                     if (V) {
151                         Log.v(TAG, "SearchReg  masId:  " + msg.arg1 + " notfStatus: " + msg.arg2);
152                     }
153                     mHandler.sendMessageDelayed(msgReg, MNS_SDP_SEARCH_DELAY);
154                     break;
155                 default:
156                     break;
157             }
158         }
159     }
160 
isConnected()161     public boolean isConnected() {
162         return mConnected;
163     }
164 
165     /**
166      * Disconnect the connection to MNS server.
167      * Call this when the MAS client requests a de-registration on events.
168      */
disconnect()169     public synchronized void disconnect() {
170         try {
171             if (mClientSession != null) {
172                 mClientSession.disconnect(null);
173                 if (D) {
174                     Log.d(TAG, "OBEX session disconnected");
175                 }
176             }
177         } catch (IOException e) {
178             Log.w(TAG, "OBEX session disconnect error " + e.getMessage());
179         }
180         try {
181             if (mClientSession != null) {
182                 if (D) {
183                     Log.d(TAG, "OBEX session close mClientSession");
184                 }
185                 mClientSession.close();
186                 mClientSession = null;
187                 if (D) {
188                     Log.d(TAG, "OBEX session closed");
189                 }
190             }
191         } catch (IOException e) {
192             Log.w(TAG, "OBEX session close error:" + e.getMessage());
193         }
194         if (mTransport != null) {
195             try {
196                 if (D) {
197                     Log.d(TAG, "Close Obex Transport");
198                 }
199                 mTransport.close();
200                 mTransport = null;
201                 mConnected = false;
202                 if (D) {
203                     Log.d(TAG, "Obex Transport Closed");
204                 }
205             } catch (IOException e) {
206                 Log.e(TAG, "mTransport.close error: " + e.getMessage());
207             }
208         }
209     }
210 
211     /**
212      * Shutdown the MNS.
213      */
shutdown()214     public synchronized void shutdown() {
215         /* should shutdown handler thread first to make sure
216          * handleRegistration won't be called when disconnect
217          */
218         if (mHandler != null) {
219             // Shut down the thread
220             mHandler.removeCallbacksAndMessages(null);
221             Looper looper = mHandler.getLooper();
222             if (looper != null) {
223                 looper.quit();
224             }
225         }
226 
227         /* Disconnect if connected */
228         disconnect();
229 
230         mRegisteredMasIds.clear();
231     }
232 
233     /**
234      * We store a list of registered MasIds only to control connect/disconnect
235      * @param masId
236      * @param notificationStatus
237      */
handleRegistration(int masId, int notificationStatus)238     public synchronized void handleRegistration(int masId, int notificationStatus) {
239         if (D) {
240             Log.d(TAG, "handleRegistration( " + masId + ", " + notificationStatus + ")");
241         }
242         boolean sendObserverRegistration = true;
243         if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_NO) {
244             mRegisteredMasIds.delete(masId);
245             if (mMnsLstRegRqst != null && mMnsLstRegRqst.lastMasId == masId) {
246                 //Clear last saved MNSSdpSearchInfo , if Disconnect requested for same MasId.
247                 mMnsLstRegRqst = null;
248             }
249         } else if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) {
250             /* Connect if we do not have a connection, and start the content observers providing
251              * this thread as Handler.
252              */
253             if (!isConnected()) {
254                 if (D) {
255                     Log.d(TAG, "handleRegistration: connect");
256                 }
257                 connect();
258             }
259             sendObserverRegistration = isConnected();
260             mRegisteredMasIds.put(masId, true); // We don't use the value for anything
261 
262             // Clear last saved MNSSdpSearchInfo after connect is processed.
263             mMnsLstRegRqst = null;
264         }
265 
266         if (mRegisteredMasIds.size() == 0) {
267             // No more registrations - disconnect
268             if (D) {
269                 Log.d(TAG, "handleRegistration: disconnect");
270             }
271             disconnect();
272         }
273 
274         //Register ContentObserver After connect/disconnect MNS channel.
275         if (V) {
276             Log.v(TAG, "Send  registerObserver: " + sendObserverRegistration);
277         }
278         if (mCallback != null && sendObserverRegistration) {
279             Message msg = Message.obtain(mCallback);
280             msg.what = BluetoothMapService.MSG_OBSERVER_REGISTRATION;
281             msg.arg1 = masId;
282             msg.arg2 = notificationStatus;
283             msg.sendToTarget();
284         }
285     }
286 
isValidMnsRecord()287     public boolean isValidMnsRecord() {
288         return (mMnsRecord != null);
289     }
290 
setMnsRecord(SdpMnsRecord mnsRecord)291     public void setMnsRecord(SdpMnsRecord mnsRecord) {
292         if (V) {
293             Log.v(TAG, "setMNSRecord");
294         }
295         if (isValidMnsRecord()) {
296             Log.w(TAG, "MNS Record already available. Still update.");
297         }
298         mMnsRecord = mnsRecord;
299         if (mMnsLstRegRqst != null) {
300             //SDP Search completed.
301             mMnsLstRegRqst.setIsSearchInProgress(false);
302             if (mHandler.hasMessages(MSG_MNS_NOTIFICATION_REGISTRATION)) {
303                 mHandler.removeMessages(MSG_MNS_NOTIFICATION_REGISTRATION);
304                 //Search Result obtained within MNS_SDP_SEARCH_DELAY timeout
305                 if (!isValidMnsRecord()) {
306                     // SDP info still not available for last trial.
307                     // Clear saved info.
308                     mMnsLstRegRqst = null;
309                 } else {
310                     if (V) {
311                         Log.v(TAG, "Handle registration for last saved request");
312                     }
313                     Message msgReg = mHandler.obtainMessage(MSG_MNS_NOTIFICATION_REGISTRATION);
314                     msgReg.arg1 = mMnsLstRegRqst.lastMasId;
315                     msgReg.arg2 = mMnsLstRegRqst.lastNotificationStatus;
316                     if (V) {
317                         Log.v(TAG, "SearchReg  masId:  " + msgReg.arg1 + " notfStatus: "
318                                 + msgReg.arg2);
319                     }
320                     //Handle notification registration.
321                     mHandler.sendMessageDelayed(msgReg, MNS_NOTIFICATION_DELAY);
322                 }
323             }
324         } else {
325             if (V) {
326                 Log.v(TAG, "No last saved MNSSDPInfo to handle");
327             }
328         }
329     }
330 
connect()331     public void connect() {
332 
333         mConnected = true;
334 
335         BluetoothSocket btSocket = null;
336         try {
337             // TODO: Do SDP record search again?
338             if (isValidMnsRecord() && mMnsRecord.getL2capPsm() > 0) {
339                 // Do L2CAP connect
340                 btSocket = mRemoteDevice.createL2capSocket(mMnsRecord.getL2capPsm());
341 
342             } else if (isValidMnsRecord() && mMnsRecord.getRfcommChannelNumber() > 0) {
343                 // Do Rfcomm connect
344                 btSocket = mRemoteDevice.createRfcommSocket(mMnsRecord.getRfcommChannelNumber());
345             } else {
346                 // This should not happen...
347                 Log.e(TAG, "Invalid SDP content - attempt a connect to UUID...");
348                 // TODO: Why insecure? - is it because the link is already encrypted?
349                 btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord(
350                         BLUETOOTH_UUID_OBEX_MNS.getUuid());
351             }
352             btSocket.connect();
353         } catch (IOException e) {
354             Log.e(TAG, "BtSocket Connect error " + e.getMessage(), e);
355             // TODO: do we need to report error somewhere?
356             mConnected = false;
357             return;
358         }
359 
360         mTransport = new BluetoothObexTransport(btSocket);
361 
362         try {
363             mClientSession = new ClientSession(mTransport);
364         } catch (IOException e1) {
365             Log.e(TAG, "OBEX session create error " + e1.getMessage());
366             mConnected = false;
367         }
368         if (mConnected && mClientSession != null) {
369             boolean connected = false;
370             HeaderSet hs = new HeaderSet();
371             // bb582b41-420c-11db-b0de-0800200c9a66
372             byte[] mnsTarget = {
373                     (byte) 0xbb,
374                     (byte) 0x58,
375                     (byte) 0x2b,
376                     (byte) 0x41,
377                     (byte) 0x42,
378                     (byte) 0x0c,
379                     (byte) 0x11,
380                     (byte) 0xdb,
381                     (byte) 0xb0,
382                     (byte) 0xde,
383                     (byte) 0x08,
384                     (byte) 0x00,
385                     (byte) 0x20,
386                     (byte) 0x0c,
387                     (byte) 0x9a,
388                     (byte) 0x66
389             };
390             hs.setHeader(HeaderSet.TARGET, mnsTarget);
391 
392             synchronized (this) {
393                 mWaitingForRemote = true;
394             }
395             try {
396                 mHsConnect = mClientSession.connect(hs);
397                 if (D) {
398                     Log.d(TAG, "OBEX session created");
399                 }
400                 connected = true;
401             } catch (IOException e) {
402                 Log.e(TAG, "OBEX session connect error " + e.getMessage());
403             }
404             mConnected = connected;
405         }
406         synchronized (this) {
407             mWaitingForRemote = false;
408         }
409     }
410 
411     /**
412      * Call this method to queue an event report to be send to the MNS server.
413      * @param eventBytes the encoded event data.
414      * @param masInstanceId the MasId of the instance sending the event.
415      */
sendEvent(byte[] eventBytes, int masInstanceId)416     public void sendEvent(byte[] eventBytes, int masInstanceId) {
417         // We need to check for null, to handle shutdown.
418         if (mHandler != null) {
419             Message msg = mHandler.obtainMessage(MSG_MNS_SEND_EVENT, masInstanceId, 0, eventBytes);
420             if (msg != null) {
421                 msg.sendToTarget();
422             }
423         }
424         notifyUpdateWakeLock();
425     }
426 
notifyMnsSdpSearch()427     private void notifyMnsSdpSearch() {
428         if (mCallback != null) {
429             Message msg = Message.obtain(mCallback);
430             msg.what = BluetoothMapService.MSG_MNS_SDP_SEARCH;
431             msg.sendToTarget();
432         }
433     }
434 
sendEventHandler(byte[] eventBytes, int masInstanceId)435     private int sendEventHandler(byte[] eventBytes, int masInstanceId) {
436 
437         boolean error = false;
438         int responseCode = -1;
439         HeaderSet request;
440         int maxChunkSize, bytesToWrite, bytesWritten = 0;
441         ClientSession clientSession = mClientSession;
442 
443         if ((!mConnected) || (clientSession == null)) {
444             Log.w(TAG, "sendEvent after disconnect:" + mConnected);
445             return responseCode;
446         }
447 
448         request = new HeaderSet();
449         BluetoothMapAppParams appParams = new BluetoothMapAppParams();
450         appParams.setMasInstanceId(masInstanceId);
451 
452         ClientOperation putOperation = null;
453         OutputStream outputStream = null;
454 
455         try {
456             request.setHeader(HeaderSet.TYPE, TYPE_EVENT);
457             request.setHeader(HeaderSet.APPLICATION_PARAMETER, appParams.encodeParams());
458 
459             if (mHsConnect.mConnectionID != null) {
460                 request.mConnectionID = new byte[4];
461                 System.arraycopy(mHsConnect.mConnectionID, 0, request.mConnectionID, 0, 4);
462             } else {
463                 Log.w(TAG, "sendEvent: no connection ID");
464             }
465 
466             synchronized (this) {
467                 mWaitingForRemote = true;
468             }
469             // Send the header first and then the body
470             try {
471                 if (V) {
472                     Log.v(TAG, "Send headerset Event ");
473                 }
474                 putOperation = (ClientOperation) clientSession.put(request);
475                 // TODO - Should this be kept or Removed
476 
477             } catch (IOException e) {
478                 Log.e(TAG, "Error when put HeaderSet " + e.getMessage());
479                 error = true;
480             }
481             synchronized (this) {
482                 mWaitingForRemote = false;
483             }
484             if (!error) {
485                 try {
486                     if (V) {
487                         Log.v(TAG, "Send headerset Event ");
488                     }
489                     outputStream = putOperation.openOutputStream();
490                 } catch (IOException e) {
491                     Log.e(TAG, "Error when opening OutputStream " + e.getMessage());
492                     error = true;
493                 }
494             }
495 
496             if (!error) {
497 
498                 maxChunkSize = putOperation.getMaxPacketSize();
499 
500                 while (bytesWritten < eventBytes.length) {
501                     bytesToWrite = Math.min(maxChunkSize, eventBytes.length - bytesWritten);
502                     outputStream.write(eventBytes, bytesWritten, bytesToWrite);
503                     bytesWritten += bytesToWrite;
504                 }
505 
506                 if (bytesWritten == eventBytes.length) {
507                     Log.i(TAG, "SendEvent finished send length" + eventBytes.length);
508                 } else {
509                     error = true;
510                     putOperation.abort();
511                     Log.i(TAG, "SendEvent interrupted");
512                 }
513             }
514         } catch (IOException e) {
515             handleSendException(e.toString());
516             error = true;
517         } catch (IndexOutOfBoundsException e) {
518             handleSendException(e.toString());
519             error = true;
520         } finally {
521             try {
522                 if (outputStream != null) {
523                     outputStream.close();
524                 }
525             } catch (IOException e) {
526                 Log.e(TAG, "Error when closing stream after send " + e.getMessage());
527             }
528             try {
529                 if ((!error) && (putOperation != null)) {
530                     responseCode = putOperation.getResponseCode();
531                     if (responseCode != -1) {
532                         if (V) {
533                             Log.v(TAG, "Put response code " + responseCode);
534                         }
535                         if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
536                             Log.i(TAG, "Response error code is " + responseCode);
537                         }
538                     }
539                 }
540                 if (putOperation != null) {
541                     putOperation.close();
542                 }
543             } catch (IOException e) {
544                 Log.e(TAG, "Error when closing stream after send " + e.getMessage());
545             }
546         }
547 
548         return responseCode;
549     }
550 
handleSendException(String exception)551     private void handleSendException(String exception) {
552         Log.e(TAG, "Error when sending event: " + exception);
553     }
554 
notifyUpdateWakeLock()555     private void notifyUpdateWakeLock() {
556         if (mCallback != null) {
557             Message msg = Message.obtain(mCallback);
558             msg.what = BluetoothMapService.MSG_ACQUIRE_WAKE_LOCK;
559             msg.sendToTarget();
560         }
561     }
562 }
563