/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth.avrcpcontroller; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothSocket; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.util.Log; import com.android.bluetooth.BluetoothObexTransport; import java.io.IOException; import java.lang.ref.WeakReference; import javax.obex.ClientSession; import javax.obex.HeaderSet; import javax.obex.ResponseCodes; /** * A client to a remote device's BIP Image Pull Server, as defined by a PSM passed in at * construction time. * * Once the client connection is established you can use this client to get image properties and * download images. The connection to the server is held open to service multiple requests. * * Client is good for one connection lifecycle. Please call shutdown() to clean up safely. Once a * disconnection has occurred, please create a new client. */ public class AvrcpBipClient { private static final String TAG = "AvrcpBipClient"; private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); // AVRCP Controller BIP Image Initiator/Cover Art UUID - AVRCP 1.6 Section 5.14.2.1 private static final byte[] BLUETOOTH_UUID_AVRCP_COVER_ART = new byte[] { (byte) 0x71, (byte) 0x63, (byte) 0xDD, (byte) 0x54, (byte) 0x4A, (byte) 0x7E, (byte) 0x11, (byte) 0xE2, (byte) 0xB4, (byte) 0x7C, (byte) 0x00, (byte) 0x50, (byte) 0xC2, (byte) 0x49, (byte) 0x00, (byte) 0x48 }; private static final int CONNECT = 0; private static final int DISCONNECT = 1; private static final int REQUEST = 2; private static final int REFRESH_OBEX_SESSION = 3; private final Handler mHandler; private final HandlerThread mThread; private final BluetoothDevice mDevice; private final int mPsm; private int mState = BluetoothProfile.STATE_DISCONNECTED; private BluetoothSocket mSocket; private BluetoothObexTransport mTransport; private ClientSession mSession; private final Callback mCallback; /** * Callback object used to be notified of when a request has been completed. */ interface Callback { /** * Notify of a connection state change in the client * * @param oldState The old state of the client * @param newState The new state of the client */ void onConnectionStateChanged(int oldState, int newState); /** * Notify of a get image properties completing * * @param status A status code to indicate a success or error * @param properties The BipImageProperties object returned if successful, null otherwise */ void onGetImagePropertiesComplete(int status, String imageHandle, BipImageProperties properties); /** * Notify of a get image operation completing * * @param status A status code of the request. success or error * @param image The BipImage object returned if successful, null otherwise */ void onGetImageComplete(int status, String imageHandle, BipImage image); } /** * Creates a BIP image pull client and connects to a remote device's BIP image push server. */ public AvrcpBipClient(BluetoothDevice remoteDevice, int psm, Callback callback) { if (remoteDevice == null) { throw new NullPointerException("Remote device is null"); } if (callback == null) { throw new NullPointerException("Callback is null"); } mDevice = remoteDevice; mPsm = psm; mCallback = callback; mThread = new HandlerThread("AvrcpBipClient"); mThread.start(); Looper looper = mThread.getLooper(); mHandler = new AvrcpBipClientHandler(looper, this); mHandler.obtainMessage(CONNECT).sendToTarget(); } /** * Refreshes this client's OBEX session */ public void refreshSession() { debug("Refresh client session"); if (!isConnected()) { error("Tried to do a reconnect operation on a client that is not connected"); return; } try { mHandler.obtainMessage(REFRESH_OBEX_SESSION).sendToTarget(); } catch (IllegalStateException e) { // Means we haven't been started or we're already stopped. Doing this makes this call // always safe no matter the state. return; } } /** * Safely disconnects the client from the server */ public void shutdown() { debug("Shutdown client"); try { mHandler.obtainMessage(DISCONNECT).sendToTarget(); } catch (IllegalStateException e) { // Means we haven't been started or we're already stopped. Doing this makes this call // always safe no matter the state. return; } mThread.quitSafely(); } /** * Determines if this client is connected to the server * * @return True if connected, False otherwise */ public synchronized int getState() { return mState; } /** * Determines if this client is connected to the server * * @return True if connected, False otherwise */ public boolean isConnected() { return getState() == BluetoothProfile.STATE_CONNECTED; } /** * Return the L2CAP PSM used to connect to the server. * * @return The L2CAP PSM */ public int getL2capPsm() { return mPsm; } /** * Retrieve the image properties associated with the given imageHandle */ public boolean getImageProperties(String imageHandle) { RequestGetImageProperties request = new RequestGetImageProperties(imageHandle); boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request)); if (!status) { error("Adding messages failed, connection state: " + isConnected()); return false; } return true; } /** * Download the image object associated with the given imageHandle */ public boolean getImage(String imageHandle, BipImageDescriptor descriptor) { RequestGetImage request = new RequestGetImage(imageHandle, descriptor); boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request)); if (!status) { error("Adding messages failed, connection state: " + isConnected()); return false; } return true; } /** * Update our client's connection state and notify of the new status */ private void setConnectionState(int state) { int oldState = -1; synchronized (this) { oldState = mState; mState = state; } if (oldState != state) { mCallback.onConnectionStateChanged(oldState, mState); } } /** * Connects to the remote device's BIP Image Pull server */ private synchronized void connect() { debug("Connect using psm: " + mPsm); if (isConnected()) { warn("Already connected"); return; } try { setConnectionState(BluetoothProfile.STATE_CONNECTING); mSocket = mDevice.createL2capSocket(mPsm); mSocket.connect(); mTransport = new BluetoothObexTransport(mSocket); mSession = new ClientSession(mTransport); HeaderSet headerSet = new HeaderSet(); headerSet.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_AVRCP_COVER_ART); headerSet = mSession.connect(headerSet); int responseCode = headerSet.getResponseCode(); if (responseCode == ResponseCodes.OBEX_HTTP_OK) { setConnectionState(BluetoothProfile.STATE_CONNECTED); debug("Connection established"); } else { error("Error connecting, code: " + responseCode); disconnect(); } } catch (IOException e) { error("Exception while connecting to AVRCP BIP server", e); disconnect(); } } /** * Disconnect and reconnect the OBEX session. */ private synchronized void refreshObexSession() { if (mSession == null) return; try { setConnectionState(BluetoothProfile.STATE_DISCONNECTING); mSession.disconnect(null); debug("Disconnected from OBEX session"); } catch (IOException e) { error("Exception while disconnecting from AVRCP BIP server", e); disconnect(); return; } try { setConnectionState(BluetoothProfile.STATE_CONNECTING); HeaderSet headerSet = new HeaderSet(); headerSet.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_AVRCP_COVER_ART); headerSet = mSession.connect(headerSet); int responseCode = headerSet.getResponseCode(); if (responseCode == ResponseCodes.OBEX_HTTP_OK) { setConnectionState(BluetoothProfile.STATE_CONNECTED); debug("Reconnection established"); } else { error("Error reconnecting, code: " + responseCode); disconnect(); } } catch (IOException e) { error("Exception while reconnecting to AVRCP BIP server", e); disconnect(); } } /** * Permanently disconnects this client from the remote device's BIP server and notifies of the * new connection status. * */ private synchronized void disconnect() { if (mSession != null) { setConnectionState(BluetoothProfile.STATE_DISCONNECTING); try { mSession.disconnect(null); debug("Disconnected from OBEX session"); } catch (IOException e) { error("Exception while disconnecting from AVRCP BIP server: " + e.toString()); } try { mSession.close(); mTransport.close(); mSocket.close(); debug("Closed underlying session, transport and socket"); } catch (IOException e) { error("Exception while closing AVRCP BIP session: ", e); } mSession = null; mTransport = null; mSocket = null; } setConnectionState(BluetoothProfile.STATE_DISCONNECTED); } private void executeRequest(BipRequest request) { if (!isConnected()) { error("Cannot execute request " + request.toString() + ", we're not connected"); notifyCaller(request); return; } try { request.execute(mSession); notifyCaller(request); debug("Completed request - " + request.toString()); } catch (IOException e) { error("Request failed: " + request.toString()); notifyCaller(request); disconnect(); } } private void notifyCaller(BipRequest request) { int type = request.getType(); int responseCode = request.getResponseCode(); String imageHandle = null; debug("Notifying caller of request complete - " + request.toString()); switch (type) { case BipRequest.TYPE_GET_IMAGE_PROPERTIES: imageHandle = ((RequestGetImageProperties) request).getImageHandle(); BipImageProperties properties = ((RequestGetImageProperties) request).getImageProperties(); mCallback.onGetImagePropertiesComplete(responseCode, imageHandle, properties); break; case BipRequest.TYPE_GET_IMAGE: imageHandle = ((RequestGetImage) request).getImageHandle(); BipImage image = ((RequestGetImage) request).getImage(); mCallback.onGetImageComplete(responseCode, imageHandle, image); break; } } /** * Handles this AVRCP BIP Image Pull Client's requests */ private static class AvrcpBipClientHandler extends Handler { WeakReference mInst; AvrcpBipClientHandler(Looper looper, AvrcpBipClient inst) { super(looper); mInst = new WeakReference<>(inst); } @Override public void handleMessage(Message msg) { AvrcpBipClient inst = mInst.get(); switch (msg.what) { case CONNECT: if (!inst.isConnected()) { inst.connect(); } break; case DISCONNECT: if (inst.isConnected()) { inst.disconnect(); } break; case REFRESH_OBEX_SESSION: if (inst.isConnected()) { inst.refreshObexSession(); } break; case REQUEST: if (inst.isConnected()) { inst.executeRequest((BipRequest) msg.obj); } break; } } } private String getStateName() { int state = getState(); switch (state) { case BluetoothProfile.STATE_DISCONNECTED: return "Disconnected"; case BluetoothProfile.STATE_CONNECTING: return "Connecting"; case BluetoothProfile.STATE_CONNECTED: return "Connected"; case BluetoothProfile.STATE_DISCONNECTING: return "Disconnecting"; } return "Unknown"; } @Override public String toString() { return ""; } /** * Print to debug if debug is enabled for this class */ private void debug(String msg) { if (DBG) { Log.d(TAG, "[" + mDevice.getAddress() + "] " + msg); } } /** * Print to warn */ private void warn(String msg) { Log.w(TAG, "[" + mDevice.getAddress() + "] " + msg); } /** * Print to error */ private void error(String msg) { Log.e(TAG, "[" + mDevice.getAddress() + "] " + msg); } private void error(String msg, Throwable e) { Log.e(TAG, "[" + mDevice.getAddress() + "] " + msg, e); } }