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

Commit 773a98c2 authored by Sal Savage's avatar Sal Savage Committed by Gerrit Code Review
Browse files

Merge changes from topic "bt-avrcp-controller-cover-art"

* changes:
  Avrcp controller cover art feature tests
  Add support for the AVRCP Controller Cover Art feature
  House all AVRCP metadata in custom objects instead of MediaMetadata
  Migrate the rest of AVRCP Controller to use MediaCompat
parents 581dddb7 93b9e082
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -345,6 +345,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"
+67 −38
Original line number Diff line number Diff line
@@ -49,8 +49,9 @@ static jmethodID method_handleSetAddressedPlayerRsp;
static jmethodID method_handleAddressedPlayerChanged;
static jmethodID method_handleNowPlayingContentChanged;
static jmethodID method_onAvailablePlayerChanged;
static jmethodID method_getRcPsm;

static jclass class_MediaBrowser_MediaItem;
static jclass class_AvrcpItem;
static jclass class_AvrcpPlayer;

static const btrc_ctrl_interface_t* sBluetoothAvrcpInterface = NULL;
@@ -458,7 +459,7 @@ static void btavrcp_get_folder_items_callback(
        sCallbackEnv->NewObjectArray((jint)count, class_AvrcpPlayer, 0));
  } else {
    itemArray.reset(sCallbackEnv->NewObjectArray(
        (jint)count, class_MediaBrowser_MediaItem, 0));
        (jint)count, class_AvrcpItem, 0));
  }
  if (!itemArray.get()) {
    ALOGE("%s itemArray allocation failed.", __func__);
@@ -511,11 +512,11 @@ static void btavrcp_get_folder_items_callback(
        ScopedLocalRef<jobject> mediaObj(
            sCallbackEnv.get(),
            (jobject)sCallbackEnv->CallObjectMethod(
                sCallbacksObj, method_createFromNativeMediaItem, uid,
                (jint)item->media.type, mediaName.get(), attrIdArray.get(),
                attrValArray.get()));
                sCallbacksObj, method_createFromNativeMediaItem, addr.get(),
                uid, (jint)item->media.type, mediaName.get(),
                attrIdArray.get(), attrValArray.get()));
        if (!mediaObj.get()) {
          ALOGE("%s failed to creae MediaItem for type ITEM_MEDIA", __func__);
          ALOGE("%s failed to create AvrcpItem for type ITEM_MEDIA", __func__);
          return;
        }
        sCallbackEnv->SetObjectArrayElement(itemArray.get(), i, mediaObj.get());
@@ -536,11 +537,11 @@ static void btavrcp_get_folder_items_callback(
        ScopedLocalRef<jobject> folderObj(
            sCallbackEnv.get(),
            (jobject)sCallbackEnv->CallObjectMethod(
                sCallbacksObj, method_createFromNativeFolderItem, uid,
                (jint)item->folder.type, folderName.get(),
                sCallbacksObj, method_createFromNativeFolderItem, addr.get(),
                uid, (jint)item->folder.type, folderName.get(),
                (jint)item->folder.playable));
        if (!folderObj.get()) {
          ALOGE("%s failed to create MediaItem for type ITEM_FOLDER", __func__);
          ALOGE("%s failed to create AvrcpItem for type ITEM_FOLDER", __func__);
          return;
        }
        sCallbackEnv->SetObjectArrayElement(itemArray.get(), i,
@@ -576,8 +577,8 @@ static void btavrcp_get_folder_items_callback(
        ScopedLocalRef<jobject> playerObj(
            sCallbackEnv.get(),
            (jobject)sCallbackEnv->CallObjectMethod(
                sCallbacksObj, method_createFromNativePlayerItem, id,
                playerName.get(), featureBitArray.get(), playStatus,
                sCallbacksObj, method_createFromNativePlayerItem, addr.get(),
                id, playerName.get(), featureBitArray.get(), playStatus,
                playerType));
        if (!playerObj.get()) {
          ALOGE("%s failed to create AvrcpPlayer from ITEM_PLAYER", __func__);
@@ -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");

@@ -805,21 +832,23 @@ static void classInitNative(JNIEnv* env, jclass clazz) {

  method_handleGetFolderItemsRsp =
      env->GetMethodID(clazz, "handleGetFolderItemsRsp",
                       "([BI[Landroid/media/browse/MediaBrowser$MediaItem;)V");
                       "([BI[Lcom/android/bluetooth/avrcpcontroller/"
                       "AvrcpItem;)V");
  method_handleGetPlayerItemsRsp = env->GetMethodID(
      clazz, "handleGetPlayerItemsRsp",
      "([B[Lcom/android/bluetooth/avrcpcontroller/AvrcpPlayer;)V");

  method_createFromNativeMediaItem =
      env->GetMethodID(clazz, "createFromNativeMediaItem",
                       "(JILjava/lang/String;[I[Ljava/lang/String;)Landroid/"
                       "media/browse/MediaBrowser$MediaItem;");
                       "([BJILjava/lang/String;[I[Ljava/lang/String;)Lcom/"
                       "android/bluetooth/avrcpcontroller/AvrcpItem;");
  method_createFromNativeFolderItem = env->GetMethodID(
      clazz, "createFromNativeFolderItem",
      "(JILjava/lang/String;I)Landroid/media/browse/MediaBrowser$MediaItem;");
      "([BJILjava/lang/String;I)Lcom/android/bluetooth/avrcpcontroller/"
      "AvrcpItem;");
  method_createFromNativePlayerItem =
      env->GetMethodID(clazz, "createFromNativePlayerItem",
                       "(ILjava/lang/String;[BII)Lcom/android/bluetooth/"
                       "([BILjava/lang/String;[BII)Lcom/android/bluetooth/"
                       "avrcpcontroller/AvrcpPlayer;");
  method_handleChangeFolderRsp =
      env->GetMethodID(clazz, "handleChangeFolderRsp", "([BI)V");
@@ -840,9 +869,9 @@ static void classInitNative(JNIEnv* env, jclass clazz) {
static void initNative(JNIEnv* env, jobject object) {
  std::unique_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);

  jclass tmpMediaItem =
      env->FindClass("android/media/browse/MediaBrowser$MediaItem");
  class_MediaBrowser_MediaItem = (jclass)env->NewGlobalRef(tmpMediaItem);
  jclass tmpAvrcpItem =
      env->FindClass("com/android/bluetooth/avrcpcontroller/AvrcpItem");
  class_AvrcpItem = (jclass)env->NewGlobalRef(tmpAvrcpItem);

  jclass tmpBtPlayer =
      env->FindClass("com/android/bluetooth/avrcpcontroller/AvrcpPlayer");
+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);
    }
}
+123 −85

File changed.

Preview size limit exceeded, changes collapsed.

Loading