1/*
2* Copyright (C) 2013 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.content.Context;
20import android.os.Handler;
21import android.os.HandlerThread;
22import android.os.Looper;
23import android.os.Message;
24import android.os.ParcelUuid;
25import android.util.Log;
26
27import java.io.BufferedInputStream;
28import java.io.File;
29import java.io.FileInputStream;
30import java.io.IOException;
31import java.io.OutputStream;
32
33import javax.obex.ApplicationParameter;
34import javax.obex.ClientOperation;
35import javax.obex.ClientSession;
36import javax.obex.HeaderSet;
37import javax.obex.ObexTransport;
38import javax.obex.ResponseCodes;
39
40/**
41 * The Message Notification Service class runs its own message handler thread,
42 * to avoid executing long operations on the MAP service Thread.
43 * This handler context is passed to the content observers,
44 * hence all call-backs (and thereby transmission of data) is executed
45 * from this thread.
46 */
47public class BluetoothMnsObexClient {
48
49    private static final String TAG = "BluetoothMnsObexClient";
50    private static final boolean D = false;
51    private static final boolean V = false;
52
53    private ObexTransport mTransport;
54    private Context mContext;
55    public Handler mHandler = null;
56    private volatile boolean mWaitingForRemote;
57    private static final String TYPE_EVENT = "x-bt/MAP-event-report";
58    private ClientSession mClientSession;
59    private boolean mConnected = false;
60    BluetoothDevice mRemoteDevice;
61    private BluetoothMapContentObserver mObserver;
62    private boolean mObserverRegistered = false;
63
64    // Used by the MAS to forward notification registrations
65    public static final int MSG_MNS_NOTIFICATION_REGISTRATION = 1;
66
67
68    public static final ParcelUuid BluetoothUuid_ObexMns =
69            ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB");
70
71
72    public BluetoothMnsObexClient(Context context, BluetoothDevice remoteDevice) {
73        if (remoteDevice == null) {
74            throw new NullPointerException("Obex transport is null");
75        }
76        HandlerThread thread = new HandlerThread("BluetoothMnsObexClient");
77        thread.start();
78        Looper looper = thread.getLooper();
79        mHandler = new MnsObexClientHandler(looper);
80        mContext = context;
81        mRemoteDevice = remoteDevice;
82        mObserver = new BluetoothMapContentObserver(mContext);
83        mObserver.init();
84    }
85
86    public Handler getMessageHandler() {
87        return mHandler;
88    }
89
90    public BluetoothMapContentObserver getContentObserver() {
91        return mObserver;
92    }
93
94    private final class MnsObexClientHandler extends Handler {
95        private MnsObexClientHandler(Looper looper) {
96            super(looper);
97        }
98
99        @Override
100        public void handleMessage(Message msg) {
101            switch (msg.what) {
102            case MSG_MNS_NOTIFICATION_REGISTRATION:
103                handleRegistration(msg.arg1 /*masId*/, msg.arg2 /*status*/);
104                break;
105            default:
106                break;
107            }
108        }
109    }
110
111    public boolean isConnected() {
112        return mConnected;
113    }
114
115    public void disconnect() {
116        try {
117            if (mClientSession != null) {
118                mClientSession.disconnect(null);
119                if (D) Log.d(TAG, "OBEX session disconnected");
120            }
121        } catch (IOException e) {
122            Log.w(TAG, "OBEX session disconnect error " + e.getMessage());
123        }
124        try {
125            if (mClientSession != null) {
126                if (D) Log.d(TAG, "OBEX session close mClientSession");
127                mClientSession.close();
128                mClientSession = null;
129                if (D) Log.d(TAG, "OBEX session closed");
130            }
131        } catch (IOException e) {
132            Log.w(TAG, "OBEX session close error:" + e.getMessage());
133        }
134        if (mTransport != null) {
135            try {
136                if (D) Log.d(TAG, "Close Obex Transport");
137                mTransport.close();
138                mTransport = null;
139                mConnected = false;
140                if (D) Log.d(TAG, "Obex Transport Closed");
141            } catch (IOException e) {
142                Log.e(TAG, "mTransport.close error: " + e.getMessage());
143            }
144        }
145        if(mObserverRegistered) {
146            mObserver.unregisterObserver();
147            mObserverRegistered = false;
148        }
149        if (mObserver != null) {
150            mObserver.deinit();
151            mObserver = null;
152        }
153        if (mHandler != null) {
154            // Shut down the thread
155            mHandler.removeCallbacksAndMessages(null);
156            Looper looper = mHandler.getLooper();
157            if (looper != null) {
158                looper.quit();
159            }
160            mHandler = null;
161        }
162    }
163
164    private HeaderSet hsConnect = null;
165
166    public void handleRegistration(int masId, int notificationStatus){
167        Log.d(TAG, "handleRegistration( " + masId + ", " + notificationStatus + ")");
168
169        if((isConnected() == false) &&
170           (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES)) {
171            Log.d(TAG, "handleRegistration: connect");
172            connect();
173        }
174
175        if(notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_NO) {
176            // Unregister - should we disconnect, or keep the connection? - the spec. says nothing about this.
177            if(mObserverRegistered == true) {
178                mObserver.unregisterObserver();
179                mObserverRegistered = false;
180            }
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(mObserverRegistered == false) {
186                mObserver.registerObserver(this, masId);
187                mObserverRegistered = true;
188            }
189        }
190    }
191
192    public void connect() {
193        Log.d(TAG, "handleRegistration: connect 2");
194
195        BluetoothSocket btSocket = null;
196        try {
197            btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord(
198                    BluetoothUuid_ObexMns.getUuid());
199            btSocket.connect();
200        } catch (IOException e) {
201            Log.e(TAG, "BtSocket Connect error " + e.getMessage(), e);
202            // TODO: do we need to report error somewhere?
203            return;
204        }
205
206        mTransport = new BluetoothMnsRfcommTransport(btSocket);
207
208        try {
209            mClientSession = new ClientSession(mTransport);
210            mConnected = true;
211        } catch (IOException e1) {
212            Log.e(TAG, "OBEX session create error " + e1.getMessage());
213        }
214        if (mConnected && mClientSession != null) {
215            mConnected = false;
216            HeaderSet hs = new HeaderSet();
217            // bb582b41-420c-11db-b0de-0800200c9a66
218            byte[] mnsTarget = { (byte) 0xbb, (byte) 0x58, (byte) 0x2b, (byte) 0x41,
219                                 (byte) 0x42, (byte) 0x0c, (byte) 0x11, (byte) 0xdb,
220                                 (byte) 0xb0, (byte) 0xde, (byte) 0x08, (byte) 0x00,
221                                 (byte) 0x20, (byte) 0x0c, (byte) 0x9a, (byte) 0x66 };
222            hs.setHeader(HeaderSet.TARGET, mnsTarget);
223
224            synchronized (this) {
225                mWaitingForRemote = true;
226            }
227            try {
228                hsConnect = mClientSession.connect(hs);
229                if (D) Log.d(TAG, "OBEX session created");
230                mConnected = true;
231            } catch (IOException e) {
232                Log.e(TAG, "OBEX session connect error " + e.getMessage());
233            }
234        }
235            synchronized (this) {
236                mWaitingForRemote = false;
237        }
238    }
239
240    public int sendEvent(byte[] eventBytes, int masInstanceId) {
241
242        boolean error = false;
243        int responseCode = -1;
244        HeaderSet request;
245        int maxChunkSize, bytesToWrite, bytesWritten = 0;
246        request = new HeaderSet();
247        BluetoothMapAppParams appParams = new BluetoothMapAppParams();
248        appParams.setMasInstanceId(masInstanceId);
249
250        ClientOperation putOperation = null;
251        OutputStream outputStream = null;
252
253        try {
254            request.setHeader(HeaderSet.TYPE, TYPE_EVENT);
255            request.setHeader(HeaderSet.APPLICATION_PARAMETER, appParams.EncodeParams());
256
257            request.mConnectionID = new byte[4];
258            System.arraycopy(hsConnect.mConnectionID, 0, request.mConnectionID, 0, 4);
259
260            synchronized (this) {
261                mWaitingForRemote = true;
262            }
263            // Send the header first and then the body
264            try {
265                if (V) Log.v(TAG, "Send headerset Event ");
266                putOperation = (ClientOperation)mClientSession.put(request);
267                // TODO - Should this be kept or Removed
268
269            } catch (IOException e) {
270                Log.e(TAG, "Error when put HeaderSet " + e.getMessage());
271                error = true;
272            }
273            synchronized (this) {
274                mWaitingForRemote = false;
275            }
276            if (!error) {
277                try {
278                    if (V) Log.v(TAG, "Send headerset Event ");
279                    outputStream = putOperation.openOutputStream();
280                } catch (IOException e) {
281                    Log.e(TAG, "Error when opening OutputStream " + e.getMessage());
282                    error = true;
283                }
284            }
285
286            if (!error) {
287
288                maxChunkSize = putOperation.getMaxPacketSize();
289
290                while (bytesWritten < eventBytes.length) {
291                    bytesToWrite = Math.min(maxChunkSize, eventBytes.length - bytesWritten);
292                    outputStream.write(eventBytes, bytesWritten, bytesToWrite);
293                    bytesWritten += bytesToWrite;
294                }
295
296                if (bytesWritten == eventBytes.length) {
297                    Log.i(TAG, "SendEvent finished send length" + eventBytes.length);
298                    outputStream.close();
299                } else {
300                    error = true;
301                    outputStream.close();
302                    putOperation.abort();
303                    Log.i(TAG, "SendEvent interrupted");
304                }
305            }
306        } catch (IOException e) {
307            handleSendException(e.toString());
308            error = true;
309        } catch (IndexOutOfBoundsException e) {
310            handleSendException(e.toString());
311            error = true;
312        } finally {
313            try {
314                if (!error) {
315                    responseCode = putOperation.getResponseCode();
316                    if (responseCode != -1) {
317                        if (V) Log.v(TAG, "Put response code " + responseCode);
318                        if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
319                            Log.i(TAG, "Response error code is " + responseCode);
320                        }
321                    }
322                }
323                if (putOperation != null) {
324                   putOperation.close();
325                }
326            } catch (IOException e) {
327                Log.e(TAG, "Error when closing stream after send " + e.getMessage());
328            }
329        }
330
331        return responseCode;
332    }
333
334    private void handleSendException(String exception) {
335        Log.e(TAG, "Error when sending event: " + exception);
336    }
337}
338