BluetoothMnsObexClient.java revision 326b5e610063ac24c0ba467ac585bd4c7f618a67
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*/
15package com.android.bluetooth.map;
16
17import android.bluetooth.BluetoothDevice;
18import android.bluetooth.BluetoothSocket;
19import android.os.Handler;
20import android.os.HandlerThread;
21import android.os.Looper;
22import android.os.Message;
23import android.os.ParcelUuid;
24import android.util.Log;
25import android.util.SparseBooleanArray;
26
27import java.io.IOException;
28import java.io.OutputStream;
29
30import javax.obex.ClientOperation;
31import javax.obex.ClientSession;
32import javax.obex.HeaderSet;
33import javax.obex.ObexTransport;
34import 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 */
43public 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
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
84    public Handler getMessageHandler() {
85        return mHandler;
86    }
87
88    private final class MnsObexClientHandler extends Handler {
89        private MnsObexClientHandler(Looper looper) {
90            super(looper);
91        }
92
93        @Override
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
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     */
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     */
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     */
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
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     */
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
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
376    private void handleSendException(String exception) {
377        Log.e(TAG, "Error when sending event: " + exception);
378    }
379
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