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