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 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,
78             SdpMnsRecord mnsRecord, 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 isSearchInProgress;
99         int lastMasId;
100         int lastNotificationStatus;
101 
MnsSdpSearchInfo(boolean isSearchON, int masId, int notification)102         MnsSdpSearchInfo (boolean isSearchON, int masId, int notification) {
103             isSearchInProgress = isSearchON;
104             lastMasId = masId;
105             lastNotificationStatus = notification;
106         }
107 
isSearchInProgress()108         public boolean isSearchInProgress() {
109             return isSearchInProgress;
110         }
111 
setIsSearchInProgress(boolean isSearchON)112         public void setIsSearchInProgress(boolean isSearchON) {
113             isSearchInProgress = 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) Log.v(TAG, "Reg  masId:  " + msg.arg1 + " notfStatus: " + msg.arg2);
127                 if (isValidMnsRecord()) {
128                     handleRegistration(msg.arg1 /*masId*/, msg.arg2 /*status*/);
129                 } else {
130                     //Should not happen
131                     if (D) Log.d(TAG, "MNS SDP info not available yet - Cannot Connect.");
132                 }
133                 break;
134             case MSG_MNS_SEND_EVENT:
135                 sendEventHandler((byte[])msg.obj/*byte[]*/, msg.arg1 /*masId*/);
136                 break;
137             case MSG_MNS_SDP_SEARCH_REGISTRATION:
138                 //Initiate SDP Search
139                 notifyMnsSdpSearch();
140                 //Save the mns search info
141                 mMnsLstRegRqst = new MnsSdpSearchInfo(true, msg.arg1, msg.arg2);
142                 //Handle notification registration.
143                 Message msgReg =
144                         mHandler.obtainMessage(MSG_MNS_NOTIFICATION_REGISTRATION,
145                                 msg.arg1, msg.arg2);
146                 if (V) Log.v(TAG, "SearchReg  masId:  " + msg.arg1 + " notfStatus: " + msg.arg2);
147                 mHandler.sendMessageDelayed(msgReg, MNS_SDP_SEARCH_DELAY);
148                 break;
149             default:
150                 break;
151             }
152         }
153     }
154 
isConnected()155     public boolean isConnected() {
156         return mConnected;
157     }
158 
159     /**
160      * Disconnect the connection to MNS server.
161      * Call this when the MAS client requests a de-registration on events.
162      */
disconnect()163     public synchronized void disconnect() {
164         try {
165             if (mClientSession != null) {
166                 mClientSession.disconnect(null);
167                 if (D) Log.d(TAG, "OBEX session disconnected");
168             }
169         } catch (IOException e) {
170             Log.w(TAG, "OBEX session disconnect error " + e.getMessage());
171         }
172         try {
173             if (mClientSession != null) {
174                 if (D) Log.d(TAG, "OBEX session close mClientSession");
175                 mClientSession.close();
176                 mClientSession = null;
177                 if (D) Log.d(TAG, "OBEX session closed");
178             }
179         } catch (IOException e) {
180             Log.w(TAG, "OBEX session close error:" + e.getMessage());
181         }
182         if (mTransport != null) {
183             try {
184                 if (D) Log.d(TAG, "Close Obex Transport");
185                 mTransport.close();
186                 mTransport = null;
187                 mConnected = false;
188                 if (D) Log.d(TAG, "Obex Transport Closed");
189             } catch (IOException e) {
190                 Log.e(TAG, "mTransport.close error: " + e.getMessage());
191             }
192         }
193     }
194 
195     /**
196      * Shutdown the MNS.
197      */
shutdown()198     public synchronized void shutdown() {
199         /* should shutdown handler thread first to make sure
200          * handleRegistration won't be called when disconnect
201          */
202         if (mHandler != null) {
203             // Shut down the thread
204             mHandler.removeCallbacksAndMessages(null);
205             Looper looper = mHandler.getLooper();
206             if (looper != null) {
207                 looper.quit();
208             }
209             mHandler = null;
210         }
211 
212         /* Disconnect if connected */
213         disconnect();
214 
215         mRegisteredMasIds.clear();
216     }
217 
218     /**
219      * We store a list of registered MasIds only to control connect/disconnect
220      * @param masId
221      * @param notificationStatus
222      */
handleRegistration(int masId, int notificationStatus)223     public synchronized void handleRegistration(int masId, int notificationStatus){
224         if(D) Log.d(TAG, "handleRegistration( " + masId + ", " + notificationStatus + ")");
225         boolean sendObserverRegistration = true;
226         if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_NO) {
227             mRegisteredMasIds.delete(masId);
228             if (mMnsLstRegRqst != null &&  mMnsLstRegRqst.lastMasId == masId) {
229                 //Clear last saved MNSSdpSearchInfo , if Disconnect requested for same MasId.
230                 mMnsLstRegRqst = null;
231             }
232         } else if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) {
233             /* Connect if we do not have a connection, and start the content observers providing
234              * this thread as Handler.
235              */
236             if (isConnected() == false) {
237                 if(D) Log.d(TAG, "handleRegistration: connect");
238                 connect();
239             }
240             sendObserverRegistration = isConnected();
241             mRegisteredMasIds.put(masId, true); // We don't use the value for anything
242 
243             // Clear last saved MNSSdpSearchInfo after connect is processed.
244             mMnsLstRegRqst = null;
245         }
246 
247         if (mRegisteredMasIds.size() == 0) {
248             // No more registrations - disconnect
249             if(D) Log.d(TAG, "handleRegistration: disconnect");
250             disconnect();
251         }
252 
253         //Register ContentObserver After connect/disconnect MNS channel.
254         if (V) Log.v(TAG, "Send  registerObserver: " + sendObserverRegistration);
255         if (mCallback != null && sendObserverRegistration) {
256             Message msg = Message.obtain(mCallback);
257             msg.what = BluetoothMapService.MSG_OBSERVER_REGISTRATION;
258             msg.arg1 = masId;
259             msg.arg2 = notificationStatus;
260             msg.sendToTarget();
261         }
262     }
263 
isValidMnsRecord()264     public boolean isValidMnsRecord() {
265         return (mMnsRecord != null);
266     }
267 
setMnsRecord(SdpMnsRecord mnsRecord)268     public void setMnsRecord(SdpMnsRecord mnsRecord) {
269         if (V) Log.v(TAG, "setMNSRecord");
270         if (isValidMnsRecord()) {
271            Log.w(TAG,"MNS Record already available. Still update.");
272         }
273         mMnsRecord = mnsRecord;
274         if (mMnsLstRegRqst != null) {
275             //SDP Search completed.
276             mMnsLstRegRqst.setIsSearchInProgress(false);
277             if (mHandler.hasMessages(MSG_MNS_NOTIFICATION_REGISTRATION)) {
278                 mHandler.removeMessages(MSG_MNS_NOTIFICATION_REGISTRATION);
279                 //Search Result obtained within MNS_SDP_SEARCH_DELAY timeout
280                 if (!isValidMnsRecord()) {
281                     // SDP info still not available for last trial.
282                     // Clear saved info.
283                     mMnsLstRegRqst = null;
284                 } else {
285                     if (V) Log.v(TAG, "Handle registration for last saved request");
286                     Message msgReg =
287                             mHandler.obtainMessage(MSG_MNS_NOTIFICATION_REGISTRATION);
288                     msgReg.arg1 = mMnsLstRegRqst.lastMasId;
289                     msgReg.arg2 = mMnsLstRegRqst.lastNotificationStatus;
290                     if (V) Log.v(TAG, "SearchReg  masId:  " + msgReg.arg1
291                         + " notfStatus: " + msgReg.arg2);
292                     //Handle notification registration.
293                     mHandler.sendMessageDelayed(msgReg, MNS_NOTIFICATION_DELAY);
294                 }
295             }
296         } else {
297            if (V) Log.v(TAG, "No last saved MNSSDPInfo to handle");
298         }
299     }
300 
connect()301     public void connect() {
302 
303         mConnected = true;
304 
305         BluetoothSocket btSocket = null;
306         try {
307             // TODO: Do SDP record search again?
308             if (isValidMnsRecord() && mMnsRecord.getL2capPsm() > 0) {
309                 // Do L2CAP connect
310                 btSocket = mRemoteDevice.createL2capSocket(mMnsRecord.getL2capPsm());
311 
312             } else if (isValidMnsRecord() && mMnsRecord.getRfcommChannelNumber() > 0) {
313                 // Do Rfcomm connect
314                 btSocket = mRemoteDevice.createRfcommSocket(mMnsRecord.getRfcommChannelNumber());
315             } else {
316                 // This should not happen...
317                 Log.e(TAG, "Invalid SDP content - attempt a connect to UUID...");
318                 // TODO: Why insecure? - is it because the link is already encrypted?
319               btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord(
320                       BLUETOOTH_UUID_OBEX_MNS.getUuid());
321             }
322             btSocket.connect();
323         } catch (IOException e) {
324             Log.e(TAG, "BtSocket Connect error " + e.getMessage(), e);
325             // TODO: do we need to report error somewhere?
326             mConnected = false;
327             return;
328         }
329 
330         mTransport = new BluetoothObexTransport(btSocket);
331 
332         try {
333             mClientSession = new ClientSession(mTransport);
334         } catch (IOException e1) {
335             Log.e(TAG, "OBEX session create error " + e1.getMessage());
336             mConnected = false;
337         }
338         if (mConnected && mClientSession != null) {
339             boolean connected = false;
340             HeaderSet hs = new HeaderSet();
341             // bb582b41-420c-11db-b0de-0800200c9a66
342             byte[] mnsTarget = { (byte) 0xbb, (byte) 0x58, (byte) 0x2b, (byte) 0x41,
343                                  (byte) 0x42, (byte) 0x0c, (byte) 0x11, (byte) 0xdb,
344                                  (byte) 0xb0, (byte) 0xde, (byte) 0x08, (byte) 0x00,
345                                  (byte) 0x20, (byte) 0x0c, (byte) 0x9a, (byte) 0x66 };
346             hs.setHeader(HeaderSet.TARGET, mnsTarget);
347 
348             synchronized (this) {
349                 mWaitingForRemote = true;
350             }
351             try {
352                 mHsConnect = mClientSession.connect(hs);
353                 if (D) Log.d(TAG, "OBEX session created");
354                 connected = true;
355             } catch (IOException e) {
356                 Log.e(TAG, "OBEX session connect error " + e.getMessage());
357             }
358             mConnected = connected;
359         }
360         synchronized (this) {
361                 mWaitingForRemote = false;
362         }
363     }
364 
365     /**
366      * Call this method to queue an event report to be send to the MNS server.
367      * @param eventBytes the encoded event data.
368      * @param masInstanceId the MasId of the instance sending the event.
369      */
sendEvent(byte[] eventBytes, int masInstanceId)370     public void sendEvent(byte[] eventBytes, int masInstanceId) {
371         // We need to check for null, to handle shutdown.
372         if(mHandler != null) {
373             Message msg = mHandler.obtainMessage(MSG_MNS_SEND_EVENT, masInstanceId, 0, eventBytes);
374             if(msg != null) {
375                 msg.sendToTarget();
376             }
377         }
378         notifyUpdateWakeLock();
379     }
380 
notifyMnsSdpSearch()381     private void notifyMnsSdpSearch() {
382         if (mCallback != null) {
383             Message msg = Message.obtain(mCallback);
384             msg.what = BluetoothMapService.MSG_MNS_SDP_SEARCH;
385             msg.sendToTarget();
386         }
387     }
388 
sendEventHandler(byte[] eventBytes, int masInstanceId)389     private int sendEventHandler(byte[] eventBytes, int masInstanceId) {
390 
391         boolean error = false;
392         int responseCode = -1;
393         HeaderSet request;
394         int maxChunkSize, bytesToWrite, bytesWritten = 0;
395         ClientSession clientSession = mClientSession;
396 
397         if ((!mConnected) || (clientSession == null)) {
398             Log.w(TAG, "sendEvent after disconnect:" + mConnected);
399             return responseCode;
400         }
401 
402         request = new HeaderSet();
403         BluetoothMapAppParams appParams = new BluetoothMapAppParams();
404         appParams.setMasInstanceId(masInstanceId);
405 
406         ClientOperation putOperation = null;
407         OutputStream outputStream = null;
408 
409         try {
410             request.setHeader(HeaderSet.TYPE, TYPE_EVENT);
411             request.setHeader(HeaderSet.APPLICATION_PARAMETER, appParams.EncodeParams());
412 
413             if (mHsConnect.mConnectionID != null) {
414                 request.mConnectionID = new byte[4];
415                 System.arraycopy(mHsConnect.mConnectionID, 0, request.mConnectionID, 0, 4);
416             } else {
417                 Log.w(TAG, "sendEvent: no connection ID");
418             }
419 
420             synchronized (this) {
421                 mWaitingForRemote = true;
422             }
423             // Send the header first and then the body
424             try {
425                 if (V) Log.v(TAG, "Send headerset Event ");
426                 putOperation = (ClientOperation)clientSession.put(request);
427                 // TODO - Should this be kept or Removed
428 
429             } catch (IOException e) {
430                 Log.e(TAG, "Error when put HeaderSet " + e.getMessage());
431                 error = true;
432             }
433             synchronized (this) {
434                 mWaitingForRemote = false;
435             }
436             if (!error) {
437                 try {
438                     if (V) Log.v(TAG, "Send headerset Event ");
439                     outputStream = putOperation.openOutputStream();
440                 } catch (IOException e) {
441                     Log.e(TAG, "Error when opening OutputStream " + e.getMessage());
442                     error = true;
443                 }
444             }
445 
446             if (!error) {
447 
448                 maxChunkSize = putOperation.getMaxPacketSize();
449 
450                 while (bytesWritten < eventBytes.length) {
451                     bytesToWrite = Math.min(maxChunkSize, eventBytes.length - bytesWritten);
452                     outputStream.write(eventBytes, bytesWritten, bytesToWrite);
453                     bytesWritten += bytesToWrite;
454                 }
455 
456                 if (bytesWritten == eventBytes.length) {
457                     Log.i(TAG, "SendEvent finished send length" + eventBytes.length);
458                 } else {
459                     error = true;
460                     putOperation.abort();
461                     Log.i(TAG, "SendEvent interrupted");
462                 }
463             }
464         } catch (IOException e) {
465             handleSendException(e.toString());
466             error = true;
467         } catch (IndexOutOfBoundsException e) {
468             handleSendException(e.toString());
469             error = true;
470         } finally {
471             try {
472                 if (outputStream != null) {
473                     outputStream.close();
474                 }
475             } catch (IOException e) {
476                 Log.e(TAG, "Error when closing stream after send " + e.getMessage());
477             }
478             try {
479                 if ((!error) && (putOperation != null)) {
480                     responseCode = putOperation.getResponseCode();
481                     if (responseCode != -1) {
482                         if (V) Log.v(TAG, "Put response code " + responseCode);
483                         if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
484                             Log.i(TAG, "Response error code is " + responseCode);
485                         }
486                     }
487                 }
488                 if (putOperation != null) {
489                     putOperation.close();
490                 }
491             } catch (IOException e) {
492                 Log.e(TAG, "Error when closing stream after send " + e.getMessage());
493             }
494         }
495 
496         return responseCode;
497     }
498 
handleSendException(String exception)499     private void handleSendException(String exception) {
500         Log.e(TAG, "Error when sending event: " + exception);
501     }
502 
notifyUpdateWakeLock()503     private void notifyUpdateWakeLock() {
504         if(mCallback != null) {
505             Message msg = Message.obtain(mCallback);
506             msg.what = BluetoothMapService.MSG_ACQUIRE_WAKE_LOCK;
507             msg.sendToTarget();
508         }
509     }
510 }
511