blob: aa1d739037fc3df135654e793e70bb04f6c8f2b4 [file] [log] [blame]
/*
* 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<AvrcpBipClient> 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 "<AvrcpBipClient" + " device=" + mDevice.getAddress() + " psm=" + mPsm
+ " state=" + getStateName() + ">";
}
/**
* 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);
}
}