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 */ 15 package com.android.bluetooth.map; 16 17 import android.bluetooth.BluetoothDevice; 18 import android.bluetooth.BluetoothSocket; 19 import android.bluetooth.SdpMnsRecord; 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 import android.util.SparseBooleanArray; 27 28 import com.android.bluetooth.BluetoothObexTransport; 29 30 import java.io.IOException; 31 import java.io.OutputStream; 32 33 import javax.obex.ClientOperation; 34 import javax.obex.ClientSession; 35 import javax.obex.HeaderSet; 36 import javax.obex.ObexTransport; 37 import 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 */ 46 public 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 BluetoothMnsObexClient(BluetoothDevice remoteDevice, SdpMnsRecord mnsRecord, Handler callback)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 getMessageHandler()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 MnsSdpSearchInfo(boolean isSearchON, int masId, int notification)102 MnsSdpSearchInfo(boolean isSearchON, int masId, int notification) { 103 mIsSearchInProgress = isSearchON; 104 lastMasId = masId; 105 lastNotificationStatus = notification; 106 } 107 isSearchInProgress()108 public boolean isSearchInProgress() { 109 return mIsSearchInProgress; 110 } 111 setIsSearchInProgress(boolean isSearchON)112 public void setIsSearchInProgress(boolean isSearchON) { 113 mIsSearchInProgress = isSearchON; 114 } 115 } 116 117 private final class MnsObexClientHandler extends Handler { MnsObexClientHandler(Looper looper)118 private MnsObexClientHandler(Looper looper) { 119 super(looper); 120 } 121 122 @Override handleMessage(Message msg)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 isConnected()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 */ disconnect()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 */ shutdown()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 } 226 227 /* Disconnect if connected */ 228 disconnect(); 229 230 mRegisteredMasIds.clear(); 231 } 232 233 /** 234 * We store a list of registered MasIds only to control connect/disconnect 235 * @param masId 236 * @param notificationStatus 237 */ handleRegistration(int masId, int notificationStatus)238 public synchronized void handleRegistration(int masId, int notificationStatus) { 239 if (D) { 240 Log.d(TAG, "handleRegistration( " + masId + ", " + notificationStatus + ")"); 241 } 242 boolean sendObserverRegistration = true; 243 if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_NO) { 244 mRegisteredMasIds.delete(masId); 245 if (mMnsLstRegRqst != null && mMnsLstRegRqst.lastMasId == masId) { 246 //Clear last saved MNSSdpSearchInfo , if Disconnect requested for same MasId. 247 mMnsLstRegRqst = null; 248 } 249 } else if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) { 250 /* Connect if we do not have a connection, and start the content observers providing 251 * this thread as Handler. 252 */ 253 if (!isConnected()) { 254 if (D) { 255 Log.d(TAG, "handleRegistration: connect"); 256 } 257 connect(); 258 } 259 sendObserverRegistration = isConnected(); 260 mRegisteredMasIds.put(masId, true); // We don't use the value for anything 261 262 // Clear last saved MNSSdpSearchInfo after connect is processed. 263 mMnsLstRegRqst = null; 264 } 265 266 if (mRegisteredMasIds.size() == 0) { 267 // No more registrations - disconnect 268 if (D) { 269 Log.d(TAG, "handleRegistration: disconnect"); 270 } 271 disconnect(); 272 } 273 274 //Register ContentObserver After connect/disconnect MNS channel. 275 if (V) { 276 Log.v(TAG, "Send registerObserver: " + sendObserverRegistration); 277 } 278 if (mCallback != null && sendObserverRegistration) { 279 Message msg = Message.obtain(mCallback); 280 msg.what = BluetoothMapService.MSG_OBSERVER_REGISTRATION; 281 msg.arg1 = masId; 282 msg.arg2 = notificationStatus; 283 msg.sendToTarget(); 284 } 285 } 286 isValidMnsRecord()287 public boolean isValidMnsRecord() { 288 return (mMnsRecord != null); 289 } 290 setMnsRecord(SdpMnsRecord mnsRecord)291 public void setMnsRecord(SdpMnsRecord mnsRecord) { 292 if (V) { 293 Log.v(TAG, "setMNSRecord"); 294 } 295 if (isValidMnsRecord()) { 296 Log.w(TAG, "MNS Record already available. Still update."); 297 } 298 mMnsRecord = mnsRecord; 299 if (mMnsLstRegRqst != null) { 300 //SDP Search completed. 301 mMnsLstRegRqst.setIsSearchInProgress(false); 302 if (mHandler.hasMessages(MSG_MNS_NOTIFICATION_REGISTRATION)) { 303 mHandler.removeMessages(MSG_MNS_NOTIFICATION_REGISTRATION); 304 //Search Result obtained within MNS_SDP_SEARCH_DELAY timeout 305 if (!isValidMnsRecord()) { 306 // SDP info still not available for last trial. 307 // Clear saved info. 308 mMnsLstRegRqst = null; 309 } else { 310 if (V) { 311 Log.v(TAG, "Handle registration for last saved request"); 312 } 313 Message msgReg = mHandler.obtainMessage(MSG_MNS_NOTIFICATION_REGISTRATION); 314 msgReg.arg1 = mMnsLstRegRqst.lastMasId; 315 msgReg.arg2 = mMnsLstRegRqst.lastNotificationStatus; 316 if (V) { 317 Log.v(TAG, "SearchReg masId: " + msgReg.arg1 + " notfStatus: " 318 + msgReg.arg2); 319 } 320 //Handle notification registration. 321 mHandler.sendMessageDelayed(msgReg, MNS_NOTIFICATION_DELAY); 322 } 323 } 324 } else { 325 if (V) { 326 Log.v(TAG, "No last saved MNSSDPInfo to handle"); 327 } 328 } 329 } 330 connect()331 public void connect() { 332 333 mConnected = true; 334 335 BluetoothSocket btSocket = null; 336 try { 337 // TODO: Do SDP record search again? 338 if (isValidMnsRecord() && mMnsRecord.getL2capPsm() > 0) { 339 // Do L2CAP connect 340 btSocket = mRemoteDevice.createL2capSocket(mMnsRecord.getL2capPsm()); 341 342 } else if (isValidMnsRecord() && mMnsRecord.getRfcommChannelNumber() > 0) { 343 // Do Rfcomm connect 344 btSocket = mRemoteDevice.createRfcommSocket(mMnsRecord.getRfcommChannelNumber()); 345 } else { 346 // This should not happen... 347 Log.e(TAG, "Invalid SDP content - attempt a connect to UUID..."); 348 // TODO: Why insecure? - is it because the link is already encrypted? 349 btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord( 350 BLUETOOTH_UUID_OBEX_MNS.getUuid()); 351 } 352 btSocket.connect(); 353 } catch (IOException e) { 354 Log.e(TAG, "BtSocket Connect error " + e.getMessage(), e); 355 // TODO: do we need to report error somewhere? 356 mConnected = false; 357 return; 358 } 359 360 mTransport = new BluetoothObexTransport(btSocket); 361 362 try { 363 mClientSession = new ClientSession(mTransport); 364 } catch (IOException e1) { 365 Log.e(TAG, "OBEX session create error " + e1.getMessage()); 366 mConnected = false; 367 } 368 if (mConnected && mClientSession != null) { 369 boolean connected = false; 370 HeaderSet hs = new HeaderSet(); 371 // bb582b41-420c-11db-b0de-0800200c9a66 372 byte[] mnsTarget = { 373 (byte) 0xbb, 374 (byte) 0x58, 375 (byte) 0x2b, 376 (byte) 0x41, 377 (byte) 0x42, 378 (byte) 0x0c, 379 (byte) 0x11, 380 (byte) 0xdb, 381 (byte) 0xb0, 382 (byte) 0xde, 383 (byte) 0x08, 384 (byte) 0x00, 385 (byte) 0x20, 386 (byte) 0x0c, 387 (byte) 0x9a, 388 (byte) 0x66 389 }; 390 hs.setHeader(HeaderSet.TARGET, mnsTarget); 391 392 synchronized (this) { 393 mWaitingForRemote = true; 394 } 395 try { 396 mHsConnect = mClientSession.connect(hs); 397 if (D) { 398 Log.d(TAG, "OBEX session created"); 399 } 400 connected = true; 401 } catch (IOException e) { 402 Log.e(TAG, "OBEX session connect error " + e.getMessage()); 403 } 404 mConnected = connected; 405 } 406 synchronized (this) { 407 mWaitingForRemote = false; 408 } 409 } 410 411 /** 412 * Call this method to queue an event report to be send to the MNS server. 413 * @param eventBytes the encoded event data. 414 * @param masInstanceId the MasId of the instance sending the event. 415 */ sendEvent(byte[] eventBytes, int masInstanceId)416 public void sendEvent(byte[] eventBytes, int masInstanceId) { 417 // We need to check for null, to handle shutdown. 418 if (mHandler != null) { 419 Message msg = mHandler.obtainMessage(MSG_MNS_SEND_EVENT, masInstanceId, 0, eventBytes); 420 if (msg != null) { 421 msg.sendToTarget(); 422 } 423 } 424 notifyUpdateWakeLock(); 425 } 426 notifyMnsSdpSearch()427 private void notifyMnsSdpSearch() { 428 if (mCallback != null) { 429 Message msg = Message.obtain(mCallback); 430 msg.what = BluetoothMapService.MSG_MNS_SDP_SEARCH; 431 msg.sendToTarget(); 432 } 433 } 434 sendEventHandler(byte[] eventBytes, int masInstanceId)435 private int sendEventHandler(byte[] eventBytes, int masInstanceId) { 436 437 boolean error = false; 438 int responseCode = -1; 439 HeaderSet request; 440 int maxChunkSize, bytesToWrite, bytesWritten = 0; 441 ClientSession clientSession = mClientSession; 442 443 if ((!mConnected) || (clientSession == null)) { 444 Log.w(TAG, "sendEvent after disconnect:" + mConnected); 445 return responseCode; 446 } 447 448 request = new HeaderSet(); 449 BluetoothMapAppParams appParams = new BluetoothMapAppParams(); 450 appParams.setMasInstanceId(masInstanceId); 451 452 ClientOperation putOperation = null; 453 OutputStream outputStream = null; 454 455 try { 456 request.setHeader(HeaderSet.TYPE, TYPE_EVENT); 457 request.setHeader(HeaderSet.APPLICATION_PARAMETER, appParams.encodeParams()); 458 459 if (mHsConnect.mConnectionID != null) { 460 request.mConnectionID = new byte[4]; 461 System.arraycopy(mHsConnect.mConnectionID, 0, request.mConnectionID, 0, 4); 462 } else { 463 Log.w(TAG, "sendEvent: no connection ID"); 464 } 465 466 synchronized (this) { 467 mWaitingForRemote = true; 468 } 469 // Send the header first and then the body 470 try { 471 if (V) { 472 Log.v(TAG, "Send headerset Event "); 473 } 474 putOperation = (ClientOperation) clientSession.put(request); 475 // TODO - Should this be kept or Removed 476 477 } catch (IOException e) { 478 Log.e(TAG, "Error when put HeaderSet " + e.getMessage()); 479 error = true; 480 } 481 synchronized (this) { 482 mWaitingForRemote = false; 483 } 484 if (!error) { 485 try { 486 if (V) { 487 Log.v(TAG, "Send headerset Event "); 488 } 489 outputStream = putOperation.openOutputStream(); 490 } catch (IOException e) { 491 Log.e(TAG, "Error when opening OutputStream " + e.getMessage()); 492 error = true; 493 } 494 } 495 496 if (!error) { 497 498 maxChunkSize = putOperation.getMaxPacketSize(); 499 500 while (bytesWritten < eventBytes.length) { 501 bytesToWrite = Math.min(maxChunkSize, eventBytes.length - bytesWritten); 502 outputStream.write(eventBytes, bytesWritten, bytesToWrite); 503 bytesWritten += bytesToWrite; 504 } 505 506 if (bytesWritten == eventBytes.length) { 507 Log.i(TAG, "SendEvent finished send length" + eventBytes.length); 508 } else { 509 error = true; 510 putOperation.abort(); 511 Log.i(TAG, "SendEvent interrupted"); 512 } 513 } 514 } catch (IOException e) { 515 handleSendException(e.toString()); 516 error = true; 517 } catch (IndexOutOfBoundsException e) { 518 handleSendException(e.toString()); 519 error = true; 520 } finally { 521 try { 522 if (outputStream != null) { 523 outputStream.close(); 524 } 525 } catch (IOException e) { 526 Log.e(TAG, "Error when closing stream after send " + e.getMessage()); 527 } 528 try { 529 if ((!error) && (putOperation != null)) { 530 responseCode = putOperation.getResponseCode(); 531 if (responseCode != -1) { 532 if (V) { 533 Log.v(TAG, "Put response code " + responseCode); 534 } 535 if (responseCode != ResponseCodes.OBEX_HTTP_OK) { 536 Log.i(TAG, "Response error code is " + responseCode); 537 } 538 } 539 } 540 if (putOperation != null) { 541 putOperation.close(); 542 } 543 } catch (IOException e) { 544 Log.e(TAG, "Error when closing stream after send " + e.getMessage()); 545 } 546 } 547 548 return responseCode; 549 } 550 handleSendException(String exception)551 private void handleSendException(String exception) { 552 Log.e(TAG, "Error when sending event: " + exception); 553 } 554 notifyUpdateWakeLock()555 private void notifyUpdateWakeLock() { 556 if (mCallback != null) { 557 Message msg = Message.obtain(mCallback); 558 msg.what = BluetoothMapService.MSG_ACQUIRE_WAKE_LOCK; 559 msg.sendToTarget(); 560 } 561 } 562 } 563