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 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
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
93    public Handler getMessageHandler() {
94        return mHandler;
95    }
96
97    class MnsSdpSearchInfo {
98        private boolean isSearchInProgress;
99        int lastMasId;
100        int lastNotificationStatus;
101
102        MnsSdpSearchInfo (boolean isSearchON, int masId, int notification) {
103            isSearchInProgress = isSearchON;
104            lastMasId = masId;
105            lastNotificationStatus = notification;
106        }
107
108        public boolean isSearchInProgress() {
109            return isSearchInProgress;
110        }
111
112        public void setIsSearchInProgress(boolean isSearchON) {
113            isSearchInProgress = isSearchON;
114        }
115    }
116
117    private final class MnsObexClientHandler extends Handler {
118        private MnsObexClientHandler(Looper looper) {
119            super(looper);
120        }
121
122        @Override
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
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     */
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     */
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     */
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
264    public boolean isValidMnsRecord() {
265        return (mMnsRecord != null);
266    }
267
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
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     */
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
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
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
499    private void handleSendException(String exception) {
500        Log.e(TAG, "Error when sending event: " + exception);
501    }
502
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