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 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, 78 SdpMnsRecord mnsRecord, 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 isSearchInProgress; 99 int lastMasId; 100 int lastNotificationStatus; 101 MnsSdpSearchInfo(boolean isSearchON, int masId, int notification)102 MnsSdpSearchInfo (boolean isSearchON, int masId, int notification) { 103 isSearchInProgress = isSearchON; 104 lastMasId = masId; 105 lastNotificationStatus = notification; 106 } 107 isSearchInProgress()108 public boolean isSearchInProgress() { 109 return isSearchInProgress; 110 } 111 setIsSearchInProgress(boolean isSearchON)112 public void setIsSearchInProgress(boolean isSearchON) { 113 isSearchInProgress = 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) Log.v(TAG, "Reg masId: " + msg.arg1 + " notfStatus: " + msg.arg2); 127 if (isValidMnsRecord()) { 128 handleRegistration(msg.arg1 /*masId*/, msg.arg2 /*status*/); 129 } else { 130 //Should not happen 131 if (D) Log.d(TAG, "MNS SDP info not available yet - Cannot Connect."); 132 } 133 break; 134 case MSG_MNS_SEND_EVENT: 135 sendEventHandler((byte[])msg.obj/*byte[]*/, msg.arg1 /*masId*/); 136 break; 137 case MSG_MNS_SDP_SEARCH_REGISTRATION: 138 //Initiate SDP Search 139 notifyMnsSdpSearch(); 140 //Save the mns search info 141 mMnsLstRegRqst = new MnsSdpSearchInfo(true, msg.arg1, msg.arg2); 142 //Handle notification registration. 143 Message msgReg = 144 mHandler.obtainMessage(MSG_MNS_NOTIFICATION_REGISTRATION, 145 msg.arg1, msg.arg2); 146 if (V) Log.v(TAG, "SearchReg masId: " + msg.arg1 + " notfStatus: " + msg.arg2); 147 mHandler.sendMessageDelayed(msgReg, MNS_SDP_SEARCH_DELAY); 148 break; 149 default: 150 break; 151 } 152 } 153 } 154 isConnected()155 public boolean isConnected() { 156 return mConnected; 157 } 158 159 /** 160 * Disconnect the connection to MNS server. 161 * Call this when the MAS client requests a de-registration on events. 162 */ disconnect()163 public synchronized void disconnect() { 164 try { 165 if (mClientSession != null) { 166 mClientSession.disconnect(null); 167 if (D) Log.d(TAG, "OBEX session disconnected"); 168 } 169 } catch (IOException e) { 170 Log.w(TAG, "OBEX session disconnect error " + e.getMessage()); 171 } 172 try { 173 if (mClientSession != null) { 174 if (D) Log.d(TAG, "OBEX session close mClientSession"); 175 mClientSession.close(); 176 mClientSession = null; 177 if (D) Log.d(TAG, "OBEX session closed"); 178 } 179 } catch (IOException e) { 180 Log.w(TAG, "OBEX session close error:" + e.getMessage()); 181 } 182 if (mTransport != null) { 183 try { 184 if (D) Log.d(TAG, "Close Obex Transport"); 185 mTransport.close(); 186 mTransport = null; 187 mConnected = false; 188 if (D) Log.d(TAG, "Obex Transport Closed"); 189 } catch (IOException e) { 190 Log.e(TAG, "mTransport.close error: " + e.getMessage()); 191 } 192 } 193 } 194 195 /** 196 * Shutdown the MNS. 197 */ shutdown()198 public synchronized void shutdown() { 199 /* should shutdown handler thread first to make sure 200 * handleRegistration won't be called when disconnect 201 */ 202 if (mHandler != null) { 203 // Shut down the thread 204 mHandler.removeCallbacksAndMessages(null); 205 Looper looper = mHandler.getLooper(); 206 if (looper != null) { 207 looper.quit(); 208 } 209 mHandler = null; 210 } 211 212 /* Disconnect if connected */ 213 disconnect(); 214 215 mRegisteredMasIds.clear(); 216 } 217 218 /** 219 * We store a list of registered MasIds only to control connect/disconnect 220 * @param masId 221 * @param notificationStatus 222 */ handleRegistration(int masId, int notificationStatus)223 public synchronized void handleRegistration(int masId, int notificationStatus){ 224 if(D) Log.d(TAG, "handleRegistration( " + masId + ", " + notificationStatus + ")"); 225 boolean sendObserverRegistration = true; 226 if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_NO) { 227 mRegisteredMasIds.delete(masId); 228 if (mMnsLstRegRqst != null && mMnsLstRegRqst.lastMasId == masId) { 229 //Clear last saved MNSSdpSearchInfo , if Disconnect requested for same MasId. 230 mMnsLstRegRqst = null; 231 } 232 } else if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) { 233 /* Connect if we do not have a connection, and start the content observers providing 234 * this thread as Handler. 235 */ 236 if (isConnected() == false) { 237 if(D) Log.d(TAG, "handleRegistration: connect"); 238 connect(); 239 } 240 sendObserverRegistration = isConnected(); 241 mRegisteredMasIds.put(masId, true); // We don't use the value for anything 242 243 // Clear last saved MNSSdpSearchInfo after connect is processed. 244 mMnsLstRegRqst = null; 245 } 246 247 if (mRegisteredMasIds.size() == 0) { 248 // No more registrations - disconnect 249 if(D) Log.d(TAG, "handleRegistration: disconnect"); 250 disconnect(); 251 } 252 253 //Register ContentObserver After connect/disconnect MNS channel. 254 if (V) Log.v(TAG, "Send registerObserver: " + sendObserverRegistration); 255 if (mCallback != null && sendObserverRegistration) { 256 Message msg = Message.obtain(mCallback); 257 msg.what = BluetoothMapService.MSG_OBSERVER_REGISTRATION; 258 msg.arg1 = masId; 259 msg.arg2 = notificationStatus; 260 msg.sendToTarget(); 261 } 262 } 263 isValidMnsRecord()264 public boolean isValidMnsRecord() { 265 return (mMnsRecord != null); 266 } 267 setMnsRecord(SdpMnsRecord mnsRecord)268 public void setMnsRecord(SdpMnsRecord mnsRecord) { 269 if (V) Log.v(TAG, "setMNSRecord"); 270 if (isValidMnsRecord()) { 271 Log.w(TAG,"MNS Record already available. Still update."); 272 } 273 mMnsRecord = mnsRecord; 274 if (mMnsLstRegRqst != null) { 275 //SDP Search completed. 276 mMnsLstRegRqst.setIsSearchInProgress(false); 277 if (mHandler.hasMessages(MSG_MNS_NOTIFICATION_REGISTRATION)) { 278 mHandler.removeMessages(MSG_MNS_NOTIFICATION_REGISTRATION); 279 //Search Result obtained within MNS_SDP_SEARCH_DELAY timeout 280 if (!isValidMnsRecord()) { 281 // SDP info still not available for last trial. 282 // Clear saved info. 283 mMnsLstRegRqst = null; 284 } else { 285 if (V) Log.v(TAG, "Handle registration for last saved request"); 286 Message msgReg = 287 mHandler.obtainMessage(MSG_MNS_NOTIFICATION_REGISTRATION); 288 msgReg.arg1 = mMnsLstRegRqst.lastMasId; 289 msgReg.arg2 = mMnsLstRegRqst.lastNotificationStatus; 290 if (V) Log.v(TAG, "SearchReg masId: " + msgReg.arg1 291 + " notfStatus: " + msgReg.arg2); 292 //Handle notification registration. 293 mHandler.sendMessageDelayed(msgReg, MNS_NOTIFICATION_DELAY); 294 } 295 } 296 } else { 297 if (V) Log.v(TAG, "No last saved MNSSDPInfo to handle"); 298 } 299 } 300 connect()301 public void connect() { 302 303 mConnected = true; 304 305 BluetoothSocket btSocket = null; 306 try { 307 // TODO: Do SDP record search again? 308 if (isValidMnsRecord() && mMnsRecord.getL2capPsm() > 0) { 309 // Do L2CAP connect 310 btSocket = mRemoteDevice.createL2capSocket(mMnsRecord.getL2capPsm()); 311 312 } else if (isValidMnsRecord() && mMnsRecord.getRfcommChannelNumber() > 0) { 313 // Do Rfcomm connect 314 btSocket = mRemoteDevice.createRfcommSocket(mMnsRecord.getRfcommChannelNumber()); 315 } else { 316 // This should not happen... 317 Log.e(TAG, "Invalid SDP content - attempt a connect to UUID..."); 318 // TODO: Why insecure? - is it because the link is already encrypted? 319 btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord( 320 BLUETOOTH_UUID_OBEX_MNS.getUuid()); 321 } 322 btSocket.connect(); 323 } catch (IOException e) { 324 Log.e(TAG, "BtSocket Connect error " + e.getMessage(), e); 325 // TODO: do we need to report error somewhere? 326 mConnected = false; 327 return; 328 } 329 330 mTransport = new BluetoothObexTransport(btSocket); 331 332 try { 333 mClientSession = new ClientSession(mTransport); 334 } catch (IOException e1) { 335 Log.e(TAG, "OBEX session create error " + e1.getMessage()); 336 mConnected = false; 337 } 338 if (mConnected && mClientSession != null) { 339 boolean connected = false; 340 HeaderSet hs = new HeaderSet(); 341 // bb582b41-420c-11db-b0de-0800200c9a66 342 byte[] mnsTarget = { (byte) 0xbb, (byte) 0x58, (byte) 0x2b, (byte) 0x41, 343 (byte) 0x42, (byte) 0x0c, (byte) 0x11, (byte) 0xdb, 344 (byte) 0xb0, (byte) 0xde, (byte) 0x08, (byte) 0x00, 345 (byte) 0x20, (byte) 0x0c, (byte) 0x9a, (byte) 0x66 }; 346 hs.setHeader(HeaderSet.TARGET, mnsTarget); 347 348 synchronized (this) { 349 mWaitingForRemote = true; 350 } 351 try { 352 mHsConnect = mClientSession.connect(hs); 353 if (D) Log.d(TAG, "OBEX session created"); 354 connected = true; 355 } catch (IOException e) { 356 Log.e(TAG, "OBEX session connect error " + e.getMessage()); 357 } 358 mConnected = connected; 359 } 360 synchronized (this) { 361 mWaitingForRemote = false; 362 } 363 } 364 365 /** 366 * Call this method to queue an event report to be send to the MNS server. 367 * @param eventBytes the encoded event data. 368 * @param masInstanceId the MasId of the instance sending the event. 369 */ sendEvent(byte[] eventBytes, int masInstanceId)370 public void sendEvent(byte[] eventBytes, int masInstanceId) { 371 // We need to check for null, to handle shutdown. 372 if(mHandler != null) { 373 Message msg = mHandler.obtainMessage(MSG_MNS_SEND_EVENT, masInstanceId, 0, eventBytes); 374 if(msg != null) { 375 msg.sendToTarget(); 376 } 377 } 378 notifyUpdateWakeLock(); 379 } 380 notifyMnsSdpSearch()381 private void notifyMnsSdpSearch() { 382 if (mCallback != null) { 383 Message msg = Message.obtain(mCallback); 384 msg.what = BluetoothMapService.MSG_MNS_SDP_SEARCH; 385 msg.sendToTarget(); 386 } 387 } 388 sendEventHandler(byte[] eventBytes, int masInstanceId)389 private int sendEventHandler(byte[] eventBytes, int masInstanceId) { 390 391 boolean error = false; 392 int responseCode = -1; 393 HeaderSet request; 394 int maxChunkSize, bytesToWrite, bytesWritten = 0; 395 ClientSession clientSession = mClientSession; 396 397 if ((!mConnected) || (clientSession == null)) { 398 Log.w(TAG, "sendEvent after disconnect:" + mConnected); 399 return responseCode; 400 } 401 402 request = new HeaderSet(); 403 BluetoothMapAppParams appParams = new BluetoothMapAppParams(); 404 appParams.setMasInstanceId(masInstanceId); 405 406 ClientOperation putOperation = null; 407 OutputStream outputStream = null; 408 409 try { 410 request.setHeader(HeaderSet.TYPE, TYPE_EVENT); 411 request.setHeader(HeaderSet.APPLICATION_PARAMETER, appParams.EncodeParams()); 412 413 if (mHsConnect.mConnectionID != null) { 414 request.mConnectionID = new byte[4]; 415 System.arraycopy(mHsConnect.mConnectionID, 0, request.mConnectionID, 0, 4); 416 } else { 417 Log.w(TAG, "sendEvent: no connection ID"); 418 } 419 420 synchronized (this) { 421 mWaitingForRemote = true; 422 } 423 // Send the header first and then the body 424 try { 425 if (V) Log.v(TAG, "Send headerset Event "); 426 putOperation = (ClientOperation)clientSession.put(request); 427 // TODO - Should this be kept or Removed 428 429 } catch (IOException e) { 430 Log.e(TAG, "Error when put HeaderSet " + e.getMessage()); 431 error = true; 432 } 433 synchronized (this) { 434 mWaitingForRemote = false; 435 } 436 if (!error) { 437 try { 438 if (V) Log.v(TAG, "Send headerset Event "); 439 outputStream = putOperation.openOutputStream(); 440 } catch (IOException e) { 441 Log.e(TAG, "Error when opening OutputStream " + e.getMessage()); 442 error = true; 443 } 444 } 445 446 if (!error) { 447 448 maxChunkSize = putOperation.getMaxPacketSize(); 449 450 while (bytesWritten < eventBytes.length) { 451 bytesToWrite = Math.min(maxChunkSize, eventBytes.length - bytesWritten); 452 outputStream.write(eventBytes, bytesWritten, bytesToWrite); 453 bytesWritten += bytesToWrite; 454 } 455 456 if (bytesWritten == eventBytes.length) { 457 Log.i(TAG, "SendEvent finished send length" + eventBytes.length); 458 } else { 459 error = true; 460 putOperation.abort(); 461 Log.i(TAG, "SendEvent interrupted"); 462 } 463 } 464 } catch (IOException e) { 465 handleSendException(e.toString()); 466 error = true; 467 } catch (IndexOutOfBoundsException e) { 468 handleSendException(e.toString()); 469 error = true; 470 } finally { 471 try { 472 if (outputStream != null) { 473 outputStream.close(); 474 } 475 } catch (IOException e) { 476 Log.e(TAG, "Error when closing stream after send " + e.getMessage()); 477 } 478 try { 479 if ((!error) && (putOperation != null)) { 480 responseCode = putOperation.getResponseCode(); 481 if (responseCode != -1) { 482 if (V) Log.v(TAG, "Put response code " + responseCode); 483 if (responseCode != ResponseCodes.OBEX_HTTP_OK) { 484 Log.i(TAG, "Response error code is " + responseCode); 485 } 486 } 487 } 488 if (putOperation != null) { 489 putOperation.close(); 490 } 491 } catch (IOException e) { 492 Log.e(TAG, "Error when closing stream after send " + e.getMessage()); 493 } 494 } 495 496 return responseCode; 497 } 498 handleSendException(String exception)499 private void handleSendException(String exception) { 500 Log.e(TAG, "Error when sending event: " + exception); 501 } 502 notifyUpdateWakeLock()503 private void notifyUpdateWakeLock() { 504 if(mCallback != null) { 505 Message msg = Message.obtain(mCallback); 506 msg.what = BluetoothMapService.MSG_ACQUIRE_WAKE_LOCK; 507 msg.sendToTarget(); 508 } 509 } 510 } 511