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