1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.bluetooth.avrcpcontroller; 18 19 import android.bluetooth.BluetoothDevice; 20 import android.bluetooth.BluetoothProfile; 21 import android.bluetooth.BluetoothSocket; 22 import android.os.Handler; 23 import android.os.HandlerThread; 24 import android.os.Looper; 25 import android.os.Message; 26 import android.util.Log; 27 28 import com.android.bluetooth.BluetoothObexTransport; 29 30 import java.io.IOException; 31 import java.lang.ref.WeakReference; 32 33 import javax.obex.ClientSession; 34 import javax.obex.HeaderSet; 35 import javax.obex.ResponseCodes; 36 37 /** 38 * A client to a remote device's BIP Image Pull Server, as defined by a PSM passed in at 39 * construction time. 40 * 41 * Once the client connection is established you can use this client to get image properties and 42 * download images. The connection to the server is held open to service multiple requests. 43 * 44 * Client is good for one connection lifecycle. Please call shutdown() to clean up safely. Once a 45 * disconnection has occurred, please create a new client. 46 */ 47 public class AvrcpBipClient { 48 private static final String TAG = "AvrcpBipClient"; 49 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 50 51 // AVRCP Controller BIP Image Initiator/Cover Art UUID - AVRCP 1.6 Section 5.14.2.1 52 private static final byte[] BLUETOOTH_UUID_AVRCP_COVER_ART = new byte[] { 53 (byte) 0x71, 54 (byte) 0x63, 55 (byte) 0xDD, 56 (byte) 0x54, 57 (byte) 0x4A, 58 (byte) 0x7E, 59 (byte) 0x11, 60 (byte) 0xE2, 61 (byte) 0xB4, 62 (byte) 0x7C, 63 (byte) 0x00, 64 (byte) 0x50, 65 (byte) 0xC2, 66 (byte) 0x49, 67 (byte) 0x00, 68 (byte) 0x48 69 }; 70 71 private static final int CONNECT = 0; 72 private static final int DISCONNECT = 1; 73 private static final int REQUEST = 2; 74 private static final int REFRESH_OBEX_SESSION = 3; 75 76 private final Handler mHandler; 77 private final HandlerThread mThread; 78 79 private final BluetoothDevice mDevice; 80 private final int mPsm; 81 private int mState = BluetoothProfile.STATE_DISCONNECTED; 82 83 private BluetoothSocket mSocket; 84 private BluetoothObexTransport mTransport; 85 private ClientSession mSession; 86 87 private final Callback mCallback; 88 89 /** 90 * Callback object used to be notified of when a request has been completed. 91 */ 92 interface Callback { 93 94 /** 95 * Notify of a connection state change in the client 96 * 97 * @param oldState The old state of the client 98 * @param newState The new state of the client 99 */ onConnectionStateChanged(int oldState, int newState)100 void onConnectionStateChanged(int oldState, int newState); 101 102 /** 103 * Notify of a get image properties completing 104 * 105 * @param status A status code to indicate a success or error 106 * @param properties The BipImageProperties object returned if successful, null otherwise 107 */ onGetImagePropertiesComplete(int status, String imageHandle, BipImageProperties properties)108 void onGetImagePropertiesComplete(int status, String imageHandle, 109 BipImageProperties properties); 110 111 /** 112 * Notify of a get image operation completing 113 * 114 * @param status A status code of the request. success or error 115 * @param image The BipImage object returned if successful, null otherwise 116 */ onGetImageComplete(int status, String imageHandle, BipImage image)117 void onGetImageComplete(int status, String imageHandle, BipImage image); 118 } 119 120 /** 121 * Creates a BIP image pull client and connects to a remote device's BIP image push server. 122 */ AvrcpBipClient(BluetoothDevice remoteDevice, int psm, Callback callback)123 public AvrcpBipClient(BluetoothDevice remoteDevice, int psm, Callback callback) { 124 if (remoteDevice == null) { 125 throw new NullPointerException("Remote device is null"); 126 } 127 if (callback == null) { 128 throw new NullPointerException("Callback is null"); 129 } 130 131 mDevice = remoteDevice; 132 mPsm = psm; 133 mCallback = callback; 134 135 mThread = new HandlerThread("AvrcpBipClient"); 136 mThread.start(); 137 138 Looper looper = mThread.getLooper(); 139 140 mHandler = new AvrcpBipClientHandler(looper, this); 141 mHandler.obtainMessage(CONNECT).sendToTarget(); 142 } 143 144 /** 145 * Refreshes this client's OBEX session 146 */ refreshSession()147 public void refreshSession() { 148 debug("Refresh client session"); 149 if (!isConnected()) { 150 error("Tried to do a reconnect operation on a client that is not connected"); 151 return; 152 } 153 try { 154 mHandler.obtainMessage(REFRESH_OBEX_SESSION).sendToTarget(); 155 } catch (IllegalStateException e) { 156 // Means we haven't been started or we're already stopped. Doing this makes this call 157 // always safe no matter the state. 158 return; 159 } 160 } 161 162 /** 163 * Safely disconnects the client from the server 164 */ shutdown()165 public void shutdown() { 166 debug("Shutdown client"); 167 try { 168 mHandler.obtainMessage(DISCONNECT).sendToTarget(); 169 } catch (IllegalStateException e) { 170 // Means we haven't been started or we're already stopped. Doing this makes this call 171 // always safe no matter the state. 172 return; 173 } 174 mThread.quitSafely(); 175 } 176 177 /** 178 * Determines if this client is connected to the server 179 * 180 * @return True if connected, False otherwise 181 */ getState()182 public synchronized int getState() { 183 return mState; 184 } 185 186 /** 187 * Determines if this client is connected to the server 188 * 189 * @return True if connected, False otherwise 190 */ isConnected()191 public boolean isConnected() { 192 return getState() == BluetoothProfile.STATE_CONNECTED; 193 } 194 195 /** 196 * Return the L2CAP PSM used to connect to the server. 197 * 198 * @return The L2CAP PSM 199 */ getL2capPsm()200 public int getL2capPsm() { 201 return mPsm; 202 } 203 204 /** 205 * Retrieve the image properties associated with the given imageHandle 206 */ getImageProperties(String imageHandle)207 public boolean getImageProperties(String imageHandle) { 208 RequestGetImageProperties request = new RequestGetImageProperties(imageHandle); 209 boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request)); 210 if (!status) { 211 error("Adding messages failed, connection state: " + isConnected()); 212 return false; 213 } 214 return true; 215 } 216 217 /** 218 * Download the image object associated with the given imageHandle 219 */ getImage(String imageHandle, BipImageDescriptor descriptor)220 public boolean getImage(String imageHandle, BipImageDescriptor descriptor) { 221 RequestGetImage request = new RequestGetImage(imageHandle, descriptor); 222 boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request)); 223 if (!status) { 224 error("Adding messages failed, connection state: " + isConnected()); 225 return false; 226 } 227 return true; 228 } 229 230 /** 231 * Update our client's connection state and notify of the new status 232 */ setConnectionState(int state)233 private void setConnectionState(int state) { 234 int oldState = -1; 235 synchronized (this) { 236 oldState = mState; 237 mState = state; 238 } 239 if (oldState != state) { 240 mCallback.onConnectionStateChanged(oldState, mState); 241 } 242 } 243 244 /** 245 * Connects to the remote device's BIP Image Pull server 246 */ connect()247 private synchronized void connect() { 248 debug("Connect using psm: " + mPsm); 249 if (isConnected()) { 250 warn("Already connected"); 251 return; 252 } 253 254 try { 255 setConnectionState(BluetoothProfile.STATE_CONNECTING); 256 257 mSocket = mDevice.createL2capSocket(mPsm); 258 mSocket.connect(); 259 260 mTransport = new BluetoothObexTransport(mSocket); 261 mSession = new ClientSession(mTransport); 262 263 HeaderSet headerSet = new HeaderSet(); 264 headerSet.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_AVRCP_COVER_ART); 265 266 headerSet = mSession.connect(headerSet); 267 int responseCode = headerSet.getResponseCode(); 268 if (responseCode == ResponseCodes.OBEX_HTTP_OK) { 269 setConnectionState(BluetoothProfile.STATE_CONNECTED); 270 debug("Connection established"); 271 } else { 272 error("Error connecting, code: " + responseCode); 273 disconnect(); 274 } 275 } catch (IOException e) { 276 error("Exception while connecting to AVRCP BIP server", e); 277 disconnect(); 278 } 279 } 280 281 /** 282 * Disconnect and reconnect the OBEX session. 283 */ refreshObexSession()284 private synchronized void refreshObexSession() { 285 if (mSession == null) return; 286 287 try { 288 setConnectionState(BluetoothProfile.STATE_DISCONNECTING); 289 mSession.disconnect(null); 290 debug("Disconnected from OBEX session"); 291 } catch (IOException e) { 292 error("Exception while disconnecting from AVRCP BIP server", e); 293 disconnect(); 294 return; 295 } 296 297 try { 298 setConnectionState(BluetoothProfile.STATE_CONNECTING); 299 300 HeaderSet headerSet = new HeaderSet(); 301 headerSet.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_AVRCP_COVER_ART); 302 303 headerSet = mSession.connect(headerSet); 304 int responseCode = headerSet.getResponseCode(); 305 if (responseCode == ResponseCodes.OBEX_HTTP_OK) { 306 setConnectionState(BluetoothProfile.STATE_CONNECTED); 307 debug("Reconnection established"); 308 } else { 309 error("Error reconnecting, code: " + responseCode); 310 disconnect(); 311 } 312 } catch (IOException e) { 313 error("Exception while reconnecting to AVRCP BIP server", e); 314 disconnect(); 315 } 316 } 317 318 /** 319 * Permanently disconnects this client from the remote device's BIP server and notifies of the 320 * new connection status. 321 * 322 */ disconnect()323 private synchronized void disconnect() { 324 if (mSession != null) { 325 setConnectionState(BluetoothProfile.STATE_DISCONNECTING); 326 327 try { 328 mSession.disconnect(null); 329 debug("Disconnected from OBEX session"); 330 } catch (IOException e) { 331 error("Exception while disconnecting from AVRCP BIP server: " + e.toString()); 332 } 333 334 try { 335 mSession.close(); 336 mTransport.close(); 337 mSocket.close(); 338 debug("Closed underlying session, transport and socket"); 339 } catch (IOException e) { 340 error("Exception while closing AVRCP BIP session: ", e); 341 } 342 343 mSession = null; 344 mTransport = null; 345 mSocket = null; 346 } 347 setConnectionState(BluetoothProfile.STATE_DISCONNECTED); 348 } 349 executeRequest(BipRequest request)350 private void executeRequest(BipRequest request) { 351 if (!isConnected()) { 352 error("Cannot execute request " + request.toString() 353 + ", we're not connected"); 354 notifyCaller(request); 355 return; 356 } 357 358 try { 359 request.execute(mSession); 360 notifyCaller(request); 361 debug("Completed request - " + request.toString()); 362 } catch (IOException e) { 363 error("Request failed: " + request.toString()); 364 notifyCaller(request); 365 disconnect(); 366 } 367 } 368 notifyCaller(BipRequest request)369 private void notifyCaller(BipRequest request) { 370 int type = request.getType(); 371 int responseCode = request.getResponseCode(); 372 String imageHandle = null; 373 374 debug("Notifying caller of request complete - " + request.toString()); 375 switch (type) { 376 case BipRequest.TYPE_GET_IMAGE_PROPERTIES: 377 imageHandle = ((RequestGetImageProperties) request).getImageHandle(); 378 BipImageProperties properties = 379 ((RequestGetImageProperties) request).getImageProperties(); 380 mCallback.onGetImagePropertiesComplete(responseCode, imageHandle, properties); 381 break; 382 case BipRequest.TYPE_GET_IMAGE: 383 imageHandle = ((RequestGetImage) request).getImageHandle(); 384 BipImage image = ((RequestGetImage) request).getImage(); 385 mCallback.onGetImageComplete(responseCode, imageHandle, image); 386 break; 387 } 388 } 389 390 /** 391 * Handles this AVRCP BIP Image Pull Client's requests 392 */ 393 private static class AvrcpBipClientHandler extends Handler { 394 WeakReference<AvrcpBipClient> mInst; 395 AvrcpBipClientHandler(Looper looper, AvrcpBipClient inst)396 AvrcpBipClientHandler(Looper looper, AvrcpBipClient inst) { 397 super(looper); 398 mInst = new WeakReference<>(inst); 399 } 400 401 @Override handleMessage(Message msg)402 public void handleMessage(Message msg) { 403 AvrcpBipClient inst = mInst.get(); 404 switch (msg.what) { 405 case CONNECT: 406 if (!inst.isConnected()) { 407 inst.connect(); 408 } 409 break; 410 411 case DISCONNECT: 412 if (inst.isConnected()) { 413 inst.disconnect(); 414 } 415 break; 416 417 case REFRESH_OBEX_SESSION: 418 if (inst.isConnected()) { 419 inst.refreshObexSession(); 420 } 421 break; 422 423 case REQUEST: 424 if (inst.isConnected()) { 425 inst.executeRequest((BipRequest) msg.obj); 426 } 427 break; 428 } 429 } 430 } 431 getStateName()432 private String getStateName() { 433 int state = getState(); 434 switch (state) { 435 case BluetoothProfile.STATE_DISCONNECTED: 436 return "Disconnected"; 437 case BluetoothProfile.STATE_CONNECTING: 438 return "Connecting"; 439 case BluetoothProfile.STATE_CONNECTED: 440 return "Connected"; 441 case BluetoothProfile.STATE_DISCONNECTING: 442 return "Disconnecting"; 443 } 444 return "Unknown"; 445 } 446 447 @Override toString()448 public String toString() { 449 return "<AvrcpBipClient" + " device=" + mDevice.getAddress() + " psm=" + mPsm 450 + " state=" + getStateName() + ">"; 451 } 452 453 /** 454 * Print to debug if debug is enabled for this class 455 */ debug(String msg)456 private void debug(String msg) { 457 if (DBG) { 458 Log.d(TAG, "[" + mDevice.getAddress() + "] " + msg); 459 } 460 } 461 462 /** 463 * Print to warn 464 */ warn(String msg)465 private void warn(String msg) { 466 Log.w(TAG, "[" + mDevice.getAddress() + "] " + msg); 467 } 468 469 /** 470 * Print to error 471 */ error(String msg)472 private void error(String msg) { 473 Log.e(TAG, "[" + mDevice.getAddress() + "] " + msg); 474 } 475 error(String msg, Throwable e)476 private void error(String msg, Throwable e) { 477 Log.e(TAG, "[" + mDevice.getAddress() + "] " + msg, e); 478 } 479 } 480