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

Commit 624d60cb authored by Sal Savage's avatar Sal Savage Committed by Gerrit Code Review
Browse files

Merge changes Ic2189057,Idb699c44,I71ac3edf,Iaa90224b

* changes:
  Increase coverage for A2dpSinkStreamHandler
  Add unit tests for A2dpSinkService
  Add unit tests for StackEvent
  Add an A2dpSinkNativeInterface abstraction to allow for mocking
parents b8b29039 9371f5b6
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -256,7 +256,7 @@ static JNINativeMethod sMethods[] = {

int register_com_android_bluetooth_a2dp_sink(JNIEnv* env) {
  return jniRegisterNativeMethods(
      env, "com/android/bluetooth/a2dpsink/A2dpSinkService", sMethods,
      env, "com/android/bluetooth/a2dpsink/A2dpSinkNativeInterface", sMethods,
      NELEM(sMethods));
}
}
+193 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.a2dpsink;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.util.Log;

import com.android.bluetooth.Utils;
import com.android.internal.annotations.GuardedBy;

/**
 * A2DP Sink Native Interface to/from JNI.
 */
public class A2dpSinkNativeInterface {
    private static final String TAG = "A2dpSinkNativeInterface";
    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
    private BluetoothAdapter mAdapter;

    @GuardedBy("INSTANCE_LOCK")
    private static A2dpSinkNativeInterface sInstance;
    private static final Object INSTANCE_LOCK = new Object();

    static {
        classInitNative();
    }

    private A2dpSinkNativeInterface() {
        mAdapter = BluetoothAdapter.getDefaultAdapter();
        if (mAdapter == null) {
            Log.wtfStack(TAG, "No Bluetooth Adapter Available");
        }
    }

    /**
     * Get singleton instance.
     */
    public static A2dpSinkNativeInterface getInstance() {
        synchronized (INSTANCE_LOCK) {
            if (sInstance == null) {
                sInstance = new A2dpSinkNativeInterface();
            }
            return sInstance;
        }
    }

    /**
     * Initializes the native interface and sets the max number of connected devices
     *
     * @param maxConnectedAudioDevices The maximum number of devices that can be connected at once
     */
    public void init(int maxConnectedAudioDevices) {
        initNative(maxConnectedAudioDevices);
    }

    /**
     * Cleanup the native interface.
     */
    public void cleanup() {
        cleanupNative();
    }

    /**
     * Initiates an A2DP connection to a remote device.
     *
     * @param device the remote device
     * @return true on success, otherwise false.
     */
    public boolean connectA2dpSink(BluetoothDevice device) {
        return connectA2dpNative(Utils.getByteAddress(device));
    }

    /**
     * Disconnects A2DP from a remote device.
     *
     * @param device the remote device
     * @return true on success, otherwise false.
     */
    public boolean disconnectA2dpSink(BluetoothDevice device) {
        return disconnectA2dpNative(Utils.getByteAddress(device));
    }

    /**
     * Set a BluetoothDevice as the active device
     *
     * The active device is the only one that will receive passthrough commands and the only one
     * that will have its audio decoded.
     *
     * Sending null for the active device will make no device active.
     *
     * @param device
     * @return True if the active device request has been scheduled
     */
    public boolean setActiveDevice(BluetoothDevice device) {
        // Translate to byte address for JNI. Use an all 0 MAC for no active device
        byte[] address = null;
        if (device != null) {
            address = Utils.getByteAddress(device);
        } else {
            address = Utils.getBytesFromAddress("00:00:00:00:00:00");
        }
        return setActiveDeviceNative(address);
    }

    /**
     * Inform A2DP decoder of the current audio focus
     *
     * @param focusGranted
     */
    public void informAudioFocusState(int focusGranted) {
        informAudioFocusStateNative(focusGranted);
    }

    /**
     * Inform A2DP decoder the desired audio gain
     *
     * @param gain
     */
    public void informAudioTrackGain(float gain) {
        informAudioTrackGainNative(gain);
    }

    /**
     * Send a stack event up to the A2DP Sink Service
     */
    private void sendMessageToService(StackEvent event) {
        A2dpSinkService service = A2dpSinkService.getA2dpSinkService();
        if (service != null) {
            service.messageFromNative(event);
        } else {
            Log.e(TAG, "Event ignored, service not available: " + event);
        }
    }

    /**
     * For the JNI to send messages about connection state changes
     */
    public void onConnectionStateChanged(byte[] address, int state) {
        StackEvent event =
                StackEvent.connectionStateChanged(mAdapter.getRemoteDevice(address), state);
        if (DBG) {
            Log.d(TAG, "onConnectionStateChanged: " + event);
        }
        sendMessageToService(event);
    }

    /**
     * For the JNI to send messages about audio stream state changes
     */
    public void onAudioStateChanged(byte[] address, int state) {
        StackEvent event = StackEvent.audioStateChanged(mAdapter.getRemoteDevice(address), state);
        if (DBG) {
            Log.d(TAG, "onAudioStateChanged: " + event);
        }
        sendMessageToService(event);
    }

    /**
     * For the JNI to send messages about audio configuration changes
     */
    public void onAudioConfigChanged(byte[] address, int sampleRate, int channelCount) {
        StackEvent event = StackEvent.audioConfigChanged(
                mAdapter.getRemoteDevice(address), sampleRate, channelCount);
        if (DBG) {
            Log.d(TAG, "onAudioConfigChanged: " + event);
        }
        sendMessageToService(event);
    }

    // Native methods that call into the JNI interface
    private static native void classInitNative();
    private native void initNative(int maxConnectedAudioDevices);
    private native void cleanupNative();
    private native boolean connectA2dpNative(byte[] address);
    private native boolean disconnectA2dpNative(byte[] address);
    private native boolean setActiveDeviceNative(byte[] address);
    private native void informAudioFocusStateNative(int focusGranted);
    private native void informAudioTrackGainNative(float gain);
}
+56 −64
Original line number Diff line number Diff line
@@ -49,7 +49,7 @@ public class A2dpSinkService extends ProfileService {

    private AdapterService mAdapterService;
    private DatabaseManager mDatabaseManager;
    protected Map<BluetoothDevice, A2dpSinkStateMachine> mDeviceStateMap =
    private Map<BluetoothDevice, A2dpSinkStateMachine> mDeviceStateMap =
            new ConcurrentHashMap<>(1);

    private final Object mStreamHandlerLock = new Object();
@@ -60,9 +60,7 @@ public class A2dpSinkService extends ProfileService {
    private A2dpSinkStreamHandler mA2dpSinkStreamHandler;
    private static A2dpSinkService sService;

    static {
        classInitNative();
    }
    A2dpSinkNativeInterface mNativeInterface;

    @Override
    protected boolean start() {
@@ -70,12 +68,15 @@ public class A2dpSinkService extends ProfileService {
                "AdapterService cannot be null when A2dpSinkService starts");
        mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(),
                "DatabaseManager cannot be null when A2dpSinkService starts");
        mNativeInterface = A2dpSinkNativeInterface.getInstance();

        mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices();
        mNativeInterface.init(mMaxConnectedAudioDevices);

        synchronized (mStreamHandlerLock) {
            mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, this);
            mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, mNativeInterface);
        }
        mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices();
        initNative(mMaxConnectedAudioDevices);

        setA2dpSinkService(this);
        return true;
    }
@@ -83,7 +84,7 @@ public class A2dpSinkService extends ProfileService {
    @Override
    protected boolean stop() {
        setA2dpSinkService(null);
        cleanupNative();
        mNativeInterface.cleanup();
        for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) {
            stateMachine.quitNow();
        }
@@ -117,16 +118,8 @@ public class A2dpSinkService extends ProfileService {
     * Set the device that should be allowed to actively stream
     */
    public boolean setActiveDevice(BluetoothDevice device) {
        // Translate to byte address for JNI. Use an all 0 MAC for no active device
        byte[] address = null;
        if (device != null) {
            address = Utils.getByteAddress(device);
        } else {
            address = Utils.getBytesFromAddress("00:00:00:00:00:00");
        }

        synchronized (mActiveDeviceLock) {
            if (setActiveDeviceNative(address)) {
            if (mNativeInterface.setActiveDevice(device)) {
                mActiveDevice = device;
                return true;
            }
@@ -340,6 +333,10 @@ public class A2dpSinkService extends ProfileService {
                    + ", InstanceMap start state: " + sb.toString());
        }

        if (device == null) {
            throw new IllegalArgumentException("Null device");
        }

        A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
        // a state machine instance doesn't exist. maybe it is already gone?
        if (stateMachine == null) {
@@ -365,7 +362,8 @@ public class A2dpSinkService extends ProfileService {
    }

    protected A2dpSinkStateMachine getOrCreateStateMachine(BluetoothDevice device) {
        A2dpSinkStateMachine newStateMachine = new A2dpSinkStateMachine(device, this);
        A2dpSinkStateMachine newStateMachine =
                new A2dpSinkStateMachine(device, this, mNativeInterface);
        A2dpSinkStateMachine existingStateMachine =
                mDeviceStateMap.putIfAbsent(device, newStateMachine);
        // Given null is not a valid value in our map, ConcurrentHashMap will return null if the
@@ -377,6 +375,11 @@ public class A2dpSinkService extends ProfileService {
        return existingStateMachine;
    }

    @VisibleForTesting
    protected A2dpSinkStateMachine getStateMachineForDevice(BluetoothDevice device) {
        return mDeviceStateMap.get(device);
    }

    List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
        if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
        List<BluetoothDevice> deviceList = new ArrayList<>();
@@ -406,6 +409,7 @@ public class A2dpSinkService extends ProfileService {
     * {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected
     */
    public int getConnectionState(BluetoothDevice device) {
        if (device == null) return BluetoothProfile.STATE_DISCONNECTED;
        A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
        return (stateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED
                : stateMachine.getState();
@@ -475,6 +479,7 @@ public class A2dpSinkService extends ProfileService {
    }

    BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
        if (device == null) return null;
        A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
        // a state machine instance doesn't exist. maybe it is already gone?
        if (stateMachine == null) {
@@ -483,53 +488,37 @@ public class A2dpSinkService extends ProfileService {
        return stateMachine.getAudioConfig();
    }

    /* JNI interfaces*/

    private static native void classInitNative();

    private native void initNative(int maxConnectedAudioDevices);

    private native void cleanupNative();

    native boolean connectA2dpNative(byte[] address);

    native boolean disconnectA2dpNative(byte[] address);

    /**
     * set A2DP state machine as the active device
     * the active device is the only one that will receive passthrough commands and the only one
     * that will have its audio decoded
     *
     * @hide
     * @param address
     * @return active device request has been scheduled
     */
    public native boolean setActiveDeviceNative(byte[] address);

    /**
     * inform A2DP decoder of the current audio focus
     *
     * @param focusGranted
     */
    @VisibleForTesting
    public native void informAudioFocusStateNative(int focusGranted);

    /**
     * inform A2DP decoder the desired audio gain
     *
     * @param gain
     * Receive and route a stack event from the JNI
     */
    @VisibleForTesting
    public native void informAudioTrackGainNative(float gain);
    protected void messageFromNative(StackEvent event) {
        switch (event.mType) {
            case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
                onConnectionStateChanged(event);
                return;
            case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
                onAudioStateChanged(event);
                return;
            case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED:
                onAudioConfigChanged(event);
                return;
            default:
                Log.e(TAG, "Received unknown stack event of type " + event.mType);
                return;
        }
    }

    private void onConnectionStateChanged(byte[] address, int state) {
        StackEvent event = StackEvent.connectionStateChanged(
                BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address), state);
        A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice);
    private void onConnectionStateChanged(StackEvent event) {
        BluetoothDevice device = event.mDevice;
        if (device == null) {
            return;
        }
        A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device);
        stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event);
    }

    private void onAudioStateChanged(byte[] address, int state) {
    private void onAudioStateChanged(StackEvent event) {
        int state = event.mState;
        synchronized (mStreamHandlerLock) {
            if (mA2dpSinkStreamHandler == null) {
                Log.e(TAG, "Received audio state change before we've been started");
@@ -541,15 +530,18 @@ public class A2dpSinkService extends ProfileService {
                    || state == StackEvent.AUDIO_STATE_REMOTE_SUSPEND) {
                mA2dpSinkStreamHandler.obtainMessage(
                        A2dpSinkStreamHandler.SRC_STR_STOP).sendToTarget();
            } else {
                Log.w(TAG, "Unhandled audio state change, state=" + state);
            }
        }
    }

    private void onAudioConfigChanged(byte[] address, int sampleRate, int channelCount) {
        StackEvent event = StackEvent.audioConfigChanged(
                BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address), sampleRate,
                channelCount);
        A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice);
    private void onAudioConfigChanged(StackEvent event) {
        BluetoothDevice device = event.mDevice;
        if (device == null) {
            return;
        }
        A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device);
        stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event);
    }
}
+7 −4
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ public class A2dpSinkStateMachine extends StateMachine {
    protected final BluetoothDevice mDevice;
    protected final byte[] mDeviceAddress;
    protected final A2dpSinkService mService;
    protected final A2dpSinkNativeInterface mNativeInterface;
    protected final Disconnected mDisconnected;
    protected final Connecting mConnecting;
    protected final Connected mConnected;
@@ -63,11 +64,13 @@ public class A2dpSinkStateMachine extends StateMachine {
    protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
    protected BluetoothAudioConfig mAudioConfig = null;

    A2dpSinkStateMachine(BluetoothDevice device, A2dpSinkService service) {
    A2dpSinkStateMachine(BluetoothDevice device, A2dpSinkService service,
            A2dpSinkNativeInterface nativeInterface) {
        super(TAG);
        mDevice = device;
        mDeviceAddress = Utils.getByteAddress(mDevice);
        mService = service;
        mNativeInterface = nativeInterface;
        if (DBG) Log.d(TAG, device.toString());

        mDisconnected = new Disconnected();
@@ -177,7 +180,7 @@ public class A2dpSinkStateMachine extends StateMachine {
                                    == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
                                Log.w(TAG, "Ignore incoming connection, profile is"
                                        + " turned off for " + mDevice);
                                mService.disconnectA2dpNative(mDeviceAddress);
                                mNativeInterface.disconnectA2dpSink(mDevice);
                            } else {
                                mConnecting.mIncomingConnection = true;
                                transitionTo(mConnecting);
@@ -204,7 +207,7 @@ public class A2dpSinkStateMachine extends StateMachine {
            sendMessageDelayed(CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS);

            if (!mIncomingConnection) {
                mService.connectA2dpNative(mDeviceAddress);
                mNativeInterface.connectA2dpSink(mDevice);
            }

            super.enter();
@@ -256,7 +259,7 @@ public class A2dpSinkStateMachine extends StateMachine {
            switch (message.what) {
                case DISCONNECT:
                    transitionTo(mDisconnecting);
                    mService.disconnectA2dpNative(mDeviceAddress);
                    mNativeInterface.disconnectA2dpSink(mDevice);
                    return true;
                case STACK_EVENT:
                    processStackEvent((StackEvent) message.obj);
+16 −18
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ package com.android.bluetooth.a2dpsink;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClientCall;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
@@ -81,7 +80,7 @@ public class A2dpSinkStreamHandler extends Handler {

    // Private variables.
    private A2dpSinkService mA2dpSinkService;
    private Context mContext;
    private A2dpSinkNativeInterface mNativeInterface;
    private AudioManager mAudioManager;
    // Keep track if the remote device is providing audio
    private boolean mStreamAvailable = false;
@@ -111,10 +110,11 @@ public class A2dpSinkStreamHandler extends Handler {
        }
    };

    public A2dpSinkStreamHandler(A2dpSinkService a2dpSinkService, Context context) {
    public A2dpSinkStreamHandler(A2dpSinkService a2dpSinkService,
            A2dpSinkNativeInterface nativeInterface) {
        mA2dpSinkService = a2dpSinkService;
        mContext = context;
        mAudioManager = context.getSystemService(AudioManager.class);
        mNativeInterface = nativeInterface;
        mAudioManager = mA2dpSinkService.getSystemService(AudioManager.class);
    }

    /**
@@ -206,7 +206,7 @@ public class A2dpSinkStreamHandler extends Handler {

                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                        // Make the volume duck.
                        int duckPercent = mContext.getResources()
                        int duckPercent = mA2dpSinkService.getResources()
                                .getInteger(R.integer.a2dp_sink_duck_percent);
                        if (duckPercent < 0 || duckPercent > 100) {
                            Log.e(TAG, "Invalid duck percent using default.");
@@ -300,7 +300,7 @@ public class A2dpSinkStreamHandler extends Handler {
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .build();

            mMediaPlayer = MediaPlayer.create(mContext, R.raw.silent, attrs,
            mMediaPlayer = MediaPlayer.create(mA2dpSinkService, R.raw.silent, attrs,
                    mAudioManager.generateAudioSessionId());
            if (mMediaPlayer == null) {
                Log.e(TAG, "Failed to initialize media player. You may not get media key events");
@@ -342,18 +342,18 @@ public class A2dpSinkStreamHandler extends Handler {
    }

    private void startFluorideStreaming() {
        mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_GRANTED);
        mA2dpSinkService.informAudioTrackGainNative(1.0f);
        mNativeInterface.informAudioFocusState(STATE_FOCUS_GRANTED);
        mNativeInterface.informAudioTrackGain(1.0f);
        requestMediaKeyFocus();
    }

    private void stopFluorideStreaming() {
        releaseMediaKeyFocus();
        mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_LOST);
        mNativeInterface.informAudioFocusState(STATE_FOCUS_LOST);
    }

    private void setFluorideAudioTrackGain(float gain) {
        mA2dpSinkService.informAudioTrackGainNative(gain);
        mNativeInterface.informAudioTrackGain(gain);
    }

    private void sendAvrcpPause() {
@@ -380,20 +380,18 @@ public class A2dpSinkStreamHandler extends Handler {
        return false;
    }

    synchronized int getAudioFocus() {
        return mAudioFocus;
    }

    private boolean isIotDevice() {
        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_EMBEDDED);
        return mA2dpSinkService.getPackageManager().hasSystemFeature(
                PackageManager.FEATURE_EMBEDDED);
    }

    private boolean isTvDevice() {
        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
        return mA2dpSinkService.getPackageManager().hasSystemFeature(
                PackageManager.FEATURE_LEANBACK);
    }

    private boolean shouldRequestFocus() {
        return mContext.getResources()
        return mA2dpSinkService.getResources()
                .getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus);
    }

Loading