Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6c53cd47 authored by Sal Savage's avatar Sal Savage
Browse files

Disconnect and reconnect the OBEX session on change path

According to AVRCP 1.6.2, Section 5.14.2.2.2, the controller shall ensure
the OBEX session is disconnected and image handles are cleaned up any
time the UIDs change or, for a database unaware player (which we are),
we change paths successfully. This is further enforced with a PTS test
as well.

This change adds a "refresh" operation that will disconnect and
reconnect to OBEX without tearing down the actual L2CAP connection. This
operation is suggested by the specification in section 4.1.8. The
refresh operation is called on notification of a successful change path
operation. Since we don't support the UIDs changed event, so this should
satisfy PTS for now. In the event we do support UIDs changed we will have
to also call refreshCoverArt() there as well.

Not all Target devices support the recommendation section 4.1.8 of the
specification. Some treat an OBEX level disconnect as an indiation to
close the entire L2CAP connection. This was fixed in this change by
assuming that all disconnected states that occur when we had a client in
our set are incoming and thus unepected. We rightfully try to reconnect
at this point.

Tag: #compatibility
Bug: 149592384
Test: Build, flash, interop test, atest BluetoothInstrumentationTests
Merged-In: Icd4b2f1ac00bf32855f436be57212c8bc264a9f7
Change-Id: Icd4b2f1ac00bf32855f436be57212c8bc264a9f7
parent 14eda085
Loading
Loading
Loading
Loading
+80 −4
Original line number Diff line number Diff line
@@ -71,6 +71,7 @@ public class AvrcpBipClient {
    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;
@@ -140,6 +141,24 @@ public class AvrcpBipClient {
        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
     */
@@ -173,6 +192,15 @@ public class AvrcpBipClient {
        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
     */
@@ -239,18 +267,54 @@ public class AvrcpBipClient {
            int responseCode = headerSet.getResponseCode();
            if (responseCode == ResponseCodes.OBEX_HTTP_OK) {
                setConnectionState(BluetoothProfile.STATE_CONNECTED);
                debug("Connection established");
            } else {
                error("Error connecting, code: " + responseCode);
                disconnect();
            }
            debug("Connection established");

        } 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.
@@ -262,17 +326,23 @@ public class AvrcpBipClient {

            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.toString());
                error("Exception while closing AVRCP BIP session: ", e);
            }

            mSession = null;
            mTransport = null;
            mSocket = null;
        }
        setConnectionState(BluetoothProfile.STATE_DISCONNECTED);
    }
@@ -312,7 +382,7 @@ public class AvrcpBipClient {
            case BipRequest.TYPE_GET_IMAGE:
                imageHandle = ((RequestGetImage) request).getImageHandle();
                BipImage image = ((RequestGetImage) request).getImage();
                mCallback.onGetImageComplete(responseCode, imageHandle, image); // TODO: add handle
                mCallback.onGetImageComplete(responseCode, imageHandle, image);
                break;
        }
    }
@@ -344,6 +414,12 @@ public class AvrcpBipClient {
                    }
                    break;

                case REFRESH_OBEX_SESSION:
                    if (inst.isConnected()) {
                        inst.refreshObexSession();
                    }
                    break;

                case REQUEST:
                    if (inst.isConnected()) {
                        inst.executeRequest((BipRequest) msg.obj);
+14 −1
Original line number Diff line number Diff line
@@ -303,12 +303,21 @@ class AvrcpControllerStateMachine extends StateMachine {

    synchronized void connectCoverArt() {
        // Called from "connected" state, which assumes either control or browse is connected
        if (mCoverArtManager != null && mCoverArtPsm != 0) {
        if (mCoverArtManager != null && mCoverArtPsm != 0
                && mCoverArtManager.getState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
            logD("Attempting to connect to AVRCP BIP, psm: " + mCoverArtPsm);
            mCoverArtManager.connect(mDevice, /* psm */ mCoverArtPsm);
        }
    }

    synchronized void refreshCoverArt() {
        if (mCoverArtManager != null && mCoverArtPsm != 0
                && mCoverArtManager.getState(mDevice) == BluetoothProfile.STATE_CONNECTED) {
            logD("Attempting to refresh AVRCP BIP OBEX session, psm: " + mCoverArtPsm);
            mCoverArtManager.refreshSession(mDevice);
        }
    }

    synchronized void disconnectCoverArt() {
        // Safe to call even if we're not connected
        if (mCoverArtManager != null) {
@@ -724,6 +733,10 @@ class AvrcpControllerStateMachine extends StateMachine {
                    mBrowseTree.setCurrentBrowsedFolder(mNextStep.getID());
                    mBrowseTree.getCurrentBrowsedFolder().setExpectedChildren(msg.arg1);

                    // AVRCP Specification says, if we're not database aware, we must disconnect and
                    // reconnect our BIP client each time we successfully change path
                    refreshCoverArt();

                    if (mAbort) {
                        transitionTo(mConnected);
                    } else {
+32 −5
Original line number Diff line number Diff line
@@ -106,19 +106,36 @@ public class AvrcpCoverArtManager {
        return true;
    }

    /**
     * Refresh the OBEX session of a connected client
     *
     * @param device The remote Bluetooth device you wish to refresh
     * @return True if the refresh is successfully queued, False otherwise.
     */
    public synchronized boolean refreshSession(BluetoothDevice device) {
        debug("Refresh OBEX session for " + device.getAddress());
        AvrcpBipClient client = getClient(device);
        if (client == null) {
            warn("No client for " + device.getAddress());
            return false;
        }
        client.refreshSession();
        return true;
    }

    /**
     * Disconnect from a remote device's BIP Image Pull Server
     *
     * @param device The remote Bluetooth device you wish to connect to
     * @return True if the connection is successfully queued, False otherwise.
     * @param device The remote Bluetooth device you wish to disconnect from
     * @return True if the disconnection is successfully queued, False otherwise.
     */
    public synchronized boolean disconnect(BluetoothDevice device) {
        debug("Disconnect " + device.getAddress());
        if (!mClients.containsKey(device)) {
        AvrcpBipClient client = getClient(device);
        if (client == null) {
            warn("No client for " + device.getAddress());
            return false;
        }
        AvrcpBipClient client = getClient(device);
        client.shutdown();
        mClients.remove(device);
        mCoverArtStorage.removeImagesForDevice(device);
@@ -145,7 +162,7 @@ public class AvrcpCoverArtManager {
     * @return Connection status, based on BluetoothProfile.STATE_* constants
     */
    public int getState(BluetoothDevice device) {
        AvrcpBipClient client = mClients.get(device);
        AvrcpBipClient client = getClient(device);
        if (client == null) return BluetoothProfile.STATE_DISCONNECTED;
        return client.getState();
    }
@@ -262,11 +279,21 @@ public class AvrcpCoverArtManager {
        public void onConnectionStateChanged(int oldState, int newState) {
            debug(mDevice.getAddress() + ": " + oldState + " -> " + newState);
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                // The spec says handles are only good for the life an an OBEX connection. If we're
                // refreshing it, then we need to clear out our storage since its handle mapped.
                mCoverArtStorage.removeImagesForDevice(mDevice);

                // Once we're connected fetch the current metadata again in case the target has an
                // image handle they can now give us
                mService.getCurrentMetadataNative(Utils.getByteAddress(mDevice));
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                AvrcpBipClient client = getClient(mDevice);
                boolean shouldReconnect = (client != null);
                disconnect(mDevice);
                if (shouldReconnect) {
                    debug("Disconnect was not expected by us. Attempt to reconnect.");
                    connect(mDevice, client.getL2capPsm());
                }
            }
        }