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