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

Commit 3a35f0cc authored by Sal Savage's avatar Sal Savage
Browse files

Add support for the AVRCP Controller Cover Art feature

Bug: b/132812696
Test: Build, flash, interop test with devices, atest. New unit tests to
come as the topic of a new patch.

Change-Id: I0895dbce1fa797f73929a9e26f46c1d48b3a367f
parent c712df08
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -344,6 +344,13 @@
                <action android:name="android.bluetooth.IBluetoothAvrcpController" />
            </intent-filter>
        </service>
        <provider android:process="@string/process"
            android:name=".avrcpcontroller.AvrcpCoverArtProvider"
            android:authorities="com.android.bluetooth.avrcpcontroller.AvrcpCoverArtProvider"
            android:enabled="@bool/avrcp_controller_enable_cover_art"
            android:grantUriPermissions="true"
            android:exported="true">
        </provider>
        <service
            android:process="@string/process"
            android:name = ".hid.HidHostService"
+46 −19
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ static jmethodID method_handleSetAddressedPlayerRsp;
static jmethodID method_handleAddressedPlayerChanged;
static jmethodID method_handleNowPlayingContentChanged;
static jmethodID method_onAvailablePlayerChanged;
static jmethodID method_getRcPsm;

static jclass class_AvrcpItem;
static jclass class_AvrcpPlayer;
@@ -723,15 +724,14 @@ static void btavrcp_now_playing_content_changed_callback(
static void btavrcp_available_player_changed_callback (
    const RawAddress& bd_addr) {
  ALOGI("%s", __func__);

  std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);

  CallbackEnv sCallbackEnv(__func__);
  if (!sCallbacksObj) {
      ALOGE("%s: sCallbacksObj is null", __func__);
      return;
  }
  if (!sCallbackEnv.valid()) return;

  ScopedLocalRef<jbyteArray> addr(
      sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
  if (!addr.get()) {
@@ -745,6 +745,30 @@ static void btavrcp_available_player_changed_callback (
      sCallbacksObj, method_onAvailablePlayerChanged, addr.get());
}

static void btavrcp_get_rcpsm_callback(const RawAddress& bd_addr,
                                       uint16_t psm) {
  ALOGE("%s -> psm received of %d", __func__, psm);
  std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
  CallbackEnv sCallbackEnv(__func__);
  if (!sCallbacksObj) {
    ALOGE("%s: sCallbacksObj is null", __func__);
    return;
  }
  if (!sCallbackEnv.valid()) return;

  ScopedLocalRef<jbyteArray> addr(
      sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
  if (!addr.get()) {
    ALOGE("%s: Failed to allocate a new byte array", __func__);
    return;
  }

  sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
                                   (jbyte*)&bd_addr.address);
  sCallbackEnv->CallVoidMethod(sCallbacksObj, method_getRcPsm, addr.get(),
                               (jint)psm);
}

static btrc_ctrl_callbacks_t sBluetoothAvrcpCallbacks = {
    sizeof(sBluetoothAvrcpCallbacks),
    btavrcp_passthrough_response_callback,
@@ -765,7 +789,8 @@ static btrc_ctrl_callbacks_t sBluetoothAvrcpCallbacks = {
    btavrcp_set_addressed_player_callback,
    btavrcp_addressed_player_changed_callback,
    btavrcp_now_playing_content_changed_callback,
    btavrcp_available_player_changed_callback};
    btavrcp_available_player_changed_callback,
    btavrcp_get_rcpsm_callback};

static void classInitNative(JNIEnv* env, jclass clazz) {
  method_handlePassthroughRsp =
@@ -779,6 +804,8 @@ static void classInitNative(JNIEnv* env, jclass clazz) {

  method_getRcFeatures = env->GetMethodID(clazz, "getRcFeatures", "([BI)V");

  method_getRcPsm = env->GetMethodID(clazz, "getRcPsm", "([BI)V");

  method_setplayerappsettingrsp =
      env->GetMethodID(clazz, "setPlayerAppSettingRsp", "([BB)V");

+3 −0
Original line number Diff line number Diff line
@@ -72,6 +72,9 @@
    <!-- If true, device requests audio focus and start avrcp updates on source start or play -->
    <bool name="a2dp_sink_automatically_request_audio_focus">false</bool>

    <!-- For enabling the AVRCP Controller Cover Artwork feature -->
    <bool name="avrcp_controller_enable_cover_art">false</bool>

    <!-- For enabling the hfp client connection service -->
    <bool name="hfp_client_connection_service_enabled">false</bool>

+403 −0
Original line number Diff line number Diff line
/*
 * 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 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();
    }

    /**
     * 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;
    }

    /**
     * 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);
            } else {
                error("Error connecting, code: " + responseCode);
                disconnect();
            }
            debug("Connection established");

        } catch (IOException e) {
            error("Exception while connecting 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);
            } catch (IOException e) {
                error("Exception while disconnecting from AVRCP BIP server: " + e.toString());
            }

            try {
                mSession.close();
            } catch (IOException e) {
                error("Exception while closing AVRCP BIP session: " + e.toString());
            }

            mSession = 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); // TODO: add handle
                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 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);
    }
}
+49 −3
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import android.support.v4.media.MediaBrowserCompat.MediaItem;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;

import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.ProfileService;

@@ -94,6 +95,28 @@ public class AvrcpControllerService extends ProfileService {
    protected Map<BluetoothDevice, AvrcpControllerStateMachine> mDeviceStateMap =
            new ConcurrentHashMap<>(1);

    private boolean mCoverArtEnabled;
    protected AvrcpCoverArtManager mCoverArtManager;

    private class ImageDownloadCallback implements AvrcpCoverArtManager.Callback {
        @Override
        public void onImageDownloadComplete(BluetoothDevice device,
                AvrcpCoverArtManager.DownloadEvent event) {
            if (DBG) {
                Log.d(TAG, "Image downloaded [device: " + device + ", handle: " + event.getHandle()
                        + ", uri: " + event.getUri());
            }
            AvrcpControllerStateMachine stateMachine = getStateMachine(device);
            if (stateMachine == null) {
                Log.e(TAG, "No state machine found for device " + device);
                mCoverArtManager.removeImage(device, event.getHandle());
                return;
            }
            stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_IMAGE_DOWNLOADED,
                    event);
        }
    }

    static {
        classInitNative();
    }
@@ -106,6 +129,10 @@ public class AvrcpControllerService extends ProfileService {
    @Override
    protected boolean start() {
        initNative();
        mCoverArtEnabled = getResources().getBoolean(R.bool.avrcp_controller_enable_cover_art);
        if (mCoverArtEnabled) {
            mCoverArtManager = new AvrcpCoverArtManager(this, new ImageDownloadCallback());
        }
        sBrowseTree = new BrowseTree(null);
        sService = this;

@@ -125,6 +152,8 @@ public class AvrcpControllerService extends ProfileService {

        sService = null;
        sBrowseTree = null;
        mCoverArtManager.cleanup();
        mCoverArtManager = null;
        return true;
    }

@@ -324,6 +353,17 @@ public class AvrcpControllerService extends ProfileService {
        /* Do Nothing. */
    }

    // Called by JNI to notify Avrcp of a remote device's Cover Art PSM
    private void getRcPsm(byte[] address, int psm) {
        BluetoothDevice device = mAdapter.getRemoteDevice(address);
        if (DBG) Log.d(TAG, "getRcPsm(device=" + device + ", psm=" + psm + ")");
        AvrcpControllerStateMachine stateMachine = getOrCreateStateMachine(device);
        if (stateMachine != null) {
            stateMachine.sendMessage(
                    AvrcpControllerStateMachine.MESSAGE_PROCESS_RECEIVED_COVER_ART_PSM, psm);
        }
    }

    // Called by JNI
    private void setPlayerAppSettingRsp(byte[] address, byte accepted) {
        /* Do Nothing. */
@@ -371,7 +411,6 @@ public class AvrcpControllerService extends ProfileService {
            aib.setItemType(AvrcpItem.TYPE_MEDIA);
            aib.setUuid(UUID.randomUUID().toString());
            AvrcpItem item = aib.build();

            stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_TRACK_CHANGED,
                    item);
        }
@@ -492,7 +531,6 @@ public class AvrcpControllerService extends ProfileService {
        }
    }


    void handleGetPlayerItemsRsp(byte[] address, AvrcpPlayer[] items) {
        if (DBG) {
            Log.d(TAG, "handleGetFolderItemsRsp called with " + items.length + " items.");
@@ -529,7 +567,6 @@ public class AvrcpControllerService extends ProfileService {
        aib.setUuid(UUID.randomUUID().toString());
        aib.setPlayable(true);
        AvrcpItem item = aib.build();

        return item;
    }

@@ -689,6 +726,10 @@ public class AvrcpControllerService extends ProfileService {
        return stateMachine;
    }

    protected AvrcpCoverArtManager getCoverArtManager() {
        return mCoverArtManager;
    }

    List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
        if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
        List<BluetoothDevice> deviceList = new ArrayList<>();
@@ -725,6 +766,11 @@ public class AvrcpControllerService extends ProfileService {
            stateMachine.dump(sb);
        }
        sb.append("\n  sBrowseTree: " + sBrowseTree.toString());

        sb.append("\n  Cover Artwork Enabled: " + (mCoverArtEnabled ? "True" : "False"));
        if (mCoverArtManager != null) {
            sb.append("\n  " + mCoverArtManager.toString());
        }
    }

    /*JNI*/
Loading