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.os.Handler;
20 import android.os.HandlerThread;
21 import android.os.Looper;
22 import android.os.Message;
23 import android.os.ParcelUuid;
24 import android.util.Log;
25 import android.util.SparseBooleanArray;
26 
27 import java.io.IOException;
28 import java.io.OutputStream;
29 
30 import javax.obex.ClientOperation;
31 import javax.obex.ClientSession;
32 import javax.obex.HeaderSet;
33 import javax.obex.ObexTransport;
34 import javax.obex.ResponseCodes;
35 
36 /**
37  * The Message Notification Service class runs its own message handler thread,
38  * to avoid executing long operations on the MAP service Thread.
39  * This handler context is passed to the content observers,
40  * hence all call-backs (and thereby transmission of data) is executed
41  * from this thread.
42  */
43 public class BluetoothMnsObexClient {
44 
45     private static final String TAG = "BluetoothMnsObexClient";
46     private static final boolean D = BluetoothMapService.DEBUG;
47     private static final boolean V = BluetoothMapService.VERBOSE;
48 
49     private ObexTransport mTransport;
50     public Handler mHandler = null;
51     private volatile boolean mWaitingForRemote;
52     private static final String TYPE_EVENT = "x-bt/MAP-event-report";
53     private ClientSession mClientSession;
54     private boolean mConnected = false;
55     BluetoothDevice mRemoteDevice;
56     private SparseBooleanArray mRegisteredMasIds = new SparseBooleanArray(1);
57 
58     private HeaderSet mHsConnect = null;
59     private Handler mCallback = null;
60 
61     // Used by the MAS to forward notification registrations
62     public static final int MSG_MNS_NOTIFICATION_REGISTRATION = 1;
63     public static final int MSG_MNS_SEND_EVENT = 2;
64 
65 
66     public static final ParcelUuid BLUETOOTH_UUID_OBEX_MNS =
67             ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB");
68 
69 
BluetoothMnsObexClient(BluetoothDevice remoteDevice, Handler callback)70     public BluetoothMnsObexClient(BluetoothDevice remoteDevice, Handler callback) {
71         if (remoteDevice == null) {
72             throw new NullPointerException("Obex transport is null");
73         }
74         mRemoteDevice = remoteDevice;
75         HandlerThread thread = new HandlerThread("BluetoothMnsObexClient");
76         thread.start();
77         /* This will block until the looper have started, hence it will be safe to use it,
78            when the constructor completes */
79         Looper looper = thread.getLooper();
80         mHandler = new MnsObexClientHandler(looper);
81         mCallback = callback;
82     }
83 
getMessageHandler()84     public Handler getMessageHandler() {
85         return mHandler;
86     }
87 
88     private final class MnsObexClientHandler extends Handler {
MnsObexClientHandler(Looper looper)89         private MnsObexClientHandler(Looper looper) {
90             super(looper);
91         }
92 
93         @Override
handleMessage(Message msg)94         public void handleMessage(Message msg) {
95             switch (msg.what) {
96             case MSG_MNS_NOTIFICATION_REGISTRATION:
97                 handleRegistration(msg.arg1 /*masId*/, msg.arg2 /*status*/);
98                 break;
99             case MSG_MNS_SEND_EVENT:
100                 sendEventHandler((byte[])msg.obj/*byte[]*/, msg.arg1 /*masId*/);
101                 break;
102             default:
103                 break;
104             }
105         }
106     }
107 
isConnected()108     public boolean isConnected() {
109         return mConnected;
110     }
111 
112     /**
113      * Disconnect the connection to MNS server.
114      * Call this when the MAS client requests a de-registration on events.
115      */
disconnect()116     public synchronized void disconnect() {
117         try {
118             if (mClientSession != null) {
119                 mClientSession.disconnect(null);
120                 if (D) Log.d(TAG, "OBEX session disconnected");
121             }
122         } catch (IOException e) {
123             Log.w(TAG, "OBEX session disconnect error " + e.getMessage());
124         }
125         try {
126             if (mClientSession != null) {
127                 if (D) Log.d(TAG, "OBEX session close mClientSession");
128                 mClientSession.close();
129                 mClientSession = null;
130                 if (D) Log.d(TAG, "OBEX session closed");
131             }
132         } catch (IOException e) {
133             Log.w(TAG, "OBEX session close error:" + e.getMessage());
134         }
135         if (mTransport != null) {
136             try {
137                 if (D) Log.d(TAG, "Close Obex Transport");
138                 mTransport.close();
139                 mTransport = null;
140                 mConnected = false;
141                 if (D) Log.d(TAG, "Obex Transport Closed");
142             } catch (IOException e) {
143                 Log.e(TAG, "mTransport.close error: " + e.getMessage());
144             }
145         }
146     }
147 
148     /**
149      * Shutdown the MNS.
150      */
shutdown()151     public void shutdown() {
152         /* should shutdown handler thread first to make sure
153          * handleRegistration won't be called when disconnect
154          */
155         if (mHandler != null) {
156             // Shut down the thread
157             mHandler.removeCallbacksAndMessages(null);
158             Looper looper = mHandler.getLooper();
159             if (looper != null) {
160                 looper.quit();
161             }
162             mHandler = null;
163         }
164 
165         /* Disconnect if connected */
166         disconnect();
167 
168         mRegisteredMasIds.clear();
169     }
170 
171     /**
172      * We store a list of registered MasIds only to control connect/disconnect
173      * @param masId
174      * @param notificationStatus
175      */
handleRegistration(int masId, int notificationStatus)176     public void handleRegistration(int masId, int notificationStatus){
177         if(D) Log.d(TAG, "handleRegistration( " + masId + ", " + notificationStatus + ")");
178 
179         if(notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_NO) {
180             mRegisteredMasIds.delete(masId);
181         } else if(notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) {
182             /* Connect if we do not have a connection, and start the content observers providing
183              * this thread as Handler.
184              */
185             if(isConnected() == false) {
186                 if(D) Log.d(TAG, "handleRegistration: connect");
187                 connect();
188             }
189             mRegisteredMasIds.put(masId, true); // We don't use the value for anything
190         }
191         if(mRegisteredMasIds.size() == 0) {
192             // No more registrations - disconnect
193             if(D) Log.d(TAG, "handleRegistration: disconnect");
194             disconnect();
195         }
196     }
197 
connect()198     public void connect() {
199 
200         mConnected = true;
201 
202         BluetoothSocket btSocket = null;
203         try {
204             // TODO: Why insecure? - is it because the link is already encrypted?
205             btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord(
206                     BLUETOOTH_UUID_OBEX_MNS.getUuid());
207             btSocket.connect();
208         } catch (IOException e) {
209             Log.e(TAG, "BtSocket Connect error " + e.getMessage(), e);
210             // TODO: do we need to report error somewhere?
211             mConnected = false;
212             return;
213         }
214 
215         mTransport = new BluetoothMnsRfcommTransport(btSocket);
216 
217         try {
218             mClientSession = new ClientSession(mTransport);
219         } catch (IOException e1) {
220             Log.e(TAG, "OBEX session create error " + e1.getMessage());
221             mConnected = false;
222         }
223         if (mConnected && mClientSession != null) {
224             boolean connected = false;
225             HeaderSet hs = new HeaderSet();
226             // bb582b41-420c-11db-b0de-0800200c9a66
227             byte[] mnsTarget = { (byte) 0xbb, (byte) 0x58, (byte) 0x2b, (byte) 0x41,
228                                  (byte) 0x42, (byte) 0x0c, (byte) 0x11, (byte) 0xdb,
229                                  (byte) 0xb0, (byte) 0xde, (byte) 0x08, (byte) 0x00,
230                                  (byte) 0x20, (byte) 0x0c, (byte) 0x9a, (byte) 0x66 };
231             hs.setHeader(HeaderSet.TARGET, mnsTarget);
232 
233             synchronized (this) {
234                 mWaitingForRemote = true;
235             }
236             try {
237                 mHsConnect = mClientSession.connect(hs);
238                 if (D) Log.d(TAG, "OBEX session created");
239                 connected = true;
240             } catch (IOException e) {
241                 Log.e(TAG, "OBEX session connect error " + e.getMessage());
242             }
243             mConnected = connected;
244         }
245             synchronized (this) {
246                 mWaitingForRemote = false;
247         }
248     }
249 
250     /**
251      * Call this method to queue an event report to be send to the MNS server.
252      * @param eventBytes the encoded event data.
253      * @param masInstanceId the MasId of the instance sending the event.
254      */
sendEvent(byte[] eventBytes, int masInstanceId)255     public void sendEvent(byte[] eventBytes, int masInstanceId) {
256         // We need to check for null, to handle shutdown.
257         if(mHandler != null) {
258             Message msg = mHandler.obtainMessage(MSG_MNS_SEND_EVENT, masInstanceId, 0, eventBytes);
259             if(msg != null) {
260                 msg.sendToTarget();
261             }
262         }
263         notifyUpdateWakeLock();
264     }
265 
sendEventHandler(byte[] eventBytes, int masInstanceId)266     private int sendEventHandler(byte[] eventBytes, int masInstanceId) {
267 
268         boolean error = false;
269         int responseCode = -1;
270         HeaderSet request;
271         int maxChunkSize, bytesToWrite, bytesWritten = 0;
272         ClientSession clientSession = mClientSession;
273 
274         if ((!mConnected) || (clientSession == null)) {
275             Log.w(TAG, "sendEvent after disconnect:" + mConnected);
276             return responseCode;
277         }
278 
279         request = new HeaderSet();
280         BluetoothMapAppParams appParams = new BluetoothMapAppParams();
281         appParams.setMasInstanceId(masInstanceId);
282 
283         ClientOperation putOperation = null;
284         OutputStream outputStream = null;
285 
286         try {
287             request.setHeader(HeaderSet.TYPE, TYPE_EVENT);
288             request.setHeader(HeaderSet.APPLICATION_PARAMETER, appParams.EncodeParams());
289 
290             if (mHsConnect.mConnectionID != null) {
291                 request.mConnectionID = new byte[4];
292                 System.arraycopy(mHsConnect.mConnectionID, 0, request.mConnectionID, 0, 4);
293             } else {
294                 Log.w(TAG, "sendEvent: no connection ID");
295             }
296 
297             synchronized (this) {
298                 mWaitingForRemote = true;
299             }
300             // Send the header first and then the body
301             try {
302                 if (V) Log.v(TAG, "Send headerset Event ");
303                 putOperation = (ClientOperation)clientSession.put(request);
304                 // TODO - Should this be kept or Removed
305 
306             } catch (IOException e) {
307                 Log.e(TAG, "Error when put HeaderSet " + e.getMessage());
308                 error = true;
309             }
310             synchronized (this) {
311                 mWaitingForRemote = false;
312             }
313             if (!error) {
314                 try {
315                     if (V) Log.v(TAG, "Send headerset Event ");
316                     outputStream = putOperation.openOutputStream();
317                 } catch (IOException e) {
318                     Log.e(TAG, "Error when opening OutputStream " + e.getMessage());
319                     error = true;
320                 }
321             }
322 
323             if (!error) {
324 
325                 maxChunkSize = putOperation.getMaxPacketSize();
326 
327                 while (bytesWritten < eventBytes.length) {
328                     bytesToWrite = Math.min(maxChunkSize, eventBytes.length - bytesWritten);
329                     outputStream.write(eventBytes, bytesWritten, bytesToWrite);
330                     bytesWritten += bytesToWrite;
331                 }
332 
333                 if (bytesWritten == eventBytes.length) {
334                     Log.i(TAG, "SendEvent finished send length" + eventBytes.length);
335                 } else {
336                     error = true;
337                     putOperation.abort();
338                     Log.i(TAG, "SendEvent interrupted");
339                 }
340             }
341         } catch (IOException e) {
342             handleSendException(e.toString());
343             error = true;
344         } catch (IndexOutOfBoundsException e) {
345             handleSendException(e.toString());
346             error = true;
347         } finally {
348             try {
349                 if (outputStream != null) {
350                     outputStream.close();
351                 }
352             } catch (IOException e) {
353                 Log.e(TAG, "Error when closing stream after send " + e.getMessage());
354             }
355             try {
356                 if ((!error) && (putOperation != null)) {
357                     responseCode = putOperation.getResponseCode();
358                     if (responseCode != -1) {
359                         if (V) Log.v(TAG, "Put response code " + responseCode);
360                         if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
361                             Log.i(TAG, "Response error code is " + responseCode);
362                         }
363                     }
364                 }
365                 if (putOperation != null) {
366                     putOperation.close();
367                 }
368             } catch (IOException e) {
369                 Log.e(TAG, "Error when closing stream after send " + e.getMessage());
370             }
371         }
372 
373         return responseCode;
374     }
375 
handleSendException(String exception)376     private void handleSendException(String exception) {
377         Log.e(TAG, "Error when sending event: " + exception);
378     }
379 
notifyUpdateWakeLock()380     private void notifyUpdateWakeLock() {
381         if(mCallback != null) {
382             Message msg = Message.obtain(mCallback);
383             msg.what = BluetoothMapService.MSG_ACQUIRE_WAKE_LOCK;
384             msg.sendToTarget();
385         }
386     }
387 }
388