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