Loading android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java +40 −83 Original line number Diff line number Diff line Loading @@ -27,8 +27,6 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.IBluetoothA2dpSink; import android.content.AttributionSource; import android.content.Context; import android.media.AudioManager; import android.os.Looper; import android.sysprop.BluetoothProperties; import android.util.Log; Loading @@ -51,62 +49,43 @@ import java.util.concurrent.ConcurrentHashMap; public class A2dpSinkService extends ProfileService { private static final String TAG = A2dpSinkService.class.getSimpleName(); private static A2dpSinkService sService; // This is also used as a lock for shared data in {@link A2dpSinkService} @GuardedBy("mDeviceStateMap") private final Map<BluetoothDevice, A2dpSinkStateMachine> mDeviceStateMap = new ConcurrentHashMap<>(1); private final Object mActiveDeviceLock = new Object(); private final Object mStreamHandlerLock = new Object(); private final AdapterService mAdapterService; private final DatabaseManager mDatabaseManager; private final A2dpSinkNativeInterface mNativeInterface; private final Looper mLooper; private final int mMaxConnectedAudioDevices; private final Object mActiveDeviceLock = new Object(); @GuardedBy("mStreamHandlerLock") private final A2dpSinkStreamHandler mA2dpSinkStreamHandler; @GuardedBy("mActiveDeviceLock") private BluetoothDevice mActiveDevice = null; private final Object mStreamHandlerLock = new Object(); @GuardedBy("mStreamHandlerLock") private A2dpSinkStreamHandler mA2dpSinkStreamHandler; private static A2dpSinkService sService; private int mMaxConnectedAudioDevices; private AdapterService mAdapterService; private DatabaseManager mDatabaseManager; public A2dpSinkService(Context ctx) { super(ctx); mNativeInterface = requireNonNull(A2dpSinkNativeInterface.getInstance()); mLooper = Looper.getMainLooper(); public A2dpSinkService(AdapterService adapterService) { this(adapterService, A2dpSinkNativeInterface.getInstance(), Looper.getMainLooper()); } @VisibleForTesting A2dpSinkService(Context ctx, A2dpSinkNativeInterface nativeInterface, Looper looper) { super(ctx); A2dpSinkService( AdapterService adapterService, A2dpSinkNativeInterface nativeInterface, Looper looper) { super(requireNonNull(adapterService)); mAdapterService = adapterService; mDatabaseManager = requireNonNull(mAdapterService.getDatabase()); mNativeInterface = requireNonNull(nativeInterface); mLooper = looper; } public static boolean isEnabled() { return BluetoothProperties.isProfileA2dpSinkEnabled().orElse(false); } @Override public void start() { mAdapterService = requireNonNull( AdapterService.getAdapterService(), "AdapterService cannot be null when A2dpSinkService starts"); mDatabaseManager = requireNonNull( AdapterService.getAdapterService().getDatabase(), "DatabaseManager cannot be null when A2dpSinkService starts"); mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices(); mNativeInterface.init(mMaxConnectedAudioDevices); synchronized (mStreamHandlerLock) { mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, mNativeInterface); } Loading @@ -114,6 +93,10 @@ public class A2dpSinkService extends ProfileService { setA2dpSinkService(this); } public static boolean isEnabled() { return BluetoothProperties.isProfileA2dpSinkEnabled().orElse(false); } @Override public void stop() { setA2dpSinkService(null); Loading @@ -125,10 +108,7 @@ public class A2dpSinkService extends ProfileService { mDeviceStateMap.clear(); } synchronized (mStreamHandlerLock) { if (mA2dpSinkStreamHandler != null) { mA2dpSinkStreamHandler.cleanup(); mA2dpSinkStreamHandler = null; } } } Loading Loading @@ -164,7 +144,6 @@ public class A2dpSinkService extends ProfileService { /** Request audio focus such that the designated device can stream audio */ public void requestAudioFocus(BluetoothDevice device, boolean request) { synchronized (mStreamHandlerLock) { if (mA2dpSinkStreamHandler == null) return; mA2dpSinkStreamHandler.requestAudioFocus(request); } } Loading @@ -176,17 +155,12 @@ public class A2dpSinkService extends ProfileService { */ public int getFocusState() { synchronized (mStreamHandlerLock) { if (mA2dpSinkStreamHandler == null) return AudioManager.ERROR; return mA2dpSinkStreamHandler.getFocusState(); } } @RequiresPermission(BLUETOOTH_PRIVILEGED) boolean isA2dpPlaying(BluetoothDevice device) { enforceCallingOrSelfPermission( BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); synchronized (mStreamHandlerLock) { if (mA2dpSinkStreamHandler == null) return false; return mA2dpSinkStreamHandler.isPlaying(); } } Loading Loading @@ -309,6 +283,9 @@ public class A2dpSinkService extends ProfileService { if (service == null) { return false; } service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); return service.isA2dpPlaying(device); } Loading Loading @@ -407,19 +384,14 @@ public class A2dpSinkService extends ProfileService { } protected A2dpSinkStateMachine getOrCreateStateMachine(BluetoothDevice device) { A2dpSinkStateMachine newStateMachine = new A2dpSinkStateMachine(mLooper, device, this, mNativeInterface); synchronized (mDeviceStateMap) { A2dpSinkStateMachine existingStateMachine = mDeviceStateMap.putIfAbsent(device, newStateMachine); // Given null is not a valid value in our map, ConcurrentHashMap will return null if the // key was absent and our new value was added. We should then start and return it. Else // we quit the new one so we don't leak a thread if (existingStateMachine == null) { newStateMachine.start(); return newStateMachine; A2dpSinkStateMachine sm = mDeviceStateMap.get(device); if (sm != null) { return sm; } return existingStateMachine; sm = new A2dpSinkStateMachine(this, device, mLooper, mNativeInterface); mDeviceStateMap.put(device, sm); return sm; } } Loading Loading @@ -543,18 +515,10 @@ public class A2dpSinkService extends ProfileService { /** Receive and route a stack event from the JNI */ 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; case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED -> onConnectionStateChanged(event); case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED -> onAudioStateChanged(event); case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED -> onAudioConfigChanged(event); default -> Log.e(TAG, "Received unknown stack event of type " + event.mType); } } Loading @@ -570,18 +534,11 @@ public class A2dpSinkService extends ProfileService { 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"); return; } else if (state == StackEvent.AUDIO_STATE_STARTED) { mA2dpSinkStreamHandler .obtainMessage(A2dpSinkStreamHandler.SRC_STR_START) .sendToTarget(); if (state == StackEvent.AUDIO_STATE_STARTED) { mA2dpSinkStreamHandler.sendEmptyMessage(A2dpSinkStreamHandler.SRC_STR_START); } else if (state == StackEvent.AUDIO_STATE_STOPPED || state == StackEvent.AUDIO_STATE_REMOTE_SUSPEND) { mA2dpSinkStreamHandler .obtainMessage(A2dpSinkStreamHandler.SRC_STR_STOP) .sendToTarget(); mA2dpSinkStreamHandler.sendEmptyMessage(A2dpSinkStreamHandler.SRC_STR_STOP); } else { Log.w(TAG, "Unhandled audio state change, state=" + state); } Loading android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java +78 −86 Original line number Diff line number Diff line Loading @@ -17,6 +17,11 @@ package com.android.bluetooth.a2dpsink; import static android.Manifest.permission.BLUETOOTH_CONNECT; import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; import static android.bluetooth.BluetoothProfile.STATE_CONNECTING; import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING; import android.annotation.RequiresPermission; import android.bluetooth.BluetoothA2dpSink; Loading @@ -41,15 +46,15 @@ class A2dpSinkStateMachine extends StateMachine { private static final String TAG = A2dpSinkStateMachine.class.getSimpleName(); // 0->99 Events from Outside @VisibleForTesting static final int CONNECT = 1; @VisibleForTesting static final int DISCONNECT = 2; @VisibleForTesting static final int MESSAGE_CONNECT = 1; @VisibleForTesting static final int MESSAGE_DISCONNECT = 2; // 100->199 Internal Events @VisibleForTesting static final int CLEANUP = 100; @VisibleForTesting static final int CONNECT_TIMEOUT = 101; @VisibleForTesting static final int MESSAGE_CONNECT_TIMEOUT = 101; // 200->299 Events from Native @VisibleForTesting static final int STACK_EVENT = 200; @VisibleForTesting static final int MESSAGE_STACK_EVENT = 200; static final int CONNECT_TIMEOUT_MS = 10000; Loading @@ -62,13 +67,13 @@ class A2dpSinkStateMachine extends StateMachine { protected final Connected mConnected; protected final Disconnecting mDisconnecting; protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED; protected int mMostRecentState = STATE_DISCONNECTED; protected BluetoothAudioConfig mAudioConfig = null; A2dpSinkStateMachine( Looper looper, BluetoothDevice device, A2dpSinkService service, BluetoothDevice device, Looper looper, A2dpSinkNativeInterface nativeInterface) { super(TAG, looper); mDevice = device; Loading @@ -88,6 +93,7 @@ class A2dpSinkStateMachine extends StateMachine { setInitialState(mDisconnected); Log.d(TAG, "[" + mDevice + "] State machine created"); start(); } /** Loading Loading @@ -115,17 +121,17 @@ class A2dpSinkStateMachine extends StateMachine { /** send the Connect command asynchronously */ final void connect() { sendMessage(CONNECT); sendMessage(MESSAGE_CONNECT); } /** send the Disconnect command asynchronously */ final void disconnect() { sendMessage(DISCONNECT); sendMessage(MESSAGE_DISCONNECT); } /** send the stack event asynchronously */ final void onStackEvent(StackEvent event) { sendMessage(STACK_EVENT, event); sendMessage(MESSAGE_STACK_EVENT, event); } /** Loading Loading @@ -154,37 +160,36 @@ class A2dpSinkStateMachine extends StateMachine { @Override public void enter() { Log.d(TAG, "[" + mDevice + "] Enter Disconnected"); if (mMostRecentState != BluetoothProfile.STATE_DISCONNECTED) { if (mMostRecentState != STATE_DISCONNECTED) { sendMessage(CLEANUP); } onConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTED); onConnectionStateChanged(STATE_DISCONNECTED); } @Override public boolean processMessage(Message message) { switch (message.what) { case STACK_EVENT: processStackEvent((StackEvent) message.obj); return true; case CONNECT: case MESSAGE_STACK_EVENT -> processStackEvent((StackEvent) message.obj); case MESSAGE_CONNECT -> { Log.d(TAG, "[" + mDevice + "] Connect"); transitionTo(mConnecting); return true; case CLEANUP: mService.removeStateMachine(A2dpSinkStateMachine.this); return true; } case CLEANUP -> mService.removeStateMachine(A2dpSinkStateMachine.this); default -> { return false; } } return true; } @RequiresPermission(BLUETOOTH_PRIVILEGED) void processStackEvent(StackEvent event) { switch (event.mType) { case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: if (event.mType != StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) { return; } switch (event.mState) { case StackEvent.CONNECTION_STATE_CONNECTING: if (mService.getConnectionPolicy(mDevice) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { case STATE_CONNECTING -> { if (mService.getConnectionPolicy(mDevice) == CONNECTION_POLICY_FORBIDDEN) { Log.w( TAG, "[" Loading @@ -196,14 +201,9 @@ class A2dpSinkStateMachine extends StateMachine { mConnecting.mIncomingConnection = true; transitionTo(mConnecting); } break; case StackEvent.CONNECTION_STATE_CONNECTED: transitionTo(mConnected); break; case StackEvent.CONNECTION_STATE_DISCONNECTED: sendMessage(CLEANUP); break; } case STATE_CONNECTED -> transitionTo(mConnected); case STATE_DISCONNECTED -> sendMessage(CLEANUP); } } } Loading @@ -214,8 +214,8 @@ class A2dpSinkStateMachine extends StateMachine { @Override public void enter() { Log.d(TAG, "[" + mDevice + "] Enter Connecting"); onConnectionStateChanged(BluetoothProfile.STATE_CONNECTING); sendMessageDelayed(CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS); onConnectionStateChanged(STATE_CONNECTING); sendMessageDelayed(MESSAGE_CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS); if (!mIncomingConnection) { mNativeInterface.connectA2dpSink(mDevice); Loading @@ -227,13 +227,9 @@ class A2dpSinkStateMachine extends StateMachine { @Override public boolean processMessage(Message message) { switch (message.what) { case STACK_EVENT: processStackEvent((StackEvent) message.obj); return true; case CONNECT_TIMEOUT: transitionTo(mDisconnected); return true; case DISCONNECT: case MESSAGE_STACK_EVENT -> processStackEvent((StackEvent) message.obj); case MESSAGE_CONNECT_TIMEOUT -> transitionTo(mDisconnected); case MESSAGE_DISCONNECT -> { Log.d( TAG, "[" Loading @@ -241,28 +237,27 @@ class A2dpSinkStateMachine extends StateMachine { + "] Received disconnect message while connecting." + "deferred"); deferMessage(message); return true; } default -> { return false; } } return true; } void processStackEvent(StackEvent event) { switch (event.mType) { case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: switch (event.mState) { case StackEvent.CONNECTION_STATE_CONNECTED: transitionTo(mConnected); break; case StackEvent.CONNECTION_STATE_DISCONNECTED: transitionTo(mDisconnected); break; if (event.mType != StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) { return; } switch (event.mState) { case STATE_CONNECTED -> transitionTo(mConnected); case STATE_DISCONNECTED -> transitionTo(mDisconnected); } } @Override public void exit() { removeMessages(CONNECT_TIMEOUT); removeMessages(MESSAGE_CONNECT_TIMEOUT); mIncomingConnection = false; } } Loading @@ -271,42 +266,39 @@ class A2dpSinkStateMachine extends StateMachine { @Override public void enter() { Log.d(TAG, "[" + mDevice + "] Enter Connected"); onConnectionStateChanged(BluetoothProfile.STATE_CONNECTED); onConnectionStateChanged(STATE_CONNECTED); } @Override public boolean processMessage(Message message) { switch (message.what) { case DISCONNECT: case MESSAGE_DISCONNECT -> { transitionTo(mDisconnecting); mNativeInterface.disconnectA2dpSink(mDevice); return true; case STACK_EVENT: processStackEvent((StackEvent) message.obj); return true; } case MESSAGE_STACK_EVENT -> processStackEvent((StackEvent) message.obj); default -> { return false; } } return true; } void processStackEvent(StackEvent event) { switch (event.mType) { case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED -> { switch (event.mState) { case StackEvent.CONNECTION_STATE_DISCONNECTING: transitionTo(mDisconnecting); break; case StackEvent.CONNECTION_STATE_DISCONNECTED: transitionTo(mDisconnected); break; case STATE_DISCONNECTING -> transitionTo(mDisconnecting); case STATE_DISCONNECTED -> transitionTo(mDisconnected); } break; case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED: } case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED -> { mAudioConfig = new BluetoothAudioConfig( event.mSampleRate, event.mChannelCount, AudioFormat.ENCODING_PCM_16BIT); break; } } } } Loading @@ -315,7 +307,7 @@ class A2dpSinkStateMachine extends StateMachine { @Override public void enter() { Log.d(TAG, "[" + mDevice + "] Enter Disconnecting"); onConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTING); onConnectionStateChanged(STATE_DISCONNECTING); transitionTo(mDisconnected); } } Loading @@ -324,7 +316,7 @@ class A2dpSinkStateMachine extends StateMachine { if (mMostRecentState == currentState) { return; } if (currentState == BluetoothProfile.STATE_CONNECTED) { if (currentState == STATE_CONNECTED) { MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.A2DP_SINK); } Log.d(TAG, "[" + mDevice + "] Connection state: " + mMostRecentState + "->" + currentState); Loading android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java +4 −3 Original line number Diff line number Diff line Loading @@ -70,9 +70,10 @@ public class A2dpSinkStreamHandler extends Handler { private static final int STATE_FOCUS_GRANTED = 1; // Private variables. private A2dpSinkService mA2dpSinkService; private A2dpSinkNativeInterface mNativeInterface; private AudioManager mAudioManager; private final A2dpSinkService mA2dpSinkService; private final A2dpSinkNativeInterface mNativeInterface; private final AudioManager mAudioManager; // Keep track if the remote device is providing audio private boolean mStreamAvailable = false; // Keep track of the relevant audio focus (None, Transient, Gain) Loading android/app/src/com/android/bluetooth/a2dpsink/StackEvent.java +0 −6 Original line number Diff line number Diff line Loading @@ -24,12 +24,6 @@ final class StackEvent { static final int EVENT_TYPE_AUDIO_STATE_CHANGED = 2; static final int EVENT_TYPE_AUDIO_CONFIG_CHANGED = 3; // match up with btav_connection_state_t enum of bt_av.h static final int CONNECTION_STATE_DISCONNECTED = 0; static final int CONNECTION_STATE_CONNECTING = 1; static final int CONNECTION_STATE_CONNECTED = 2; static final int CONNECTION_STATE_DISCONNECTING = 3; // match up with btav_audio_state_t enum of bt_av.h static final int AUDIO_STATE_REMOTE_SUSPEND = 0; static final int AUDIO_STATE_STOPPED = 1; Loading android/app/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java +97 −131 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java +40 −83 Original line number Diff line number Diff line Loading @@ -27,8 +27,6 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.IBluetoothA2dpSink; import android.content.AttributionSource; import android.content.Context; import android.media.AudioManager; import android.os.Looper; import android.sysprop.BluetoothProperties; import android.util.Log; Loading @@ -51,62 +49,43 @@ import java.util.concurrent.ConcurrentHashMap; public class A2dpSinkService extends ProfileService { private static final String TAG = A2dpSinkService.class.getSimpleName(); private static A2dpSinkService sService; // This is also used as a lock for shared data in {@link A2dpSinkService} @GuardedBy("mDeviceStateMap") private final Map<BluetoothDevice, A2dpSinkStateMachine> mDeviceStateMap = new ConcurrentHashMap<>(1); private final Object mActiveDeviceLock = new Object(); private final Object mStreamHandlerLock = new Object(); private final AdapterService mAdapterService; private final DatabaseManager mDatabaseManager; private final A2dpSinkNativeInterface mNativeInterface; private final Looper mLooper; private final int mMaxConnectedAudioDevices; private final Object mActiveDeviceLock = new Object(); @GuardedBy("mStreamHandlerLock") private final A2dpSinkStreamHandler mA2dpSinkStreamHandler; @GuardedBy("mActiveDeviceLock") private BluetoothDevice mActiveDevice = null; private final Object mStreamHandlerLock = new Object(); @GuardedBy("mStreamHandlerLock") private A2dpSinkStreamHandler mA2dpSinkStreamHandler; private static A2dpSinkService sService; private int mMaxConnectedAudioDevices; private AdapterService mAdapterService; private DatabaseManager mDatabaseManager; public A2dpSinkService(Context ctx) { super(ctx); mNativeInterface = requireNonNull(A2dpSinkNativeInterface.getInstance()); mLooper = Looper.getMainLooper(); public A2dpSinkService(AdapterService adapterService) { this(adapterService, A2dpSinkNativeInterface.getInstance(), Looper.getMainLooper()); } @VisibleForTesting A2dpSinkService(Context ctx, A2dpSinkNativeInterface nativeInterface, Looper looper) { super(ctx); A2dpSinkService( AdapterService adapterService, A2dpSinkNativeInterface nativeInterface, Looper looper) { super(requireNonNull(adapterService)); mAdapterService = adapterService; mDatabaseManager = requireNonNull(mAdapterService.getDatabase()); mNativeInterface = requireNonNull(nativeInterface); mLooper = looper; } public static boolean isEnabled() { return BluetoothProperties.isProfileA2dpSinkEnabled().orElse(false); } @Override public void start() { mAdapterService = requireNonNull( AdapterService.getAdapterService(), "AdapterService cannot be null when A2dpSinkService starts"); mDatabaseManager = requireNonNull( AdapterService.getAdapterService().getDatabase(), "DatabaseManager cannot be null when A2dpSinkService starts"); mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices(); mNativeInterface.init(mMaxConnectedAudioDevices); synchronized (mStreamHandlerLock) { mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, mNativeInterface); } Loading @@ -114,6 +93,10 @@ public class A2dpSinkService extends ProfileService { setA2dpSinkService(this); } public static boolean isEnabled() { return BluetoothProperties.isProfileA2dpSinkEnabled().orElse(false); } @Override public void stop() { setA2dpSinkService(null); Loading @@ -125,10 +108,7 @@ public class A2dpSinkService extends ProfileService { mDeviceStateMap.clear(); } synchronized (mStreamHandlerLock) { if (mA2dpSinkStreamHandler != null) { mA2dpSinkStreamHandler.cleanup(); mA2dpSinkStreamHandler = null; } } } Loading Loading @@ -164,7 +144,6 @@ public class A2dpSinkService extends ProfileService { /** Request audio focus such that the designated device can stream audio */ public void requestAudioFocus(BluetoothDevice device, boolean request) { synchronized (mStreamHandlerLock) { if (mA2dpSinkStreamHandler == null) return; mA2dpSinkStreamHandler.requestAudioFocus(request); } } Loading @@ -176,17 +155,12 @@ public class A2dpSinkService extends ProfileService { */ public int getFocusState() { synchronized (mStreamHandlerLock) { if (mA2dpSinkStreamHandler == null) return AudioManager.ERROR; return mA2dpSinkStreamHandler.getFocusState(); } } @RequiresPermission(BLUETOOTH_PRIVILEGED) boolean isA2dpPlaying(BluetoothDevice device) { enforceCallingOrSelfPermission( BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); synchronized (mStreamHandlerLock) { if (mA2dpSinkStreamHandler == null) return false; return mA2dpSinkStreamHandler.isPlaying(); } } Loading Loading @@ -309,6 +283,9 @@ public class A2dpSinkService extends ProfileService { if (service == null) { return false; } service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); return service.isA2dpPlaying(device); } Loading Loading @@ -407,19 +384,14 @@ public class A2dpSinkService extends ProfileService { } protected A2dpSinkStateMachine getOrCreateStateMachine(BluetoothDevice device) { A2dpSinkStateMachine newStateMachine = new A2dpSinkStateMachine(mLooper, device, this, mNativeInterface); synchronized (mDeviceStateMap) { A2dpSinkStateMachine existingStateMachine = mDeviceStateMap.putIfAbsent(device, newStateMachine); // Given null is not a valid value in our map, ConcurrentHashMap will return null if the // key was absent and our new value was added. We should then start and return it. Else // we quit the new one so we don't leak a thread if (existingStateMachine == null) { newStateMachine.start(); return newStateMachine; A2dpSinkStateMachine sm = mDeviceStateMap.get(device); if (sm != null) { return sm; } return existingStateMachine; sm = new A2dpSinkStateMachine(this, device, mLooper, mNativeInterface); mDeviceStateMap.put(device, sm); return sm; } } Loading Loading @@ -543,18 +515,10 @@ public class A2dpSinkService extends ProfileService { /** Receive and route a stack event from the JNI */ 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; case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED -> onConnectionStateChanged(event); case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED -> onAudioStateChanged(event); case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED -> onAudioConfigChanged(event); default -> Log.e(TAG, "Received unknown stack event of type " + event.mType); } } Loading @@ -570,18 +534,11 @@ public class A2dpSinkService extends ProfileService { 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"); return; } else if (state == StackEvent.AUDIO_STATE_STARTED) { mA2dpSinkStreamHandler .obtainMessage(A2dpSinkStreamHandler.SRC_STR_START) .sendToTarget(); if (state == StackEvent.AUDIO_STATE_STARTED) { mA2dpSinkStreamHandler.sendEmptyMessage(A2dpSinkStreamHandler.SRC_STR_START); } else if (state == StackEvent.AUDIO_STATE_STOPPED || state == StackEvent.AUDIO_STATE_REMOTE_SUSPEND) { mA2dpSinkStreamHandler .obtainMessage(A2dpSinkStreamHandler.SRC_STR_STOP) .sendToTarget(); mA2dpSinkStreamHandler.sendEmptyMessage(A2dpSinkStreamHandler.SRC_STR_STOP); } else { Log.w(TAG, "Unhandled audio state change, state=" + state); } Loading
android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java +78 −86 Original line number Diff line number Diff line Loading @@ -17,6 +17,11 @@ package com.android.bluetooth.a2dpsink; import static android.Manifest.permission.BLUETOOTH_CONNECT; import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; import static android.bluetooth.BluetoothProfile.STATE_CONNECTING; import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING; import android.annotation.RequiresPermission; import android.bluetooth.BluetoothA2dpSink; Loading @@ -41,15 +46,15 @@ class A2dpSinkStateMachine extends StateMachine { private static final String TAG = A2dpSinkStateMachine.class.getSimpleName(); // 0->99 Events from Outside @VisibleForTesting static final int CONNECT = 1; @VisibleForTesting static final int DISCONNECT = 2; @VisibleForTesting static final int MESSAGE_CONNECT = 1; @VisibleForTesting static final int MESSAGE_DISCONNECT = 2; // 100->199 Internal Events @VisibleForTesting static final int CLEANUP = 100; @VisibleForTesting static final int CONNECT_TIMEOUT = 101; @VisibleForTesting static final int MESSAGE_CONNECT_TIMEOUT = 101; // 200->299 Events from Native @VisibleForTesting static final int STACK_EVENT = 200; @VisibleForTesting static final int MESSAGE_STACK_EVENT = 200; static final int CONNECT_TIMEOUT_MS = 10000; Loading @@ -62,13 +67,13 @@ class A2dpSinkStateMachine extends StateMachine { protected final Connected mConnected; protected final Disconnecting mDisconnecting; protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED; protected int mMostRecentState = STATE_DISCONNECTED; protected BluetoothAudioConfig mAudioConfig = null; A2dpSinkStateMachine( Looper looper, BluetoothDevice device, A2dpSinkService service, BluetoothDevice device, Looper looper, A2dpSinkNativeInterface nativeInterface) { super(TAG, looper); mDevice = device; Loading @@ -88,6 +93,7 @@ class A2dpSinkStateMachine extends StateMachine { setInitialState(mDisconnected); Log.d(TAG, "[" + mDevice + "] State machine created"); start(); } /** Loading Loading @@ -115,17 +121,17 @@ class A2dpSinkStateMachine extends StateMachine { /** send the Connect command asynchronously */ final void connect() { sendMessage(CONNECT); sendMessage(MESSAGE_CONNECT); } /** send the Disconnect command asynchronously */ final void disconnect() { sendMessage(DISCONNECT); sendMessage(MESSAGE_DISCONNECT); } /** send the stack event asynchronously */ final void onStackEvent(StackEvent event) { sendMessage(STACK_EVENT, event); sendMessage(MESSAGE_STACK_EVENT, event); } /** Loading Loading @@ -154,37 +160,36 @@ class A2dpSinkStateMachine extends StateMachine { @Override public void enter() { Log.d(TAG, "[" + mDevice + "] Enter Disconnected"); if (mMostRecentState != BluetoothProfile.STATE_DISCONNECTED) { if (mMostRecentState != STATE_DISCONNECTED) { sendMessage(CLEANUP); } onConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTED); onConnectionStateChanged(STATE_DISCONNECTED); } @Override public boolean processMessage(Message message) { switch (message.what) { case STACK_EVENT: processStackEvent((StackEvent) message.obj); return true; case CONNECT: case MESSAGE_STACK_EVENT -> processStackEvent((StackEvent) message.obj); case MESSAGE_CONNECT -> { Log.d(TAG, "[" + mDevice + "] Connect"); transitionTo(mConnecting); return true; case CLEANUP: mService.removeStateMachine(A2dpSinkStateMachine.this); return true; } case CLEANUP -> mService.removeStateMachine(A2dpSinkStateMachine.this); default -> { return false; } } return true; } @RequiresPermission(BLUETOOTH_PRIVILEGED) void processStackEvent(StackEvent event) { switch (event.mType) { case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: if (event.mType != StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) { return; } switch (event.mState) { case StackEvent.CONNECTION_STATE_CONNECTING: if (mService.getConnectionPolicy(mDevice) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { case STATE_CONNECTING -> { if (mService.getConnectionPolicy(mDevice) == CONNECTION_POLICY_FORBIDDEN) { Log.w( TAG, "[" Loading @@ -196,14 +201,9 @@ class A2dpSinkStateMachine extends StateMachine { mConnecting.mIncomingConnection = true; transitionTo(mConnecting); } break; case StackEvent.CONNECTION_STATE_CONNECTED: transitionTo(mConnected); break; case StackEvent.CONNECTION_STATE_DISCONNECTED: sendMessage(CLEANUP); break; } case STATE_CONNECTED -> transitionTo(mConnected); case STATE_DISCONNECTED -> sendMessage(CLEANUP); } } } Loading @@ -214,8 +214,8 @@ class A2dpSinkStateMachine extends StateMachine { @Override public void enter() { Log.d(TAG, "[" + mDevice + "] Enter Connecting"); onConnectionStateChanged(BluetoothProfile.STATE_CONNECTING); sendMessageDelayed(CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS); onConnectionStateChanged(STATE_CONNECTING); sendMessageDelayed(MESSAGE_CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS); if (!mIncomingConnection) { mNativeInterface.connectA2dpSink(mDevice); Loading @@ -227,13 +227,9 @@ class A2dpSinkStateMachine extends StateMachine { @Override public boolean processMessage(Message message) { switch (message.what) { case STACK_EVENT: processStackEvent((StackEvent) message.obj); return true; case CONNECT_TIMEOUT: transitionTo(mDisconnected); return true; case DISCONNECT: case MESSAGE_STACK_EVENT -> processStackEvent((StackEvent) message.obj); case MESSAGE_CONNECT_TIMEOUT -> transitionTo(mDisconnected); case MESSAGE_DISCONNECT -> { Log.d( TAG, "[" Loading @@ -241,28 +237,27 @@ class A2dpSinkStateMachine extends StateMachine { + "] Received disconnect message while connecting." + "deferred"); deferMessage(message); return true; } default -> { return false; } } return true; } void processStackEvent(StackEvent event) { switch (event.mType) { case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: switch (event.mState) { case StackEvent.CONNECTION_STATE_CONNECTED: transitionTo(mConnected); break; case StackEvent.CONNECTION_STATE_DISCONNECTED: transitionTo(mDisconnected); break; if (event.mType != StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) { return; } switch (event.mState) { case STATE_CONNECTED -> transitionTo(mConnected); case STATE_DISCONNECTED -> transitionTo(mDisconnected); } } @Override public void exit() { removeMessages(CONNECT_TIMEOUT); removeMessages(MESSAGE_CONNECT_TIMEOUT); mIncomingConnection = false; } } Loading @@ -271,42 +266,39 @@ class A2dpSinkStateMachine extends StateMachine { @Override public void enter() { Log.d(TAG, "[" + mDevice + "] Enter Connected"); onConnectionStateChanged(BluetoothProfile.STATE_CONNECTED); onConnectionStateChanged(STATE_CONNECTED); } @Override public boolean processMessage(Message message) { switch (message.what) { case DISCONNECT: case MESSAGE_DISCONNECT -> { transitionTo(mDisconnecting); mNativeInterface.disconnectA2dpSink(mDevice); return true; case STACK_EVENT: processStackEvent((StackEvent) message.obj); return true; } case MESSAGE_STACK_EVENT -> processStackEvent((StackEvent) message.obj); default -> { return false; } } return true; } void processStackEvent(StackEvent event) { switch (event.mType) { case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED -> { switch (event.mState) { case StackEvent.CONNECTION_STATE_DISCONNECTING: transitionTo(mDisconnecting); break; case StackEvent.CONNECTION_STATE_DISCONNECTED: transitionTo(mDisconnected); break; case STATE_DISCONNECTING -> transitionTo(mDisconnecting); case STATE_DISCONNECTED -> transitionTo(mDisconnected); } break; case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED: } case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED -> { mAudioConfig = new BluetoothAudioConfig( event.mSampleRate, event.mChannelCount, AudioFormat.ENCODING_PCM_16BIT); break; } } } } Loading @@ -315,7 +307,7 @@ class A2dpSinkStateMachine extends StateMachine { @Override public void enter() { Log.d(TAG, "[" + mDevice + "] Enter Disconnecting"); onConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTING); onConnectionStateChanged(STATE_DISCONNECTING); transitionTo(mDisconnected); } } Loading @@ -324,7 +316,7 @@ class A2dpSinkStateMachine extends StateMachine { if (mMostRecentState == currentState) { return; } if (currentState == BluetoothProfile.STATE_CONNECTED) { if (currentState == STATE_CONNECTED) { MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.A2DP_SINK); } Log.d(TAG, "[" + mDevice + "] Connection state: " + mMostRecentState + "->" + currentState); Loading
android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java +4 −3 Original line number Diff line number Diff line Loading @@ -70,9 +70,10 @@ public class A2dpSinkStreamHandler extends Handler { private static final int STATE_FOCUS_GRANTED = 1; // Private variables. private A2dpSinkService mA2dpSinkService; private A2dpSinkNativeInterface mNativeInterface; private AudioManager mAudioManager; private final A2dpSinkService mA2dpSinkService; private final A2dpSinkNativeInterface mNativeInterface; private final AudioManager mAudioManager; // Keep track if the remote device is providing audio private boolean mStreamAvailable = false; // Keep track of the relevant audio focus (None, Transient, Gain) Loading
android/app/src/com/android/bluetooth/a2dpsink/StackEvent.java +0 −6 Original line number Diff line number Diff line Loading @@ -24,12 +24,6 @@ final class StackEvent { static final int EVENT_TYPE_AUDIO_STATE_CHANGED = 2; static final int EVENT_TYPE_AUDIO_CONFIG_CHANGED = 3; // match up with btav_connection_state_t enum of bt_av.h static final int CONNECTION_STATE_DISCONNECTED = 0; static final int CONNECTION_STATE_CONNECTING = 1; static final int CONNECTION_STATE_CONNECTED = 2; static final int CONNECTION_STATE_DISCONNECTING = 3; // match up with btav_audio_state_t enum of bt_av.h static final int AUDIO_STATE_REMOTE_SUSPEND = 0; static final int AUDIO_STATE_STOPPED = 1; Loading
android/app/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java +97 −131 File changed.Preview size limit exceeded, changes collapsed. Show changes