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 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.obex.ClientSession; 31 import com.android.obex.HeaderSet; 32 import com.android.obex.ResponseCodes; 33 34 import java.io.IOException; 35 import java.lang.ref.WeakReference; 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 * <p>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 * <p>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.class.getSimpleName(); 49 50 // AVRCP Controller BIP Image Initiator/Cover Art UUID - AVRCP 1.6 Section 5.14.2.1 51 private static final byte[] BLUETOOTH_UUID_AVRCP_COVER_ART = 52 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 /** Callback object used to be notified of when a request has been completed. */ 90 interface Callback { 91 92 /** 93 * Notify of a connection state change in the client 94 * 95 * @param oldState The old state of the client 96 * @param newState The new state of the client 97 */ onConnectionStateChanged(int oldState, int newState)98 void onConnectionStateChanged(int oldState, int newState); 99 100 /** 101 * Notify of a get image properties completing 102 * 103 * @param status A status code to indicate a success or error 104 * @param properties The BipImageProperties object returned if successful, null otherwise 105 */ onGetImagePropertiesComplete( int status, String imageHandle, BipImageProperties properties)106 void onGetImagePropertiesComplete( 107 int status, String imageHandle, BipImageProperties properties); 108 109 /** 110 * Notify of a get image operation completing 111 * 112 * @param status A status code of the request. success or error 113 * @param image The BipImage object returned if successful, null otherwise 114 */ onGetImageComplete(int status, String imageHandle, BipImage image)115 void onGetImageComplete(int status, String imageHandle, BipImage image); 116 } 117 118 /** 119 * Creates a BIP image pull client 120 * 121 * <p>{@link connectAsync()} must be called separately. 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 } 142 143 /** Refreshes this client's OBEX session */ refreshSession()144 public void refreshSession() { 145 debug("Refresh client session"); 146 if (!isConnected()) { 147 error("Tried to do a reconnect operation on a client that is not connected"); 148 return; 149 } 150 try { 151 mHandler.obtainMessage(REFRESH_OBEX_SESSION).sendToTarget(); 152 } catch (IllegalStateException e) { 153 // Means we haven't been started or we're already stopped. Doing this makes this call 154 // always safe no matter the state. 155 return; 156 } 157 } 158 159 /** Safely disconnects the client from the server */ shutdown()160 public void shutdown() { 161 debug("Shutdown client"); 162 try { 163 disconnectAsync(); 164 } catch (IllegalStateException e) { 165 // Means we haven't been started or we're already stopped. Doing this makes this call 166 // always safe no matter the state. 167 return; 168 } 169 mThread.quitSafely(); 170 } 171 172 /** 173 * Determines if this client is connected to the server 174 * 175 * @return True if connected, False otherwise 176 */ getState()177 public synchronized int getState() { 178 return mState; 179 } 180 181 /** 182 * Determines if this client is connected to the server 183 * 184 * @return True if connected, False otherwise 185 */ isConnected()186 public boolean isConnected() { 187 return getState() == BluetoothProfile.STATE_CONNECTED; 188 } 189 190 /** 191 * Return the L2CAP PSM used to connect to the server. 192 * 193 * @return The L2CAP PSM 194 */ getL2capPsm()195 public int getL2capPsm() { 196 return mPsm; 197 } 198 199 /** Retrieve the image properties associated with the given imageHandle */ getImageProperties(String imageHandle)200 public boolean getImageProperties(String imageHandle) { 201 RequestGetImageProperties request = new RequestGetImageProperties(imageHandle); 202 boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request)); 203 if (!status) { 204 error("Adding messages failed, connection state: " + isConnected()); 205 return false; 206 } 207 return true; 208 } 209 210 /** Download the image object associated with the given imageHandle */ getImage(String imageHandle, BipImageDescriptor descriptor)211 public boolean getImage(String imageHandle, BipImageDescriptor descriptor) { 212 RequestGetImage request = new RequestGetImage(imageHandle, descriptor); 213 boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request)); 214 if (!status) { 215 error("Adding messages failed, connection state: " + isConnected()); 216 return false; 217 } 218 return true; 219 } 220 221 /** Update our client's connection state and notify of the new status */ 222 @VisibleForTesting setConnectionState(int state)223 void setConnectionState(int state) { 224 int oldState = -1; 225 synchronized (this) { 226 oldState = mState; 227 mState = state; 228 } 229 if (oldState != state) { 230 mCallback.onConnectionStateChanged(oldState, mState); 231 } 232 } 233 234 /** Connects asynchronously */ connectAsync()235 void connectAsync() { 236 mHandler.obtainMessage(CONNECT).sendToTarget(); 237 } 238 239 /** Connects to the remote device's BIP Image Pull server */ connect()240 private synchronized void connect() { 241 debug("Connect using psm: " + mPsm); 242 if (isConnected()) { 243 warn("Already connected"); 244 return; 245 } 246 247 try { 248 setConnectionState(BluetoothProfile.STATE_CONNECTING); 249 250 mSocket = mDevice.createL2capSocket(mPsm); 251 mSocket.connect(); 252 253 mTransport = new BluetoothObexTransport(mSocket); 254 mSession = new ClientSession(mTransport); 255 256 HeaderSet headerSet = new HeaderSet(); 257 headerSet.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_AVRCP_COVER_ART); 258 259 headerSet = mSession.connect(headerSet); 260 int responseCode = headerSet.getResponseCode(); 261 if (responseCode == ResponseCodes.OBEX_HTTP_OK) { 262 setConnectionState(BluetoothProfile.STATE_CONNECTED); 263 debug("Connection established"); 264 } else { 265 error("Error connecting, code: " + responseCode); 266 disconnect(); 267 } 268 } catch (IOException e) { 269 error("Exception while connecting to AVRCP BIP server", e); 270 disconnect(); 271 } 272 } 273 274 /** Disconnect and reconnect the OBEX session. */ refreshObexSession()275 private synchronized void refreshObexSession() { 276 if (mSession == null) return; 277 278 try { 279 setConnectionState(BluetoothProfile.STATE_DISCONNECTING); 280 mSession.disconnect(null); 281 debug("Disconnected from OBEX session"); 282 } catch (IOException e) { 283 error("Exception while disconnecting from AVRCP BIP server", e); 284 disconnect(); 285 return; 286 } 287 288 try { 289 setConnectionState(BluetoothProfile.STATE_CONNECTING); 290 291 HeaderSet headerSet = new HeaderSet(); 292 headerSet.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_AVRCP_COVER_ART); 293 294 headerSet = mSession.connect(headerSet); 295 int responseCode = headerSet.getResponseCode(); 296 if (responseCode == ResponseCodes.OBEX_HTTP_OK) { 297 setConnectionState(BluetoothProfile.STATE_CONNECTED); 298 debug("Reconnection established"); 299 } else { 300 error("Error reconnecting, code: " + responseCode); 301 disconnect(); 302 } 303 } catch (IOException e) { 304 error("Exception while reconnecting to AVRCP BIP server", e); 305 disconnect(); 306 } 307 } 308 309 /** Disconnects asynchronously */ disconnectAsync()310 void disconnectAsync() { 311 mHandler.obtainMessage(DISCONNECT).sendToTarget(); 312 } 313 314 /** 315 * Permanently disconnects this client from the remote device's BIP server and notifies of the 316 * new connection status. 317 */ disconnect()318 private synchronized void disconnect() { 319 if (mSession != null) { 320 setConnectionState(BluetoothProfile.STATE_DISCONNECTING); 321 322 try { 323 mSession.disconnect(null); 324 debug("Disconnected from OBEX session"); 325 } catch (IOException e) { 326 error("Exception while disconnecting from AVRCP BIP server: " + e.toString()); 327 } 328 329 try { 330 mSession.close(); 331 mTransport.close(); 332 mSocket.close(); 333 debug("Closed underlying session, transport and socket"); 334 } catch (IOException e) { 335 error("Exception while closing AVRCP BIP session: ", e); 336 } 337 338 mSession = null; 339 mTransport = null; 340 mSocket = null; 341 } 342 setConnectionState(BluetoothProfile.STATE_DISCONNECTED); 343 } 344 executeRequest(BipRequest request)345 private void executeRequest(BipRequest request) { 346 if (!isConnected()) { 347 error("Cannot execute request " + request.toString() + ", we're not connected"); 348 notifyCaller(request); 349 return; 350 } 351 352 try { 353 request.execute(mSession); 354 notifyCaller(request); 355 debug("Completed request - " + request.toString()); 356 } catch (IOException e) { 357 error("Request failed: " + request.toString()); 358 notifyCaller(request); 359 disconnect(); 360 } 361 } 362 notifyCaller(BipRequest request)363 private void notifyCaller(BipRequest request) { 364 int type = request.getType(); 365 int responseCode = request.getResponseCode(); 366 String imageHandle = null; 367 368 debug("Notifying caller of request complete - " + request.toString()); 369 switch (type) { 370 case BipRequest.TYPE_GET_IMAGE_PROPERTIES: 371 imageHandle = ((RequestGetImageProperties) request).getImageHandle(); 372 BipImageProperties properties = 373 ((RequestGetImageProperties) request).getImageProperties(); 374 mCallback.onGetImagePropertiesComplete(responseCode, imageHandle, properties); 375 break; 376 case BipRequest.TYPE_GET_IMAGE: 377 imageHandle = ((RequestGetImage) request).getImageHandle(); 378 BipImage image = ((RequestGetImage) request).getImage(); 379 mCallback.onGetImageComplete(responseCode, imageHandle, image); 380 break; 381 } 382 } 383 384 /** Handles this AVRCP BIP Image Pull Client's requests */ 385 private static class AvrcpBipClientHandler extends Handler { 386 WeakReference<AvrcpBipClient> mInst; 387 AvrcpBipClientHandler(Looper looper, AvrcpBipClient inst)388 AvrcpBipClientHandler(Looper looper, AvrcpBipClient inst) { 389 super(looper); 390 mInst = new WeakReference<>(inst); 391 } 392 393 @Override handleMessage(Message msg)394 public void handleMessage(Message msg) { 395 AvrcpBipClient inst = mInst.get(); 396 switch (msg.what) { 397 case CONNECT: 398 if (!inst.isConnected()) { 399 inst.connect(); 400 } 401 break; 402 403 case DISCONNECT: 404 if (inst.isConnected()) { 405 inst.disconnect(); 406 } 407 break; 408 409 case REFRESH_OBEX_SESSION: 410 if (inst.isConnected()) { 411 inst.refreshObexSession(); 412 } 413 break; 414 415 case REQUEST: 416 if (inst.isConnected()) { 417 inst.executeRequest((BipRequest) msg.obj); 418 } 419 break; 420 } 421 } 422 } 423 424 @VisibleForTesting getStateName()425 String getStateName() { 426 int state = getState(); 427 switch (state) { 428 case BluetoothProfile.STATE_DISCONNECTED: 429 return "Disconnected"; 430 case BluetoothProfile.STATE_CONNECTING: 431 return "Connecting"; 432 case BluetoothProfile.STATE_CONNECTED: 433 return "Connected"; 434 case BluetoothProfile.STATE_DISCONNECTING: 435 return "Disconnecting"; 436 } 437 return "Unknown"; 438 } 439 440 @Override toString()441 public String toString() { 442 return "<AvrcpBipClient" 443 + " device=" 444 + mDevice 445 + " psm=" 446 + mPsm 447 + " state=" 448 + getStateName() 449 + ">"; 450 } 451 452 /** Print to debug if debug is enabled for this class */ debug(String msg)453 private void debug(String msg) { 454 Log.d(TAG, "[" + mDevice + "] " + msg); 455 } 456 457 /** Print to warn */ warn(String msg)458 private void warn(String msg) { 459 Log.w(TAG, "[" + mDevice + "] " + msg); 460 } 461 462 /** Print to error */ error(String msg)463 private void error(String msg) { 464 Log.e(TAG, "[" + mDevice + "] " + msg); 465 } 466 error(String msg, Throwable e)467 private void error(String msg, Throwable e) { 468 Log.e(TAG, "[" + mDevice + "] " + msg, e); 469 } 470 } 471