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