diff options
125 files changed, 6654 insertions, 5190 deletions
diff --git a/android/app/aidl/android/bluetooth/BluetoothClass.aidl b/android/app/aidl/android/bluetooth/BluetoothClass.aidl index 8699b1eee3..124ee53760 100644 --- a/android/app/aidl/android/bluetooth/BluetoothClass.aidl +++ b/android/app/aidl/android/bluetooth/BluetoothClass.aidl @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/android/app/aidl/android/bluetooth/BluetoothHidDeviceAppQosSettings.aidl b/android/app/aidl/android/bluetooth/BluetoothHidDeviceAppQosSettings.aidl index 14f91140af..74f51a8037 100644 --- a/android/app/aidl/android/bluetooth/BluetoothHidDeviceAppQosSettings.aidl +++ b/android/app/aidl/android/bluetooth/BluetoothHidDeviceAppQosSettings.aidl @@ -5,7 +5,7 @@ ** 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 +** 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, diff --git a/android/app/aidl/android/bluetooth/BluetoothHidDeviceAppSdpSettings.aidl b/android/app/aidl/android/bluetooth/BluetoothHidDeviceAppSdpSettings.aidl index 87dd10ee15..a55a794c6e 100644 --- a/android/app/aidl/android/bluetooth/BluetoothHidDeviceAppSdpSettings.aidl +++ b/android/app/aidl/android/bluetooth/BluetoothHidDeviceAppSdpSettings.aidl @@ -5,7 +5,7 @@ ** 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 +** 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, diff --git a/android/app/aidl/android/bluetooth/IBluetooth.aidl b/android/app/aidl/android/bluetooth/IBluetooth.aidl index bc46353f7c..d8a5421d6f 100644 --- a/android/app/aidl/android/bluetooth/IBluetooth.aidl +++ b/android/app/aidl/android/bluetooth/IBluetooth.aidl @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/android/app/aidl/android/bluetooth/IBluetoothCallback.aidl b/android/app/aidl/android/bluetooth/IBluetoothCallback.aidl index 9b2888eadb..6101044e29 100644 --- a/android/app/aidl/android/bluetooth/IBluetoothCallback.aidl +++ b/android/app/aidl/android/bluetooth/IBluetoothCallback.aidl @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/android/app/aidl/android/bluetooth/IBluetoothSocketManager.aidl b/android/app/aidl/android/bluetooth/IBluetoothSocketManager.aidl index ce2397ff33..bd54982d13 100644 --- a/android/app/aidl/android/bluetooth/IBluetoothSocketManager.aidl +++ b/android/app/aidl/android/bluetooth/IBluetoothSocketManager.aidl @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/android/app/aidl/android/bluetooth/IncomingRfcommSocketInfo.aidl b/android/app/aidl/android/bluetooth/IncomingRfcommSocketInfo.aidl index 38ffe6f699..c3d542c774 100644 --- a/android/app/aidl/android/bluetooth/IncomingRfcommSocketInfo.aidl +++ b/android/app/aidl/android/bluetooth/IncomingRfcommSocketInfo.aidl @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java b/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java index d4fc9d54e8..a4c265adf7 100644 --- a/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java +++ b/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java @@ -306,4 +306,9 @@ public class BluetoothMethodProxy { ComponentCaller caller, Uri uri, int modeFlags) { return caller.checkContentUriPermission(uri, modeFlags); } + + /** Proxies {@link Context#grantUriPermission(String, Uri, int)}. } */ + public void grantUriPermission(Context context, String packageName, Uri uri, int modeFlags) { + context.grantUriPermission(packageName, uri, modeFlags); + } } diff --git a/android/app/src/com/android/bluetooth/a2dp/A2dpService.java b/android/app/src/com/android/bluetooth/a2dp/A2dpService.java index 9481146d61..de37a5b3f4 100644 --- a/android/app/src/com/android/bluetooth/a2dp/A2dpService.java +++ b/android/app/src/com/android/bluetooth/a2dp/A2dpService.java @@ -26,8 +26,6 @@ import static android.bluetooth.BluetoothProfile.STATE_CONNECTING; import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING; -import static com.android.bluetooth.Utils.checkCallerTargetSdk; - import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNullElseGet; @@ -44,15 +42,12 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.bluetooth.BufferConstraints; -import android.bluetooth.IBluetoothA2dp; import android.companion.CompanionDeviceManager; -import android.content.AttributionSource; import android.content.Intent; import android.media.AudioDeviceCallback; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.BluetoothProfileConnectionInfo; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -63,7 +58,6 @@ import com.android.bluetooth.BluetoothStatsLog; import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.ActiveDeviceManager; import com.android.bluetooth.btservice.AdapterService; -import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.ServiceFactory; import com.android.bluetooth.btservice.storage.DatabaseManager; @@ -73,7 +67,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -168,7 +161,7 @@ public class A2dpService extends ProfileService { @Override protected IProfileServiceBinder initBinder() { - return new BluetoothA2dpBinder(this); + return new A2dpServiceBinder(this); } @Override @@ -203,6 +196,10 @@ public class A2dpService extends ProfileService { mHandler.removeCallbacksAndMessages(null); } + CompanionDeviceManager getCompanionDeviceManager() { + return mCompanionDeviceManager; + } + public static synchronized A2dpService getA2dpService() { if (sA2dpService == null) { Log.w(TAG, "getA2dpService(): service is null"); @@ -1001,13 +998,7 @@ public class A2dpService extends ProfileService { return null; } Log.d(TAG, "Creating a new state machine for " + device); - sm = - new A2dpStateMachine( - this, - device, - mNativeInterface, - mA2dpOffloadEnabled, - mLooper); + sm = new A2dpStateMachine(this, device, mNativeInterface, mA2dpOffloadEnabled, mLooper); mStateMachines.put(device, sm); return sm; } @@ -1345,320 +1336,6 @@ public class A2dpService extends ProfileService { : null; } - /** Binder object: must be a static class or memory leak may occur. */ - @VisibleForTesting - static class BluetoothA2dpBinder extends IBluetoothA2dp.Stub implements IProfileServiceBinder { - private A2dpService mService; - - @RequiresPermission(BLUETOOTH_CONNECT) - private A2dpService getServiceAndEnforceConnect(AttributionSource source) { - // Cache mService because it can change while getService is called - A2dpService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - return service; - } - - private A2dpService getService() { - // Cache mService because it can change while getService is called - A2dpService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG)) { - return null; - } - return service; - } - - BluetoothA2dpBinder(A2dpService svc) { - mService = svc; - } - - @Override - public void cleanup() { - mService = null; - } - - @Override - public boolean connect(BluetoothDevice device, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - return service.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - return service.disconnect(device); - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getConnectedDevices(); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return STATE_DISCONNECTED; - } - - return service.getConnectionState(device); - } - - @Override - public boolean setActiveDevice(BluetoothDevice device, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - if (device == null) { - return service.removeActiveDevice(false); - } else { - return service.setActiveDevice(device); - } - } - - @Override - public BluetoothDevice getActiveDevice(AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return null; - } - - return service.getActiveDevice(); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return CONNECTION_POLICY_UNKNOWN; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getConnectionPolicy(device); - } - - @Override - public void setAvrcpAbsoluteVolume(int volume, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - service.setAvrcpAbsoluteVolume(volume); - } - - @Override - public boolean isA2dpPlaying(BluetoothDevice device, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - return service.isA2dpPlaying(device); - } - - @Override - public List<BluetoothCodecType> getSupportedCodecTypes() { - A2dpService service = getService(); - if (service == null) { - return Collections.emptyList(); - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getSupportedCodecTypes(); - } - - @Override - public BluetoothCodecStatus getCodecStatus( - BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return null; - } - - Utils.enforceCdmAssociationIfNotBluetoothPrivileged( - service, service.mCompanionDeviceManager, source, device); - - return service.getCodecStatus(device); - } - - @Override - public void setCodecConfigPreference( - BluetoothDevice device, - BluetoothCodecConfig codecConfig, - AttributionSource source) { - requireNonNull(device); - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - Utils.enforceCdmAssociationIfNotBluetoothPrivileged( - service, service.mCompanionDeviceManager, source, device); - - service.setCodecConfigPreference(device, codecConfig); - } - - @Override - public void enableOptionalCodecs(BluetoothDevice device, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - if (checkCallerTargetSdk( - mService, source.getPackageName(), Build.VERSION_CODES.TIRAMISU)) { - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - } - service.enableOptionalCodecs(device); - } - - @Override - public void disableOptionalCodecs(BluetoothDevice device, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - if (checkCallerTargetSdk( - mService, source.getPackageName(), Build.VERSION_CODES.TIRAMISU)) { - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - } - service.disableOptionalCodecs(device); - } - - @Override - public int isOptionalCodecsSupported(BluetoothDevice device, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN; - } - - if (checkCallerTargetSdk( - mService, source.getPackageName(), Build.VERSION_CODES.TIRAMISU)) { - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - } - return service.getSupportsOptionalCodecs(device); - } - - @Override - public int isOptionalCodecsEnabled(BluetoothDevice device, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN; - } - - if (checkCallerTargetSdk( - mService, source.getPackageName(), Build.VERSION_CODES.TIRAMISU)) { - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - } - return service.getOptionalCodecsEnabled(device); - } - - @Override - public void setOptionalCodecsEnabled( - BluetoothDevice device, int value, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - if (checkCallerTargetSdk( - mService, source.getPackageName(), Build.VERSION_CODES.TIRAMISU)) { - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - } - service.setOptionalCodecsEnabled(device, value); - } - - @Override - public int getDynamicBufferSupport(AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return BluetoothA2dp.DYNAMIC_BUFFER_SUPPORT_NONE; - } - - return service.getDynamicBufferSupport(); - } - - @Override - public BufferConstraints getBufferConstraints(AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return null; - } - - return service.getBufferConstraints(); - } - - @Override - public boolean setBufferLengthMillis(int codec, int value, AttributionSource source) { - A2dpService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - return service.setBufferLengthMillis(codec, value); - } - } - @Override public void dump(StringBuilder sb) { super.dump(sb); diff --git a/android/app/src/com/android/bluetooth/a2dp/A2dpServiceBinder.java b/android/app/src/com/android/bluetooth/a2dp/A2dpServiceBinder.java new file mode 100644 index 0000000000..da8aaae42a --- /dev/null +++ b/android/app/src/com/android/bluetooth/a2dp/A2dpServiceBinder.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2025 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.a2dp; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import static com.android.bluetooth.Utils.checkCallerTargetSdk; + +import static java.util.Objects.requireNonNull; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothCodecConfig; +import android.bluetooth.BluetoothCodecStatus; +import android.bluetooth.BluetoothCodecType; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BufferConstraints; +import android.bluetooth.IBluetoothA2dp; +import android.content.AttributionSource; +import android.os.Build; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Collections; +import java.util.List; + +class A2dpServiceBinder extends IBluetoothA2dp.Stub implements IProfileServiceBinder { + private static final String TAG = A2dpServiceBinder.class.getSimpleName(); + + private A2dpService mService; + + A2dpServiceBinder(A2dpService svc) { + mService = svc; + } + + @Override + public void cleanup() { + mService = null; + } + + private A2dpService getService() { + A2dpService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG)) { + return null; + } + return service; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private A2dpService getServiceAndEnforceConnect(AttributionSource source) { + A2dpService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + return service; + } + + @Override + public boolean connect(BluetoothDevice device, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + return service.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + return service.disconnect(device); + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getConnectedDevices(); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return STATE_DISCONNECTED; + } + return service.getConnectionState(device); + } + + @Override + public boolean setActiveDevice(BluetoothDevice device, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + + if (device == null) { + return service.removeActiveDevice(false); + } else { + return service.setActiveDevice(device); + } + } + + @Override + public BluetoothDevice getActiveDevice(AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return null; + } + return service.getActiveDevice(); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return CONNECTION_POLICY_UNKNOWN; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectionPolicy(device); + } + + @Override + public void setAvrcpAbsoluteVolume(int volume, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.setAvrcpAbsoluteVolume(volume); + } + + @Override + public boolean isA2dpPlaying(BluetoothDevice device, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + return service.isA2dpPlaying(device); + } + + @Override + public List<BluetoothCodecType> getSupportedCodecTypes() { + A2dpService service = getService(); + if (service == null) { + return Collections.emptyList(); + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getSupportedCodecTypes(); + } + + @Override + public BluetoothCodecStatus getCodecStatus(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return null; + } + + Utils.enforceCdmAssociationIfNotBluetoothPrivileged( + service, service.getCompanionDeviceManager(), source, device); + + return service.getCodecStatus(device); + } + + @Override + public void setCodecConfigPreference( + BluetoothDevice device, BluetoothCodecConfig codecConfig, AttributionSource source) { + requireNonNull(device); + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + Utils.enforceCdmAssociationIfNotBluetoothPrivileged( + service, service.getCompanionDeviceManager(), source, device); + + service.setCodecConfigPreference(device, codecConfig); + } + + @Override + public void enableOptionalCodecs(BluetoothDevice device, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + if (checkCallerTargetSdk(mService, source.getPackageName(), Build.VERSION_CODES.TIRAMISU)) { + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + } + service.enableOptionalCodecs(device); + } + + @Override + public void disableOptionalCodecs(BluetoothDevice device, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + if (checkCallerTargetSdk(mService, source.getPackageName(), Build.VERSION_CODES.TIRAMISU)) { + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + } + service.disableOptionalCodecs(device); + } + + @Override + public int isOptionalCodecsSupported(BluetoothDevice device, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN; + } + + if (checkCallerTargetSdk(mService, source.getPackageName(), Build.VERSION_CODES.TIRAMISU)) { + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + } + return service.getSupportsOptionalCodecs(device); + } + + @Override + public int isOptionalCodecsEnabled(BluetoothDevice device, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN; + } + + if (checkCallerTargetSdk(mService, source.getPackageName(), Build.VERSION_CODES.TIRAMISU)) { + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + } + return service.getOptionalCodecsEnabled(device); + } + + @Override + public void setOptionalCodecsEnabled( + BluetoothDevice device, int value, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + if (checkCallerTargetSdk(mService, source.getPackageName(), Build.VERSION_CODES.TIRAMISU)) { + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + } + service.setOptionalCodecsEnabled(device, value); + } + + @Override + public int getDynamicBufferSupport(AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return BluetoothA2dp.DYNAMIC_BUFFER_SUPPORT_NONE; + } + + return service.getDynamicBufferSupport(); + } + + @Override + public BufferConstraints getBufferConstraints(AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return null; + } + + return service.getBufferConstraints(); + } + + @Override + public boolean setBufferLengthMillis(int codec, int value, AttributionSource source) { + A2dpService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + + return service.setBufferLengthMillis(codec, value); + } +} diff --git a/android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java b/android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java index 10b9249bad..6750a9a9c8 100644 --- a/android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java +++ b/android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java @@ -13,30 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + 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_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; -import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING; import static java.util.Objects.requireNonNull; -import android.annotation.RequiresPermission; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAudioConfig; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; -import android.bluetooth.IBluetoothA2dpSink; -import android.content.AttributionSource; import android.os.Looper; import android.sysprop.BluetoothProperties; import android.util.Log; -import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.storage.DatabaseManager; @@ -45,7 +39,6 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -177,136 +170,6 @@ public class A2dpSinkService extends ProfileService { return new A2dpSinkServiceBinder(this); } - // Binder object: Must be static class or memory leak may occur - @VisibleForTesting - static class A2dpSinkServiceBinder extends IBluetoothA2dpSink.Stub - implements IProfileServiceBinder { - private A2dpSinkService mService; - - A2dpSinkServiceBinder(A2dpSinkService svc) { - mService = svc; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private A2dpSinkService getService(AttributionSource source) { - // Cache mService because it can change while getService is called - A2dpSinkService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - - return service; - } - - @Override - public boolean connect(BluetoothDevice device, AttributionSource source) { - A2dpSinkService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device, AttributionSource source) { - A2dpSinkService service = getService(source); - if (service == null) { - return false; - } - return service.disconnect(device); - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - A2dpSinkService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - return service.getConnectedDevices(); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - A2dpSinkService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - A2dpSinkService service = getService(source); - if (service == null) { - return STATE_DISCONNECTED; - } - return service.getConnectionState(device); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - A2dpSinkService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { - A2dpSinkService service = getService(source); - if (service == null) { - return CONNECTION_POLICY_UNKNOWN; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getConnectionPolicy(device); - } - - @Override - public boolean isA2dpPlaying(BluetoothDevice device, AttributionSource source) { - A2dpSinkService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.isA2dpPlaying(device); - } - - @Override - public BluetoothAudioConfig getAudioConfig( - BluetoothDevice device, AttributionSource source) { - A2dpSinkService service = getService(source); - if (service == null) { - return null; - } - return service.getAudioConfig(device); - } - } - /* Generic Profile Code */ /** diff --git a/android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceBinder.java b/android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceBinder.java new file mode 100644 index 0000000000..851e48a286 --- /dev/null +++ b/android/app/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceBinder.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2025 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 static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothAudioConfig; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.IBluetoothA2dpSink; +import android.content.AttributionSource; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Collections; +import java.util.List; + +class A2dpSinkServiceBinder extends IBluetoothA2dpSink.Stub implements IProfileServiceBinder { + private static final String TAG = A2dpSinkServiceBinder.class.getSimpleName(); + + private A2dpSinkService mService; + + A2dpSinkServiceBinder(A2dpSinkService svc) { + mService = svc; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private A2dpSinkService getService(AttributionSource source) { + A2dpSinkService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + + return service; + } + + @Override + public boolean connect(BluetoothDevice device, AttributionSource source) { + A2dpSinkService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device, AttributionSource source) { + A2dpSinkService service = getService(source); + if (service == null) { + return false; + } + return service.disconnect(device); + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + A2dpSinkService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getConnectedDevices(); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + A2dpSinkService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + A2dpSinkService service = getService(source); + if (service == null) { + return STATE_DISCONNECTED; + } + return service.getConnectionState(device); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + A2dpSinkService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { + A2dpSinkService service = getService(source); + if (service == null) { + return CONNECTION_POLICY_UNKNOWN; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectionPolicy(device); + } + + @Override + public boolean isA2dpPlaying(BluetoothDevice device, AttributionSource source) { + A2dpSinkService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.isA2dpPlaying(device); + } + + @Override + public BluetoothAudioConfig getAudioConfig(BluetoothDevice device, AttributionSource source) { + A2dpSinkService service = getService(source); + if (service == null) { + return null; + } + return service.getAudioConfig(device); + } +} diff --git a/android/app/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java b/android/app/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java index 4551f320da..c4ef6a2437 100644 --- a/android/app/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java +++ b/android/app/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java @@ -16,19 +16,14 @@ package com.android.bluetooth.avrcpcontroller; -import static android.Manifest.permission.BLUETOOTH_CONNECT; import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; import static android.bluetooth.BluetoothProfile.STATE_CONNECTING; import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; import static java.util.Objects.requireNonNull; -import android.annotation.RequiresPermission; import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothAvrcpPlayerSettings; import android.bluetooth.BluetoothDevice; -import android.bluetooth.IBluetoothAvrcpController; -import android.content.AttributionSource; import android.content.Intent; import android.media.AudioManager; import android.support.v4.media.MediaBrowserCompat.MediaItem; @@ -46,7 +41,6 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; @@ -368,90 +362,6 @@ public class AvrcpControllerService extends ProfileService { return new AvrcpControllerServiceBinder(this); } - // Binder object: Must be static class or memory leak may occur - @VisibleForTesting - static class AvrcpControllerServiceBinder extends IBluetoothAvrcpController.Stub - implements IProfileServiceBinder { - private AvrcpControllerService mService; - - AvrcpControllerServiceBinder(AvrcpControllerService service) { - mService = service; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private AvrcpControllerService getService(AttributionSource source) { - // Cache mService because it can change while getService is called - AvrcpControllerService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - - return service; - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - AvrcpControllerService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - return service.getConnectedDevices(); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - AvrcpControllerService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - AvrcpControllerService service = getService(source); - if (service == null) { - return STATE_DISCONNECTED; - } - return service.getConnectionState(device); - } - - @Override - public void sendGroupNavigationCmd( - BluetoothDevice device, int keyCode, int keyState, AttributionSource source) { - getService(source); - Log.w(TAG, "sendGroupNavigationCmd not implemented"); - } - - @Override - public void setPlayerApplicationSetting( - BluetoothAvrcpPlayerSettings settings, AttributionSource source) { - getService(source); - Log.w(TAG, "setPlayerApplicationSetting not implemented"); - } - - @Override - public BluetoothAvrcpPlayerSettings getPlayerSettings( - BluetoothDevice device, AttributionSource source) { - getService(source); - Log.w(TAG, "getPlayerSettings not implemented"); - return null; - } - } - // Called by JNI when a device has connected or disconnected. @VisibleForTesting synchronized void onConnectionStateChanged( diff --git a/android/app/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerServiceBinder.java b/android/app/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerServiceBinder.java new file mode 100644 index 0000000000..105e9a8709 --- /dev/null +++ b/android/app/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerServiceBinder.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2025 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 static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothAvrcpPlayerSettings; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.IBluetoothAvrcpController; +import android.content.AttributionSource; +import android.util.Log; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Collections; +import java.util.List; + +class AvrcpControllerServiceBinder extends IBluetoothAvrcpController.Stub + implements IProfileServiceBinder { + private static final String TAG = AvrcpControllerServiceBinder.class.getSimpleName(); + + private AvrcpControllerService mService; + + AvrcpControllerServiceBinder(AvrcpControllerService service) { + mService = service; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private AvrcpControllerService getService(AttributionSource source) { + AvrcpControllerService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + + return service; + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + AvrcpControllerService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getConnectedDevices(); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + AvrcpControllerService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + AvrcpControllerService service = getService(source); + if (service == null) { + return STATE_DISCONNECTED; + } + return service.getConnectionState(device); + } + + @Override + public void sendGroupNavigationCmd( + BluetoothDevice device, int keyCode, int keyState, AttributionSource source) { + getService(source); + Log.w(TAG, "sendGroupNavigationCmd not implemented"); + } + + @Override + public void setPlayerApplicationSetting( + BluetoothAvrcpPlayerSettings settings, AttributionSource source) { + getService(source); + Log.w(TAG, "setPlayerApplicationSetting not implemented"); + } + + @Override + public BluetoothAvrcpPlayerSettings getPlayerSettings( + BluetoothDevice device, AttributionSource source) { + getService(source); + Log.w(TAG, "getPlayerSettings not implemented"); + return null; + } +} diff --git a/android/app/src/com/android/bluetooth/bass_client/BassClientService.java b/android/app/src/com/android/bluetooth/bass_client/BassClientService.java index edf88f9331..42528676b4 100644 --- a/android/app/src/com/android/bluetooth/bass_client/BassClientService.java +++ b/android/app/src/com/android/bluetooth/bass_client/BassClientService.java @@ -16,9 +16,6 @@ package com.android.bluetooth.bass_client; -import static android.Manifest.permission.BLUETOOTH_CONNECT; -import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; -import static android.Manifest.permission.BLUETOOTH_SCAN; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; @@ -36,7 +33,6 @@ import static com.android.bluetooth.flags.Flags.leaudioSortScansToSyncByFails; import static java.util.Objects.requireNonNull; -import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; @@ -50,7 +46,6 @@ import android.bluetooth.BluetoothLeBroadcastSubgroup; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothStatusCodes; import android.bluetooth.BluetoothUuid; -import android.bluetooth.IBluetoothLeBroadcastAssistant; import android.bluetooth.IBluetoothLeBroadcastAssistantCallback; import android.bluetooth.le.IScannerCallback; import android.bluetooth.le.PeriodicAdvertisingCallback; @@ -60,7 +55,6 @@ import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanRecord; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; -import android.content.AttributionSource; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -737,7 +731,7 @@ public class BassClientService extends ProfileService { @Override protected IProfileServiceBinder initBinder() { - return new BluetoothLeBroadcastAssistantBinder(this); + return new BassClientServiceBinder(this); } @Override @@ -4829,235 +4823,4 @@ public class BassClientService extends ProfileService { sEventLogger.dump(sb); sb.append("\n"); } - - /** Binder object: must be a static class or memory leak may occur */ - @VisibleForTesting - static class BluetoothLeBroadcastAssistantBinder extends IBluetoothLeBroadcastAssistant.Stub - implements IProfileServiceBinder { - BassClientService mService; - - BluetoothLeBroadcastAssistantBinder(BassClientService svc) { - mService = svc; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) - private BassClientService getServiceAndEnforceConnect(AttributionSource source) { - // Cache mService because it can change while getService is called - BassClientService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service; - } - - @RequiresPermission(allOf = {BLUETOOTH_SCAN, BLUETOOTH_PRIVILEGED}) - private BassClientService getServiceAndEnforceScan(AttributionSource source) { - // Cache mService because it can change while getService is called - BassClientService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkScanPermissionForDataDelivery( - service, source, TAG, "getServiceAndEnforceScan")) { - return null; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service; - } - - @Override - public int getConnectionState(BluetoothDevice sink, AttributionSource source) { - BassClientService service = getServiceAndEnforceConnect(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return STATE_DISCONNECTED; - } - return service.getConnectionState(sink); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - BassClientService service = getServiceAndEnforceConnect(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return Collections.emptyList(); - } - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - BassClientService service = getServiceAndEnforceConnect(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return Collections.emptyList(); - } - return service.getConnectedDevices(); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - BassClientService service = getServiceAndEnforceConnect(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return false; - } - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { - BassClientService service = getServiceAndEnforceConnect(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return CONNECTION_POLICY_FORBIDDEN; - } - return service.getConnectionPolicy(device); - } - - @Override - public void registerCallback( - IBluetoothLeBroadcastAssistantCallback cb, AttributionSource source) { - BassClientService service = getServiceAndEnforceConnect(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return; - } - service.registerCallback(cb); - } - - @Override - public void unregisterCallback( - IBluetoothLeBroadcastAssistantCallback cb, AttributionSource source) { - BassClientService service = getServiceAndEnforceConnect(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return; - } - service.unregisterCallback(cb); - } - - @Override - public void startSearchingForSources(List<ScanFilter> filters, AttributionSource source) { - BassClientService service = getServiceAndEnforceScan(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return; - } - service.startSearchingForSources(filters); - } - - @Override - public void stopSearchingForSources(AttributionSource source) { - BassClientService service = getServiceAndEnforceScan(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return; - } - service.stopSearchingForSources(); - } - - @Override - public boolean isSearchInProgress(AttributionSource source) { - BassClientService service = getServiceAndEnforceScan(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return false; - } - return service.isSearchInProgress(); - } - - @Override - public void addSource( - BluetoothDevice sink, - BluetoothLeBroadcastMetadata sourceMetadata, - boolean isGroupOp, - AttributionSource source) { - BassClientService service = getServiceAndEnforceConnect(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return; - } - service.addSource(sink, sourceMetadata, isGroupOp); - } - - @Override - public void modifySource( - BluetoothDevice sink, - int sourceId, - BluetoothLeBroadcastMetadata updatedMetadata, - AttributionSource source) { - BassClientService service = getServiceAndEnforceConnect(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return; - } - service.modifySource(sink, sourceId, updatedMetadata); - } - - @Override - public void removeSource(BluetoothDevice sink, int sourceId, AttributionSource source) { - BassClientService service = getServiceAndEnforceConnect(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return; - } - service.removeSource(sink, sourceId); - } - - @Override - public List<BluetoothLeBroadcastReceiveState> getAllSources( - BluetoothDevice sink, AttributionSource source) { - BassClientService service = getServiceAndEnforceConnect(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return Collections.emptyList(); - } - return service.getAllSources(sink); - } - - @Override - public int getMaximumSourceCapacity(BluetoothDevice sink, AttributionSource source) { - BassClientService service = getServiceAndEnforceConnect(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return 0; - } - return service.getMaximumSourceCapacity(sink); - } - - @Override - public BluetoothLeBroadcastMetadata getSourceMetadata( - BluetoothDevice sink, int sourceId, AttributionSource source) { - BassClientService service = getServiceAndEnforceConnect(source); - if (service == null) { - Log.e(TAG, "Service is null"); - return null; - } - return service.getSourceMetadata(sink, sourceId); - } - } } diff --git a/android/app/src/com/android/bluetooth/bass_client/BassClientServiceBinder.java b/android/app/src/com/android/bluetooth/bass_client/BassClientServiceBinder.java new file mode 100644 index 0000000000..d319e6ed47 --- /dev/null +++ b/android/app/src/com/android/bluetooth/bass_client/BassClientServiceBinder.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2025 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.bass_client; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static android.Manifest.permission.BLUETOOTH_SCAN; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.bluetooth.IBluetoothLeBroadcastAssistant; +import android.bluetooth.IBluetoothLeBroadcastAssistantCallback; +import android.bluetooth.le.ScanFilter; +import android.content.AttributionSource; +import android.util.Log; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Collections; +import java.util.List; + +class BassClientServiceBinder extends IBluetoothLeBroadcastAssistant.Stub + implements IProfileServiceBinder { + private static final String TAG = BassClientServiceBinder.class.getSimpleName(); + + private BassClientService mService; + + BassClientServiceBinder(BassClientService svc) { + mService = svc; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) + private BassClientService getServiceAndEnforceConnect(AttributionSource source) { + BassClientService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + + return service; + } + + @RequiresPermission(allOf = {BLUETOOTH_SCAN, BLUETOOTH_PRIVILEGED}) + private BassClientService getServiceAndEnforceScan(AttributionSource source) { + BassClientService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkScanPermissionForDataDelivery( + service, source, TAG, "getServiceAndEnforceScan")) { + return null; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + + return service; + } + + @Override + public int getConnectionState(BluetoothDevice sink, AttributionSource source) { + BassClientService service = getServiceAndEnforceConnect(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return STATE_DISCONNECTED; + } + return service.getConnectionState(sink); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + BassClientService service = getServiceAndEnforceConnect(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + BassClientService service = getServiceAndEnforceConnect(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return Collections.emptyList(); + } + return service.getConnectedDevices(); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + BassClientService service = getServiceAndEnforceConnect(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return false; + } + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { + BassClientService service = getServiceAndEnforceConnect(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return CONNECTION_POLICY_FORBIDDEN; + } + return service.getConnectionPolicy(device); + } + + @Override + public void registerCallback( + IBluetoothLeBroadcastAssistantCallback cb, AttributionSource source) { + BassClientService service = getServiceAndEnforceConnect(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.registerCallback(cb); + } + + @Override + public void unregisterCallback( + IBluetoothLeBroadcastAssistantCallback cb, AttributionSource source) { + BassClientService service = getServiceAndEnforceConnect(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.unregisterCallback(cb); + } + + @Override + public void startSearchingForSources(List<ScanFilter> filters, AttributionSource source) { + BassClientService service = getServiceAndEnforceScan(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.startSearchingForSources(filters); + } + + @Override + public void stopSearchingForSources(AttributionSource source) { + BassClientService service = getServiceAndEnforceScan(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.stopSearchingForSources(); + } + + @Override + public boolean isSearchInProgress(AttributionSource source) { + BassClientService service = getServiceAndEnforceScan(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return false; + } + return service.isSearchInProgress(); + } + + @Override + public void addSource( + BluetoothDevice sink, + BluetoothLeBroadcastMetadata sourceMetadata, + boolean isGroupOp, + AttributionSource source) { + BassClientService service = getServiceAndEnforceConnect(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.addSource(sink, sourceMetadata, isGroupOp); + } + + @Override + public void modifySource( + BluetoothDevice sink, + int sourceId, + BluetoothLeBroadcastMetadata updatedMetadata, + AttributionSource source) { + BassClientService service = getServiceAndEnforceConnect(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.modifySource(sink, sourceId, updatedMetadata); + } + + @Override + public void removeSource(BluetoothDevice sink, int sourceId, AttributionSource source) { + BassClientService service = getServiceAndEnforceConnect(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return; + } + service.removeSource(sink, sourceId); + } + + @Override + public List<BluetoothLeBroadcastReceiveState> getAllSources( + BluetoothDevice sink, AttributionSource source) { + BassClientService service = getServiceAndEnforceConnect(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return Collections.emptyList(); + } + return service.getAllSources(sink); + } + + @Override + public int getMaximumSourceCapacity(BluetoothDevice sink, AttributionSource source) { + BassClientService service = getServiceAndEnforceConnect(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return 0; + } + return service.getMaximumSourceCapacity(sink); + } + + @Override + public BluetoothLeBroadcastMetadata getSourceMetadata( + BluetoothDevice sink, int sourceId, AttributionSource source) { + BassClientService service = getServiceAndEnforceConnect(source); + if (service == null) { + Log.e(TAG, "Service is null"); + return null; + } + return service.getSourceMetadata(sink, sourceId); + } +} diff --git a/android/app/src/com/android/bluetooth/btservice/MetricsLogger.java b/android/app/src/com/android/bluetooth/btservice/MetricsLogger.java index 87794605dc..f59e69a42d 100644 --- a/android/app/src/com/android/bluetooth/btservice/MetricsLogger.java +++ b/android/app/src/com/android/bluetooth/btservice/MetricsLogger.java @@ -37,6 +37,12 @@ import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVEN import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_VOLUME_CONTROL; import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__STATE_BONDED; import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__STATE_NONE; +import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__ASHA; +import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__CLASSIC; +import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__LE_AUDIO; +import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__DAY; +import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__MONTH; +import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__WEEK; import static com.android.bluetooth.BtRestrictedStatsLog.RESTRICTED_BLUETOOTH_DEVICE_NAME_REPORTED; import android.app.AlarmManager; @@ -45,6 +51,7 @@ import android.bluetooth.BluetoothA2dpSink; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAvrcpController; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHapClient; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothHeadsetClient; import android.bluetooth.BluetoothHearingAid; @@ -60,11 +67,13 @@ import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProtoEnums; import android.bluetooth.BluetoothSap; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Build; import android.os.SystemClock; +import android.provider.Settings; import android.util.Log; import android.util.proto.ProtoOutputStream; @@ -87,11 +96,14 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.function.BiPredicate; /** Class of Bluetooth Metrics */ public class MetricsLogger { @@ -273,6 +285,7 @@ public class MetricsLogger { filter.addAction(BluetoothPbap.ACTION_CONNECTION_STATE_CHANGED); filter.addAction(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); filter.addAction(BluetoothSap.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED); mAdapterService.registerReceiver(mReceiver, filter); } @@ -285,9 +298,18 @@ public class MetricsLogger { Log.w(TAG, "Received intent with null action"); return; } + BluetoothDevice device = + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); switch (action) { case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.A2DP, intent); + if (state == BluetoothProfile.STATE_CONNECTED + && isMedicalDevice(device)) { + updateHearingDeviceActiveTime( + device, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__CLASSIC); + } break; case BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.A2DP_SINK, intent); @@ -297,12 +319,23 @@ public class MetricsLogger { break; case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.HEADSET, intent); + if (state == BluetoothProfile.STATE_CONNECTED + && isMedicalDevice(device)) { + updateHearingDeviceActiveTime( + device, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__CLASSIC); + } break; case BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.HEADSET_CLIENT, intent); break; case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.HEARING_AID, intent); + if (state == BluetoothProfile.STATE_CONNECTED) { + updateHearingDeviceActiveTime( + device, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__ASHA); + } break; case BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.HID_DEVICE, intent); @@ -331,6 +364,14 @@ public class MetricsLogger { case BluetoothSap.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.SAP, intent); break; + case BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED: + if (state == BluetoothProfile.STATE_CONNECTED) { + updateHearingDeviceActiveTime( + device, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__LE_AUDIO + ); + } + break; default: Log.w(TAG, "Received unknown intent " + intent); break; @@ -915,4 +956,56 @@ public class MetricsLogger { syncStatus, getRemoteDeviceInfoProto(device, false)); } + + void logHearingDeviceActiveEvent(BluetoothDevice device, int type, int timePeriod) { + BluetoothStatsLog.write( + BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED, + type, + timePeriod, + getRemoteDeviceInfoProto(device, true)); + } + + void updateHearingDeviceActiveTime(BluetoothDevice device, int deviceTypeProto) { + // Time comparison includes a +/- 1 hour tolerance to prevent data loss + updateLastActiveTime( + device, + deviceTypeProto, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__DAY, + "last_active_day", + (now, lastActive) -> now.isAfter(lastActive.plusDays(1).minusHours(1))); + updateLastActiveTime( + device, + deviceTypeProto, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__WEEK, + "last_active_week", + (now, lastActive) -> now.isAfter(lastActive.plusWeeks(1).minusHours(1))); + updateLastActiveTime( + device, + deviceTypeProto, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__MONTH, + "last_active_month", + (now, lastActive) -> now.isAfter(lastActive.plusMonths(1).minusHours(1))); + } + + private void updateLastActiveTime( + BluetoothDevice device, + int deviceTypeProto, + int timePeriodProto, + String timePeriodSettingsKey, + BiPredicate<LocalDateTime, LocalDateTime> timeComparison) { + final ContentResolver contentResolver = mAdapterService.getContentResolver(); + final String lastActive = Settings.Secure.getString(contentResolver, timePeriodSettingsKey); + final LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault()); + if (lastActive == null || timeComparison.test(now, LocalDateTime.parse(lastActive))) { + Settings.Secure.putString(contentResolver, timePeriodSettingsKey, now.toString()); + logHearingDeviceActiveEvent(device, deviceTypeProto, timePeriodProto); + } + } + + private boolean isMedicalDevice(BluetoothDevice device) { + final String deviceName = mAdapterService.getRemoteName(device); + final List<String> wordBreakdownList = getWordBreakdownList(deviceName); + boolean isMedicalDevice = !getMatchedStringForMedicalDevice(wordBreakdownList).isEmpty(); + return isMedicalDevice; + } } diff --git a/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java b/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java index 2b49f467a8..7a6597f5b0 100644 --- a/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java +++ b/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java @@ -17,7 +17,6 @@ package com.android.bluetooth.csip; -import static android.Manifest.permission.BLUETOOTH_CONNECT; import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; @@ -31,7 +30,6 @@ import static java.util.Objects.requireNonNullElseGet; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.RequiresPermission; import android.bluetooth.BluetoothCsipSetCoordinator; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; @@ -40,7 +38,6 @@ import android.bluetooth.BluetoothUuid; import android.bluetooth.IBluetoothCsipSetCoordinator; import android.bluetooth.IBluetoothCsipSetCoordinatorCallback; import android.bluetooth.IBluetoothCsipSetCoordinatorLockCallback; -import android.content.AttributionSource; import android.content.Intent; import android.os.Handler; import android.os.HandlerThread; @@ -152,7 +149,7 @@ public class CsipSetCoordinatorService extends ProfileService { @Override protected IProfileServiceBinder initBinder() { - return new BluetoothCsisBinder(this); + return new CsipSetCoordinatorServiceBinder(this); } @Override @@ -991,171 +988,6 @@ public class CsipSetCoordinatorService extends ProfileService { BluetoothProfile.CSIP_SET_COORDINATOR, device, fromState, toState); } - /** Binder object: must be a static class or memory leak may occur */ - @VisibleForTesting - static class BluetoothCsisBinder extends IBluetoothCsipSetCoordinator.Stub - implements IProfileServiceBinder { - private CsipSetCoordinatorService mService; - - BluetoothCsisBinder(CsipSetCoordinatorService svc) { - mService = svc; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) - private CsipSetCoordinatorService getService(AttributionSource source) { - // Cache mService because it can change while getService is called - CsipSetCoordinatorService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service; - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - requireNonNull(source); - - CsipSetCoordinatorService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getConnectedDevices(); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - requireNonNull(source); - - CsipSetCoordinatorService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - requireNonNull(source); - - CsipSetCoordinatorService service = getService(source); - if (service == null) { - return STATE_DISCONNECTED; - } - - return service.getConnectionState(device); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - requireNonNull(device); - requireNonNull(source); - - CsipSetCoordinatorService service = getService(source); - if (service == null) { - return false; - } - - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - requireNonNull(source); - - CsipSetCoordinatorService service = getService(source); - if (service == null) { - return CONNECTION_POLICY_UNKNOWN; - } - - return service.getConnectionPolicy(device); - } - - @Override - public ParcelUuid lockGroup( - int groupId, - @NonNull IBluetoothCsipSetCoordinatorLockCallback callback, - AttributionSource source) { - requireNonNull(callback); - requireNonNull(source); - - CsipSetCoordinatorService service = getService(source); - if (service == null) { - return null; - } - - UUID lockUuid = service.lockGroup(groupId, callback); - return lockUuid == null ? null : new ParcelUuid(lockUuid); - } - - @Override - public void unlockGroup(@NonNull ParcelUuid lockUuid, AttributionSource source) { - requireNonNull(lockUuid); - requireNonNull(source); - - CsipSetCoordinatorService service = getService(source); - if (service == null) { - return; - } - - service.unlockGroup(lockUuid.getUuid()); - } - - @Override - public List<Integer> getAllGroupIds(ParcelUuid uuid, AttributionSource source) { - requireNonNull(uuid); - requireNonNull(source); - - CsipSetCoordinatorService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getAllGroupIds(uuid); - } - - @Override - public Map<Integer, ParcelUuid> getGroupUuidMapByDevice( - BluetoothDevice device, AttributionSource source) { - CsipSetCoordinatorService service = getService(source); - if (service == null) { - return null; - } - - return service.getGroupUuidMapByDevice(device); - } - - @Override - public int getDesiredGroupSize(int groupId, AttributionSource source) { - CsipSetCoordinatorService service = getService(source); - if (service == null) { - return IBluetoothCsipSetCoordinator.CSIS_GROUP_SIZE_UNKNOWN; - } - - return service.getDesiredGroupSize(groupId); - } - } - @Override public void dump(StringBuilder sb) { super.dump(sb); diff --git a/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorServiceBinder.java b/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorServiceBinder.java new file mode 100644 index 0000000000..5b08c1983b --- /dev/null +++ b/android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorServiceBinder.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2025 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.csip; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.IBluetoothCsipSetCoordinator; +import android.bluetooth.IBluetoothCsipSetCoordinatorLockCallback; +import android.content.AttributionSource; +import android.os.ParcelUuid; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +class CsipSetCoordinatorServiceBinder extends IBluetoothCsipSetCoordinator.Stub + implements IProfileServiceBinder { + private static final String TAG = CsipSetCoordinatorServiceBinder.class.getSimpleName(); + + private CsipSetCoordinatorService mService; + + CsipSetCoordinatorServiceBinder(CsipSetCoordinatorService svc) { + mService = svc; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) + private CsipSetCoordinatorService getService(AttributionSource source) { + CsipSetCoordinatorService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service; + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + requireNonNull(source); + + CsipSetCoordinatorService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getConnectedDevices(); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + requireNonNull(source); + + CsipSetCoordinatorService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + requireNonNull(source); + + CsipSetCoordinatorService service = getService(source); + if (service == null) { + return STATE_DISCONNECTED; + } + return service.getConnectionState(device); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + requireNonNull(device); + requireNonNull(source); + + CsipSetCoordinatorService service = getService(source); + if (service == null) { + return false; + } + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + requireNonNull(source); + + CsipSetCoordinatorService service = getService(source); + if (service == null) { + return CONNECTION_POLICY_UNKNOWN; + } + return service.getConnectionPolicy(device); + } + + @Override + public ParcelUuid lockGroup( + int groupId, + @NonNull IBluetoothCsipSetCoordinatorLockCallback callback, + AttributionSource source) { + requireNonNull(callback); + requireNonNull(source); + + CsipSetCoordinatorService service = getService(source); + if (service == null) { + return null; + } + + UUID lockUuid = service.lockGroup(groupId, callback); + return lockUuid == null ? null : new ParcelUuid(lockUuid); + } + + @Override + public void unlockGroup(@NonNull ParcelUuid lockUuid, AttributionSource source) { + requireNonNull(lockUuid); + requireNonNull(source); + + CsipSetCoordinatorService service = getService(source); + if (service == null) { + return; + } + service.unlockGroup(lockUuid.getUuid()); + } + + @Override + public List<Integer> getAllGroupIds(ParcelUuid uuid, AttributionSource source) { + requireNonNull(uuid); + requireNonNull(source); + + CsipSetCoordinatorService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getAllGroupIds(uuid); + } + + @Override + public Map<Integer, ParcelUuid> getGroupUuidMapByDevice( + BluetoothDevice device, AttributionSource source) { + CsipSetCoordinatorService service = getService(source); + if (service == null) { + return null; + } + return service.getGroupUuidMapByDevice(device); + } + + @Override + public int getDesiredGroupSize(int groupId, AttributionSource source) { + CsipSetCoordinatorService service = getService(source); + if (service == null) { + return IBluetoothCsipSetCoordinator.CSIS_GROUP_SIZE_UNKNOWN; + } + return service.getDesiredGroupSize(groupId); + } +} diff --git a/android/app/src/com/android/bluetooth/hap/HapClientService.java b/android/app/src/com/android/bluetooth/hap/HapClientService.java index 731f3da90c..9b8092763c 100644 --- a/android/app/src/com/android/bluetooth/hap/HapClientService.java +++ b/android/app/src/com/android/bluetooth/hap/HapClientService.java @@ -159,7 +159,7 @@ public class HapClientService extends ProfileService { @Override protected IProfileServiceBinder initBinder() { - return new HapClientBinder(this); + return new HapClientServiceBinder(this); } @Override diff --git a/android/app/src/com/android/bluetooth/hap/HapClientBinder.java b/android/app/src/com/android/bluetooth/hap/HapClientServiceBinder.java index eba1d9db4a..e036e43bb2 100644 --- a/android/app/src/com/android/bluetooth/hap/HapClientBinder.java +++ b/android/app/src/com/android/bluetooth/hap/HapClientServiceBinder.java @@ -36,21 +36,19 @@ import android.content.AttributionSource; import android.util.Log; import com.android.bluetooth.Utils; -import com.android.bluetooth.btservice.ProfileService; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; import com.android.internal.annotations.VisibleForTesting; import java.util.Collections; import java.util.List; -/** HapClientBinder class */ @VisibleForTesting -class HapClientBinder extends IBluetoothHapClient.Stub - implements ProfileService.IProfileServiceBinder { - private static final String TAG = HapClientBinder.class.getSimpleName(); +class HapClientServiceBinder extends IBluetoothHapClient.Stub implements IProfileServiceBinder { + private static final String TAG = HapClientServiceBinder.class.getSimpleName(); private HapClientService mService; - HapClientBinder(HapClientService svc) { + HapClientServiceBinder(HapClientService svc) { mService = svc; } @@ -62,7 +60,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) private HapClientService getService(AttributionSource source) { requireNonNull(source); - // Cache mService because it can change while getService is called HapClientService service = mService; if (Utils.isInstrumentationTestMode()) { @@ -86,7 +83,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub if (service == null) { return Collections.emptyList(); } - return service.getConnectedDevices(); } @@ -97,7 +93,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub if (service == null) { return Collections.emptyList(); } - return service.getDevicesMatchingConnectionStates(states); } @@ -109,7 +104,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(device); - return service.getConnectionState(device); } @@ -139,7 +133,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(device); - return service.getConnectionPolicy(device); } @@ -151,7 +144,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(device); - return service.getActivePresetIndex(device); } @@ -164,7 +156,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(device); - return service.getActivePresetInfo(device); } @@ -176,7 +167,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(device); - return service.getHapGroup(device); } @@ -188,7 +178,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(device); - service.selectPreset(device, presetIndex); } @@ -198,7 +187,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub if (service == null) { return; } - service.selectPresetForGroup(groupId, presetIndex); } @@ -210,7 +198,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(device); - service.switchToNextPreset(device); } @@ -220,7 +207,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub if (service == null) { return; } - service.switchToNextPresetForGroup(groupId); } @@ -232,7 +218,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(device); - service.switchToPreviousPreset(device); } @@ -242,7 +227,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub if (service == null) { return; } - service.switchToPreviousPresetForGroup(groupId); } @@ -255,7 +239,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(device); - return service.getPresetInfo(device, presetIndex); } @@ -268,7 +251,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(device); - return service.getAllPresetInfo(device); } @@ -280,7 +262,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(device); - return service.getFeatures(device); } @@ -294,7 +275,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub requireNonNull(device); requireNonNull(name); - service.setPresetName(device, presetIndex, name); } @@ -307,7 +287,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(name); - service.setPresetNameForGroup(groupId, presetIndex, name); } @@ -319,7 +298,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(callback); - service.registerCallback(callback); } @@ -331,7 +309,6 @@ class HapClientBinder extends IBluetoothHapClient.Stub } requireNonNull(callback); - service.unregisterCallback(callback); } } diff --git a/android/app/src/com/android/bluetooth/hearingaid/HearingAidService.java b/android/app/src/com/android/bluetooth/hearingaid/HearingAidService.java index a884944bac..0bf2650bb1 100644 --- a/android/app/src/com/android/bluetooth/hearingaid/HearingAidService.java +++ b/android/app/src/com/android/bluetooth/hearingaid/HearingAidService.java @@ -17,7 +17,6 @@ package com.android.bluetooth.hearingaid; import static android.Manifest.permission.BLUETOOTH_CONNECT; -import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; @@ -27,14 +26,11 @@ import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; import static java.util.Objects.requireNonNull; -import android.annotation.RequiresPermission; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothHearingAid.AdvertisementServiceData; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; -import android.bluetooth.IBluetoothHearingAid; -import android.content.AttributionSource; import android.content.Intent; import android.media.AudioDeviceCallback; import android.media.AudioDeviceInfo; @@ -57,7 +53,6 @@ import com.android.bluetooth.flags.Flags; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -130,7 +125,7 @@ public class HearingAidService extends ProfileService { @Override protected IProfileServiceBinder initBinder() { - return new BluetoothHearingAidBinder(this); + return new HearingAidServiceBinder(this); } @Override @@ -490,7 +485,7 @@ public class HearingAidService extends ProfileService { return mDeviceCapabilitiesMap.getOrDefault(device, -1); } - private AdvertisementServiceData getAdvertisementServiceData(BluetoothDevice device) { + AdvertisementServiceData getAdvertisementServiceData(BluetoothDevice device) { int capability = mAdapterService.getAshaCapability(device); int id = mAdapterService.getAshaTruncatedHiSyncId(device); if (capability < 0) { @@ -855,214 +850,6 @@ public class HearingAidService extends ProfileService { device, BluetoothProfile.HEARING_AID, toState, fromState); } - /** Binder object: must be a static class or memory leak may occur */ - @VisibleForTesting - static class BluetoothHearingAidBinder extends IBluetoothHearingAid.Stub - implements IProfileServiceBinder { - private HearingAidService mService; - - BluetoothHearingAidBinder(HearingAidService svc) { - mService = svc; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private HearingAidService getService(AttributionSource source) { - // Cache mService because it can change while getService is called - HearingAidService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - return service; - } - - @Override - public boolean connect(BluetoothDevice device, AttributionSource source) { - HearingAidService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device, AttributionSource source) { - HearingAidService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.disconnect(device); - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - HearingAidService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getConnectedDevices(); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - HearingAidService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - HearingAidService service = getService(source); - if (service == null) { - return STATE_DISCONNECTED; - } - - return service.getConnectionState(device); - } - - @Override - public boolean setActiveDevice(BluetoothDevice device, AttributionSource source) { - HearingAidService service = getService(source); - if (service == null) { - return false; - } - - if (device == null) { - return service.removeActiveDevice(false); - } else { - return service.setActiveDevice(device); - } - } - - @Override - public List<BluetoothDevice> getActiveDevices(AttributionSource source) { - HearingAidService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getActiveDevices(); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - HearingAidService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { - HearingAidService service = getService(source); - if (service == null) { - return CONNECTION_POLICY_UNKNOWN; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getConnectionPolicy(device); - } - - @Override - public void setVolume(int volume, AttributionSource source) { - HearingAidService service = getService(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - service.setVolume(volume); - } - - @Override - public long getHiSyncId(BluetoothDevice device, AttributionSource source) { - HearingAidService service = getService(source); - if (service == null) { - return BluetoothHearingAid.HI_SYNC_ID_INVALID; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getHiSyncId(device); - } - - @Override - public int getDeviceSide(BluetoothDevice device, AttributionSource source) { - HearingAidService service = getService(source); - if (service == null) { - return BluetoothHearingAid.SIDE_UNKNOWN; - } - - int side = service.getCapabilities(device); - if (side != BluetoothHearingAid.SIDE_UNKNOWN) { - side &= 1; - } - - return side; - } - - @Override - public int getDeviceMode(BluetoothDevice device, AttributionSource source) { - HearingAidService service = getService(source); - if (service == null) { - return BluetoothHearingAid.MODE_UNKNOWN; - } - - int mode = service.getCapabilities(device); - if (mode != BluetoothHearingAid.MODE_UNKNOWN) { - mode = mode >> 1 & 1; - } - - return mode; - } - - @Override - public AdvertisementServiceData getAdvertisementServiceData( - BluetoothDevice device, AttributionSource source) { - HearingAidService service = mService; - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkScanPermissionForDataDelivery( - service, source, TAG, "getAdvertisementServiceData")) { - return null; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getAdvertisementServiceData(device); - } - } - @Override public void dump(StringBuilder sb) { super.dump(sb); diff --git a/android/app/src/com/android/bluetooth/hearingaid/HearingAidServiceBinder.java b/android/app/src/com/android/bluetooth/hearingaid/HearingAidServiceBinder.java new file mode 100644 index 0000000000..de384fba5c --- /dev/null +++ b/android/app/src/com/android/bluetooth/hearingaid/HearingAidServiceBinder.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2025 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.hearingaid; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHearingAid; +import android.bluetooth.BluetoothHearingAid.AdvertisementServiceData; +import android.bluetooth.IBluetoothHearingAid; +import android.content.AttributionSource; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Collections; +import java.util.List; + +class HearingAidServiceBinder extends IBluetoothHearingAid.Stub implements IProfileServiceBinder { + private static final String TAG = HearingAidServiceBinder.class.getSimpleName(); + + private HearingAidService mService; + + HearingAidServiceBinder(HearingAidService svc) { + mService = svc; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private HearingAidService getService(AttributionSource source) { + HearingAidService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + return service; + } + + @Override + public boolean connect(BluetoothDevice device, AttributionSource source) { + HearingAidService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device, AttributionSource source) { + HearingAidService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.disconnect(device); + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + HearingAidService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getConnectedDevices(); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + HearingAidService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + HearingAidService service = getService(source); + if (service == null) { + return STATE_DISCONNECTED; + } + return service.getConnectionState(device); + } + + @Override + public boolean setActiveDevice(BluetoothDevice device, AttributionSource source) { + HearingAidService service = getService(source); + if (service == null) { + return false; + } + + if (device == null) { + return service.removeActiveDevice(false); + } else { + return service.setActiveDevice(device); + } + } + + @Override + public List<BluetoothDevice> getActiveDevices(AttributionSource source) { + HearingAidService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getActiveDevices(); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + HearingAidService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { + HearingAidService service = getService(source); + if (service == null) { + return CONNECTION_POLICY_UNKNOWN; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectionPolicy(device); + } + + @Override + public void setVolume(int volume, AttributionSource source) { + HearingAidService service = getService(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.setVolume(volume); + } + + @Override + public long getHiSyncId(BluetoothDevice device, AttributionSource source) { + HearingAidService service = getService(source); + if (service == null) { + return BluetoothHearingAid.HI_SYNC_ID_INVALID; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getHiSyncId(device); + } + + @Override + public int getDeviceSide(BluetoothDevice device, AttributionSource source) { + HearingAidService service = getService(source); + if (service == null) { + return BluetoothHearingAid.SIDE_UNKNOWN; + } + + int side = service.getCapabilities(device); + if (side != BluetoothHearingAid.SIDE_UNKNOWN) { + side &= 1; + } + + return side; + } + + @Override + public int getDeviceMode(BluetoothDevice device, AttributionSource source) { + HearingAidService service = getService(source); + if (service == null) { + return BluetoothHearingAid.MODE_UNKNOWN; + } + + int mode = service.getCapabilities(device); + if (mode != BluetoothHearingAid.MODE_UNKNOWN) { + mode = mode >> 1 & 1; + } + + return mode; + } + + @Override + public AdvertisementServiceData getAdvertisementServiceData( + BluetoothDevice device, AttributionSource source) { + HearingAidService service = mService; + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkScanPermissionForDataDelivery( + service, source, TAG, "getAdvertisementServiceData")) { + return null; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getAdvertisementServiceData(device); + } +} diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java index 251f347ea2..c5ab1f8dde 100644 --- a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java +++ b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java @@ -17,8 +17,6 @@ package com.android.bluetooth.hfp; import static android.Manifest.permission.BLUETOOTH_CONNECT; -import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; -import static android.Manifest.permission.MODIFY_PHONE_STATE; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; @@ -33,15 +31,12 @@ import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.RequiresPermission; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothSinkAudioPolicy; import android.bluetooth.BluetoothStatusCodes; import android.bluetooth.BluetoothUuid; -import android.bluetooth.IBluetoothHeadset; -import android.content.AttributionSource; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -80,7 +75,6 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -237,7 +231,7 @@ public class HeadsetService extends ProfileService { @Override public IProfileServiceBinder initBinder() { - return new BluetoothHeadsetBinder(this); + return new HeadsetServiceBinder(this); } @Override @@ -516,303 +510,6 @@ public class HeadsetService extends ProfileService { } } - /** Handlers for incoming service calls */ - @VisibleForTesting - static class BluetoothHeadsetBinder extends IBluetoothHeadset.Stub - implements IProfileServiceBinder { - private HeadsetService mService; - - BluetoothHeadsetBinder(HeadsetService svc) { - mService = svc; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private HeadsetService getService(AttributionSource source) { - // Cache mService because it can change while getService is called - HeadsetService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - return service; - } - - @Override - public boolean connect(BluetoothDevice device, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null); - return service.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - return service.disconnect(device); - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getConnectedDevices(); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return STATE_DISCONNECTED; - } - - return service.getConnectionState(device); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null); - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return CONNECTION_POLICY_UNKNOWN; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getConnectionPolicy(device); - } - - @Override - public boolean isNoiseReductionSupported(BluetoothDevice device, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - return service.isNoiseReductionSupported(device); - } - - @Override - public boolean isVoiceRecognitionSupported( - BluetoothDevice device, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - return service.isVoiceRecognitionSupported(device); - } - - @Override - public boolean startVoiceRecognition(BluetoothDevice device, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - requireNonNull(device); - - return service.startVoiceRecognition(device); - } - - @Override - public boolean stopVoiceRecognition(BluetoothDevice device, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - return service.stopVoiceRecognition(device); - } - - @Override - public boolean isAudioConnected(BluetoothDevice device, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - return service.isAudioConnected(device); - } - - @Override - public int getAudioState(BluetoothDevice device, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return BluetoothHeadset.STATE_AUDIO_DISCONNECTED; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getAudioState(device); - } - - @Override - public int connectAudio(AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.connectAudio(); - } - - @Override - public int disconnectAudio(AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.disconnectAudio(); - } - - @Override - public void setAudioRouteAllowed(boolean allowed, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.setAudioRouteAllowed(allowed); - } - - @Override - public boolean getAudioRouteAllowed(AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getAudioRouteAllowed(); - } - - @Override - public void setForceScoAudio(boolean forced, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return; - } - - service.setForceScoAudio(forced); - } - - @Override - public boolean startScoUsingVirtualVoiceCall(AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null); - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.startScoUsingVirtualVoiceCall(); - } - - @Override - public boolean stopScoUsingVirtualVoiceCall(AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null); - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.stopScoUsingVirtualVoiceCall(); - } - - @Override - public boolean sendVendorSpecificResultCode( - BluetoothDevice device, String command, String arg, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - return service.sendVendorSpecificResultCode(device, command, arg); - } - - @Override - public boolean setActiveDevice(BluetoothDevice device, AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null); - - return service.setActiveDevice(device); - } - - @Override - public BluetoothDevice getActiveDevice(AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return null; - } - - return service.getActiveDevice(); - } - - @Override - public boolean isInbandRingingEnabled(AttributionSource source) { - HeadsetService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.isInbandRingingEnabled(); - } - } - // API methods public static synchronized HeadsetService getHeadsetService() { if (sHeadsetService == null) { @@ -947,7 +644,6 @@ public class HeadsetService extends ProfileService { * @param states an array of states from {@link BluetoothProfile} * @return a list of devices matching the array of connection states */ - @VisibleForTesting public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { ArrayList<BluetoothDevice> devices = new ArrayList<>(); synchronized (mStateMachines) { @@ -1659,7 +1355,6 @@ public class HeadsetService extends ProfileService { } } - @VisibleForTesting boolean startScoUsingVirtualVoiceCall() { Log.i(TAG, "startScoUsingVirtualVoiceCall: " + Utils.getUidPidString()); synchronized (mStateMachines) { @@ -2134,8 +1829,7 @@ public class HeadsetService extends ProfileService { } } - private boolean sendVendorSpecificResultCode( - BluetoothDevice device, String command, String arg) { + boolean sendVendorSpecificResultCode(BluetoothDevice device, String command, String arg) { synchronized (mStateMachines) { final HeadsetStateMachine stateMachine = mStateMachines.get(device); if (stateMachine == null) { @@ -2396,9 +2090,10 @@ public class HeadsetService extends ProfileService { // Do it here because some controllers cannot handle SCO and CIS // co-existence see {@link LeAudioService#setInactiveForHfpHandover} LeAudioService leAudioService = mFactory.getLeAudioService(); - boolean isLeAudioConnectedDeviceNotActive = leAudioService != null - && !leAudioService.getConnectedDevices().isEmpty() - && leAudioService.getActiveDevices().get(0) == null; + boolean isLeAudioConnectedDeviceNotActive = + leAudioService != null + && !leAudioService.getConnectedDevices().isEmpty() + && leAudioService.getActiveDevices().get(0) == null; // usually controller limitation cause CONNECTING -> DISCONNECTED, so only // resume LE audio active device if it is HFP audio only and SCO disconnected if (fromState != BluetoothHeadset.STATE_AUDIO_CONNECTING diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetServiceBinder.java b/android/app/src/com/android/bluetooth/hfp/HeadsetServiceBinder.java new file mode 100644 index 0000000000..d93ad5dc5f --- /dev/null +++ b/android/app/src/com/android/bluetooth/hfp/HeadsetServiceBinder.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2022 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.hfp; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static android.Manifest.permission.MODIFY_PHONE_STATE; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import static java.util.Objects.requireNonNull; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothStatusCodes; +import android.bluetooth.IBluetoothHeadset; +import android.content.AttributionSource; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Collections; +import java.util.List; + +class HeadsetServiceBinder extends IBluetoothHeadset.Stub implements IProfileServiceBinder { + private static final String TAG = HeadsetServiceBinder.class.getSimpleName(); + + private HeadsetService mService; + + HeadsetServiceBinder(HeadsetService svc) { + mService = svc; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private HeadsetService getService(AttributionSource source) { + HeadsetService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + return service; + } + + @Override + public boolean connect(BluetoothDevice device, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null); + return service.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + return service.disconnect(device); + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getConnectedDevices(); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return STATE_DISCONNECTED; + } + return service.getConnectionState(device); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null); + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return CONNECTION_POLICY_UNKNOWN; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectionPolicy(device); + } + + @Override + public boolean isNoiseReductionSupported(BluetoothDevice device, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + return service.isNoiseReductionSupported(device); + } + + @Override + public boolean isVoiceRecognitionSupported(BluetoothDevice device, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + return service.isVoiceRecognitionSupported(device); + } + + @Override + public boolean startVoiceRecognition(BluetoothDevice device, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + + requireNonNull(device); + return service.startVoiceRecognition(device); + } + + @Override + public boolean stopVoiceRecognition(BluetoothDevice device, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + return service.stopVoiceRecognition(device); + } + + @Override + public boolean isAudioConnected(BluetoothDevice device, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + return service.isAudioConnected(device); + } + + @Override + public int getAudioState(BluetoothDevice device, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return BluetoothHeadset.STATE_AUDIO_DISCONNECTED; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getAudioState(device); + } + + @Override + public int connectAudio(AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.connectAudio(); + } + + @Override + public int disconnectAudio(AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.disconnectAudio(); + } + + @Override + public void setAudioRouteAllowed(boolean allowed, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.setAudioRouteAllowed(allowed); + } + + @Override + public boolean getAudioRouteAllowed(AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getAudioRouteAllowed(); + } + + @Override + public void setForceScoAudio(boolean forced, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return; + } + service.setForceScoAudio(forced); + } + + @Override + public boolean startScoUsingVirtualVoiceCall(AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null); + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + + return service.startScoUsingVirtualVoiceCall(); + } + + @Override + public boolean stopScoUsingVirtualVoiceCall(AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null); + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + + return service.stopScoUsingVirtualVoiceCall(); + } + + @Override + public boolean sendVendorSpecificResultCode( + BluetoothDevice device, String command, String arg, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + return service.sendVendorSpecificResultCode(device, command, arg); + } + + @Override + public boolean setActiveDevice(BluetoothDevice device, AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null); + return service.setActiveDevice(device); + } + + @Override + public BluetoothDevice getActiveDevice(AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return null; + } + return service.getActiveDevice(); + } + + @Override + public boolean isInbandRingingEnabled(AttributionSource source) { + HeadsetService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.isInbandRingingEnabled(); + } +} diff --git a/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java b/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java index 9c0e416168..7d3a5ff43f 100644 --- a/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java +++ b/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java @@ -16,11 +16,8 @@ package com.android.bluetooth.hfpclient; -import static android.Manifest.permission.BLUETOOTH_CONNECT; -import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; -import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; import static android.bluetooth.BluetoothProfile.STATE_CONNECTING; import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; @@ -28,15 +25,11 @@ import static android.content.pm.PackageManager.FEATURE_WATCH; import static java.util.Objects.requireNonNull; -import android.annotation.RequiresPermission; import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothHeadsetClient; import android.bluetooth.BluetoothHeadsetClientCall; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothSinkAudioPolicy; import android.bluetooth.BluetoothStatusCodes; -import android.bluetooth.IBluetoothHeadsetClient; -import android.content.AttributionSource; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -58,7 +51,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -145,7 +137,7 @@ public class HeadsetClientService extends ProfileService { @Override public IProfileServiceBinder initBinder() { - return new BluetoothHeadsetClientBinder(this); + return new HeadsetClientServiceBinder(this); } @Override @@ -290,340 +282,6 @@ public class HeadsetClientService extends ProfileService { call.isInBandRing()); } - /** Handlers for incoming service calls */ - @VisibleForTesting - static class BluetoothHeadsetClientBinder extends IBluetoothHeadsetClient.Stub - implements IProfileServiceBinder { - private HeadsetClientService mService; - - BluetoothHeadsetClientBinder(HeadsetClientService svc) { - mService = svc; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private HeadsetClientService getService(AttributionSource source) { - // Cache mService because it can change while getService is called - HeadsetClientService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - - return service; - } - - @Override - public boolean connect(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.disconnect(device); - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - service.enforceCallingPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getConnectedDevices(); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - service.enforceCallingPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return STATE_DISCONNECTED; - } - - service.enforceCallingPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getConnectionState(device); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingPermission(BLUETOOTH_PRIVILEGED, null); - - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return CONNECTION_POLICY_UNKNOWN; - } - - service.enforceCallingPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getConnectionPolicy(device); - } - - @Override - public boolean startVoiceRecognition(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.startVoiceRecognition(device); - } - - @Override - public boolean stopVoiceRecognition(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.stopVoiceRecognition(device); - } - - @Override - public int getAudioState(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED; - } - - return service.getAudioState(device); - } - - @Override - public void setAudioRouteAllowed( - BluetoothDevice device, boolean allowed, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - Log.w(TAG, "Service handle is null for setAudioRouteAllowed!"); - return; - } - - service.setAudioRouteAllowed(device, allowed); - } - - @Override - public boolean getAudioRouteAllowed(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - Log.w(TAG, "Service handle is null for getAudioRouteAllowed!"); - return false; - } - - return service.getAudioRouteAllowed(device); - } - - @Override - public boolean connectAudio(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.connectAudio(device); - } - - @Override - public boolean disconnectAudio(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.disconnectAudio(device); - } - - @Override - public boolean acceptCall(BluetoothDevice device, int flag, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.acceptCall(device, flag); - } - - @Override - public boolean rejectCall(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.rejectCall(device); - } - - @Override - public boolean holdCall(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.holdCall(device); - } - - @Override - public boolean terminateCall( - BluetoothDevice device, BluetoothHeadsetClientCall call, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - Log.w(TAG, "service is null"); - return false; - } - - return service.terminateCall(device, call != null ? call.getUUID() : null); - } - - @Override - public boolean explicitCallTransfer(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.explicitCallTransfer(device); - } - - @Override - public boolean enterPrivateMode( - BluetoothDevice device, int index, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.enterPrivateMode(device, index); - } - - @Override - public BluetoothHeadsetClientCall dial( - BluetoothDevice device, String number, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return null; - } - - return toLegacyCall(service.dial(device, number)); - } - - @Override - public List<BluetoothHeadsetClientCall> getCurrentCalls( - BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - List<BluetoothHeadsetClientCall> currentCalls = new ArrayList<>(); - if (service == null) { - return currentCalls; - } - - List<HfpClientCall> calls = service.getCurrentCalls(device); - if (calls != null) { - for (HfpClientCall call : calls) { - currentCalls.add(toLegacyCall(call)); - } - } - return currentCalls; - } - - @Override - public boolean sendDTMF(BluetoothDevice device, byte code, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.sendDTMF(device, code); - } - - @Override - public boolean getLastVoiceTagNumber(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.getLastVoiceTagNumber(device); - } - - @Override - public Bundle getCurrentAgEvents(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return null; - } - - service.enforceCallingPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getCurrentAgEvents(device); - } - - @Override - public boolean sendVendorAtCommand( - BluetoothDevice device, int vendorId, String atCommand, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return false; - } - - return service.sendVendorAtCommand(device, vendorId, atCommand); - } - - @Override - public Bundle getCurrentAgFeatures(BluetoothDevice device, AttributionSource source) { - HeadsetClientService service = getService(source); - if (service == null) { - return null; - } - - return service.getCurrentAgFeaturesBundle(device); - } - } - // API methods public static synchronized HeadsetClientService getHeadsetClientService() { if (sHeadsetClientService == null) { diff --git a/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientServiceBinder.java b/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientServiceBinder.java new file mode 100644 index 0000000000..3232fccd4f --- /dev/null +++ b/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientServiceBinder.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2025 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.hfpclient; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadsetClient; +import android.bluetooth.BluetoothHeadsetClientCall; +import android.bluetooth.IBluetoothHeadsetClient; +import android.content.AttributionSource; +import android.os.Bundle; +import android.util.Log; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Handlers for incoming service calls */ +class HeadsetClientServiceBinder extends IBluetoothHeadsetClient.Stub + implements IProfileServiceBinder { + private static final String TAG = HeadsetClientServiceBinder.class.getSimpleName(); + + private HeadsetClientService mService; + + HeadsetClientServiceBinder(HeadsetClientService svc) { + mService = svc; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private HeadsetClientService getService(AttributionSource source) { + HeadsetClientService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + + return service; + } + + @Override + public boolean connect(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.disconnect(device); + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + + service.enforceCallingPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectedDevices(); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + + service.enforceCallingPermission(BLUETOOTH_PRIVILEGED, null); + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return STATE_DISCONNECTED; + } + + service.enforceCallingPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectionState(device); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingPermission(BLUETOOTH_PRIVILEGED, null); + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return CONNECTION_POLICY_UNKNOWN; + } + + service.enforceCallingPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectionPolicy(device); + } + + @Override + public boolean startVoiceRecognition(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.startVoiceRecognition(device); + } + + @Override + public boolean stopVoiceRecognition(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.stopVoiceRecognition(device); + } + + @Override + public int getAudioState(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED; + } + return service.getAudioState(device); + } + + @Override + public void setAudioRouteAllowed( + BluetoothDevice device, boolean allowed, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + Log.w(TAG, "Service handle is null for setAudioRouteAllowed!"); + return; + } + service.setAudioRouteAllowed(device, allowed); + } + + @Override + public boolean getAudioRouteAllowed(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + Log.w(TAG, "Service handle is null for getAudioRouteAllowed!"); + return false; + } + return service.getAudioRouteAllowed(device); + } + + @Override + public boolean connectAudio(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.connectAudio(device); + } + + @Override + public boolean disconnectAudio(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.disconnectAudio(device); + } + + @Override + public boolean acceptCall(BluetoothDevice device, int flag, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.acceptCall(device, flag); + } + + @Override + public boolean rejectCall(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.rejectCall(device); + } + + @Override + public boolean holdCall(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.holdCall(device); + } + + @Override + public boolean terminateCall( + BluetoothDevice device, BluetoothHeadsetClientCall call, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + Log.w(TAG, "service is null"); + return false; + } + return service.terminateCall(device, call != null ? call.getUUID() : null); + } + + @Override + public boolean explicitCallTransfer(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.explicitCallTransfer(device); + } + + @Override + public boolean enterPrivateMode(BluetoothDevice device, int index, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.enterPrivateMode(device, index); + } + + @Override + public BluetoothHeadsetClientCall dial( + BluetoothDevice device, String number, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return null; + } + return HeadsetClientService.toLegacyCall(service.dial(device, number)); + } + + @Override + public List<BluetoothHeadsetClientCall> getCurrentCalls( + BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + List<BluetoothHeadsetClientCall> currentCalls = new ArrayList<>(); + if (service == null) { + return currentCalls; + } + + List<HfpClientCall> calls = service.getCurrentCalls(device); + if (calls != null) { + for (HfpClientCall call : calls) { + currentCalls.add(HeadsetClientService.toLegacyCall(call)); + } + } + return currentCalls; + } + + @Override + public boolean sendDTMF(BluetoothDevice device, byte code, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.sendDTMF(device, code); + } + + @Override + public boolean getLastVoiceTagNumber(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.getLastVoiceTagNumber(device); + } + + @Override + public Bundle getCurrentAgEvents(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return null; + } + + service.enforceCallingPermission(BLUETOOTH_PRIVILEGED, null); + return service.getCurrentAgEvents(device); + } + + @Override + public boolean sendVendorAtCommand( + BluetoothDevice device, int vendorId, String atCommand, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return false; + } + return service.sendVendorAtCommand(device, vendorId, atCommand); + } + + @Override + public Bundle getCurrentAgFeatures(BluetoothDevice device, AttributionSource source) { + HeadsetClientService service = getService(source); + if (service == null) { + return null; + } + return service.getCurrentAgFeaturesBundle(device); + } +} diff --git a/android/app/src/com/android/bluetooth/hid/HidDeviceService.java b/android/app/src/com/android/bluetooth/hid/HidDeviceService.java index 2c2f11940e..d3d84d9630 100644 --- a/android/app/src/com/android/bluetooth/hid/HidDeviceService.java +++ b/android/app/src/com/android/bluetooth/hid/HidDeviceService.java @@ -34,9 +34,7 @@ import android.bluetooth.BluetoothHidDevice; import android.bluetooth.BluetoothHidDeviceAppQosSettings; import android.bluetooth.BluetoothHidDeviceAppSdpSettings; import android.bluetooth.BluetoothProfile; -import android.bluetooth.IBluetoothHidDevice; import android.bluetooth.IBluetoothHidDeviceCallback; -import android.content.AttributionSource; import android.content.Intent; import android.os.Binder; import android.os.Handler; @@ -50,15 +48,12 @@ import android.util.Log; import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; -import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.storage.DatabaseManager; import com.android.internal.annotations.VisibleForTesting; import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; @@ -328,204 +323,9 @@ public class HidDeviceService extends ProfileService { } }; - @VisibleForTesting - static class BluetoothHidDeviceBinder extends IBluetoothHidDevice.Stub - implements IProfileServiceBinder { - - private static final String TAG = BluetoothHidDeviceBinder.class.getSimpleName(); - - private HidDeviceService mService; - - BluetoothHidDeviceBinder(HidDeviceService service) { - mService = service; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private HidDeviceService getService(AttributionSource source) { - // Cache mService because it can change while getService is called - HidDeviceService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - return service; - } - - @Override - public boolean registerApp( - BluetoothHidDeviceAppSdpSettings sdp, - BluetoothHidDeviceAppQosSettings inQos, - BluetoothHidDeviceAppQosSettings outQos, - IBluetoothHidDeviceCallback callback, - AttributionSource source) { - Log.d(TAG, "registerApp()"); - - HidDeviceService service = getService(source); - if (service == null) { - return false; - } - - return service.registerApp(sdp, inQos, outQos, callback); - } - - @Override - public boolean unregisterApp(AttributionSource source) { - Log.d(TAG, "unregisterApp()"); - - HidDeviceService service = getService(source); - if (service == null) { - return false; - } - - return service.unregisterApp(); - } - - @Override - public boolean sendReport( - BluetoothDevice device, int id, byte[] data, AttributionSource source) { - Log.d(TAG, "sendReport(): device=" + device + " id=" + id); - - HidDeviceService service = getService(source); - if (service == null) { - return false; - } - - return service.sendReport(device, id, data); - } - - @Override - public boolean replyReport( - BluetoothDevice device, byte type, byte id, byte[] data, AttributionSource source) { - Log.d(TAG, "replyReport(): device=" + device + " type=" + type + " id=" + id); - - HidDeviceService service = getService(source); - if (service == null) { - return false; - } - - return service.replyReport(device, type, id, data); - } - - @Override - public boolean unplug(BluetoothDevice device, AttributionSource source) { - Log.d(TAG, "unplug(): device=" + device); - - HidDeviceService service = getService(source); - if (service == null) { - return false; - } - - return service.unplug(device); - } - - @Override - public boolean connect(BluetoothDevice device, AttributionSource source) { - Log.d(TAG, "connect(): device=" + device); - - HidDeviceService service = getService(source); - if (service == null) { - return false; - } - - return service.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device, AttributionSource source) { - Log.d(TAG, "disconnect(): device=" + device); - - HidDeviceService service = getService(source); - if (service == null) { - return false; - } - - return service.disconnect(device); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - Log.d( - TAG, - "setConnectionPolicy():" - + (" device=" + device) - + (" connectionPolicy=" + connectionPolicy)); - - HidDeviceService service = getService(source); - if (service == null) { - return false; - } - - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public boolean reportError(BluetoothDevice device, byte error, AttributionSource source) { - Log.d(TAG, "reportError(): device=" + device + " error=" + error); - - HidDeviceService service = getService(source); - if (service == null) { - return false; - } - - return service.reportError(device, error); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - Log.d(TAG, "getConnectionState(): device=" + device); - - HidDeviceService service = getService(source); - if (service == null) { - return BluetoothHidDevice.STATE_DISCONNECTED; - } - - return service.getConnectionState(device); - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - Log.d(TAG, "getConnectedDevices()"); - - return getDevicesMatchingConnectionStates(new int[] {STATE_CONNECTED}, source); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - Log.d(TAG, "getDevicesMatchingConnectionStates(): states=" + Arrays.toString(states)); - - HidDeviceService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public String getUserAppName(AttributionSource source) { - HidDeviceService service = getService(source); - if (service == null) { - return ""; - } - return service.getUserAppName(); - } - } - @Override protected IProfileServiceBinder initBinder() { - return new BluetoothHidDeviceBinder(this); + return new HidDeviceServiceBinder(this); } private boolean checkDevice(BluetoothDevice device) { diff --git a/android/app/src/com/android/bluetooth/hid/HidDeviceServiceBinder.java b/android/app/src/com/android/bluetooth/hid/HidDeviceServiceBinder.java new file mode 100644 index 0000000000..0cd0e1d301 --- /dev/null +++ b/android/app/src/com/android/bluetooth/hid/HidDeviceServiceBinder.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2025 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.hid; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHidDevice; +import android.bluetooth.BluetoothHidDeviceAppQosSettings; +import android.bluetooth.BluetoothHidDeviceAppSdpSettings; +import android.bluetooth.IBluetoothHidDevice; +import android.bluetooth.IBluetoothHidDeviceCallback; +import android.content.AttributionSource; +import android.util.Log; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +class HidDeviceServiceBinder extends IBluetoothHidDevice.Stub implements IProfileServiceBinder { + private static final String TAG = HidDeviceServiceBinder.class.getSimpleName(); + + private HidDeviceService mService; + + HidDeviceServiceBinder(HidDeviceService service) { + mService = service; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private HidDeviceService getService(AttributionSource source) { + // Cache mService because it can change while getService is called + HidDeviceService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + return service; + } + + @Override + public boolean registerApp( + BluetoothHidDeviceAppSdpSettings sdp, + BluetoothHidDeviceAppQosSettings inQos, + BluetoothHidDeviceAppQosSettings outQos, + IBluetoothHidDeviceCallback callback, + AttributionSource source) { + Log.d(TAG, "registerApp()"); + + HidDeviceService service = getService(source); + if (service == null) { + return false; + } + return service.registerApp(sdp, inQos, outQos, callback); + } + + @Override + public boolean unregisterApp(AttributionSource source) { + Log.d(TAG, "unregisterApp()"); + + HidDeviceService service = getService(source); + if (service == null) { + return false; + } + return service.unregisterApp(); + } + + @Override + public boolean sendReport( + BluetoothDevice device, int id, byte[] data, AttributionSource source) { + Log.d(TAG, "sendReport(): device=" + device + " id=" + id); + + HidDeviceService service = getService(source); + if (service == null) { + return false; + } + return service.sendReport(device, id, data); + } + + @Override + public boolean replyReport( + BluetoothDevice device, byte type, byte id, byte[] data, AttributionSource source) { + Log.d(TAG, "replyReport(): device=" + device + " type=" + type + " id=" + id); + + HidDeviceService service = getService(source); + if (service == null) { + return false; + } + return service.replyReport(device, type, id, data); + } + + @Override + public boolean unplug(BluetoothDevice device, AttributionSource source) { + Log.d(TAG, "unplug(): device=" + device); + + HidDeviceService service = getService(source); + if (service == null) { + return false; + } + return service.unplug(device); + } + + @Override + public boolean connect(BluetoothDevice device, AttributionSource source) { + Log.d(TAG, "connect(): device=" + device); + + HidDeviceService service = getService(source); + if (service == null) { + return false; + } + return service.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device, AttributionSource source) { + Log.d(TAG, "disconnect(): device=" + device); + + HidDeviceService service = getService(source); + if (service == null) { + return false; + } + return service.disconnect(device); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + Log.d( + TAG, + "setConnectionPolicy():" + + (" device=" + device) + + (" connectionPolicy=" + connectionPolicy)); + + HidDeviceService service = getService(source); + if (service == null) { + return false; + } + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public boolean reportError(BluetoothDevice device, byte error, AttributionSource source) { + Log.d(TAG, "reportError(): device=" + device + " error=" + error); + + HidDeviceService service = getService(source); + if (service == null) { + return false; + } + return service.reportError(device, error); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + Log.d(TAG, "getConnectionState(): device=" + device); + + HidDeviceService service = getService(source); + if (service == null) { + return BluetoothHidDevice.STATE_DISCONNECTED; + } + return service.getConnectionState(device); + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + Log.d(TAG, "getConnectedDevices()"); + + return getDevicesMatchingConnectionStates(new int[] {STATE_CONNECTED}, source); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + Log.d(TAG, "getDevicesMatchingConnectionStates(): states=" + Arrays.toString(states)); + + HidDeviceService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public String getUserAppName(AttributionSource source) { + HidDeviceService service = getService(source); + if (service == null) { + return ""; + } + return service.getUserAppName(); + } +} diff --git a/android/app/src/com/android/bluetooth/hid/HidHostService.java b/android/app/src/com/android/bluetooth/hid/HidHostService.java index 3a29ed5b62..1eac6ed336 100644 --- a/android/app/src/com/android/bluetooth/hid/HidHostService.java +++ b/android/app/src/com/android/bluetooth/hid/HidHostService.java @@ -17,7 +17,6 @@ package com.android.bluetooth.hid; import static android.Manifest.permission.BLUETOOTH_CONNECT; -import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; @@ -27,14 +26,11 @@ import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING; import static java.util.Objects.requireNonNull; -import android.annotation.RequiresPermission; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHidHost; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProtoEnums; import android.bluetooth.BluetoothUuid; -import android.bluetooth.IBluetoothHidHost; -import android.content.AttributionSource; import android.content.Intent; import android.os.Bundle; import android.os.Handler; @@ -145,7 +141,7 @@ public class HidHostService extends ProfileService { @Override public IProfileServiceBinder initBinder() { - return new BluetoothHidHostBinder(this); + return new HidHostServiceBinder(this); } @Override @@ -713,207 +709,6 @@ public class HidHostService extends ProfileService { return true; } - @VisibleForTesting - static class BluetoothHidHostBinder extends IBluetoothHidHost.Stub - implements IProfileServiceBinder { - private HidHostService mService; - - BluetoothHidHostBinder(HidHostService svc) { - mService = svc; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private HidHostService getService(AttributionSource source) { - // Cache mService because it can change while getService is called - HidHostService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - - return service; - } - - @Override - public boolean connect(BluetoothDevice device, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return false; - } - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return false; - } - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.disconnect(device); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return STATE_DISCONNECTED; - } - return service.getConnectionState(device); - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - return getDevicesMatchingConnectionStates(new int[] {STATE_CONNECTED}, source); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return false; - } - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return CONNECTION_POLICY_UNKNOWN; - } - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getConnectionPolicy(device); - } - - @Override - public boolean setPreferredTransport( - BluetoothDevice device, int transport, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return false; - } - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.setPreferredTransport(device, transport); - } - - @Override - public int getPreferredTransport(BluetoothDevice device, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return BluetoothDevice.TRANSPORT_AUTO; - } - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getPreferredTransport(device); - } - - /* The following APIs regarding test app for compliance */ - @Override - public boolean getProtocolMode(BluetoothDevice device, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return false; - } - return service.getProtocolMode(device); - } - - @Override - public boolean virtualUnplug(BluetoothDevice device, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return false; - } - return service.virtualUnplug(device); - } - - @Override - public boolean setProtocolMode( - BluetoothDevice device, int protocolMode, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return false; - } - return service.setProtocolMode(device, protocolMode); - } - - @Override - public boolean getReport( - BluetoothDevice device, - byte reportType, - byte reportId, - int bufferSize, - AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return false; - } - return service.getReport(device, reportType, reportId, bufferSize); - } - - @Override - public boolean setReport( - BluetoothDevice device, byte reportType, String report, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return false; - } - return service.setReport(device, reportType, report); - } - - @Override - public boolean sendData(BluetoothDevice device, String report, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return false; - } - return service.sendData(device, report); - } - - @Override - public boolean setIdleTime( - BluetoothDevice device, byte idleTime, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return false; - } - return service.setIdleTime(device, idleTime); - } - - @Override - public boolean getIdleTime(BluetoothDevice device, AttributionSource source) { - HidHostService service = getService(source); - if (service == null) { - return false; - } - return service.getIdleTime(device); - } - } - ; - // APIs /** @@ -982,7 +777,6 @@ public class HidHostService extends ProfileService { return STATE_DISCONNECTED; } - @VisibleForTesting List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { Log.d(TAG, "getDevicesMatchingConnectionStates()"); return mInputDevices.entrySet().stream() @@ -1073,7 +867,6 @@ public class HidHostService extends ProfileService { /** * @see BluetoothHidHost#getPreferredTransport */ - @VisibleForTesting int getPreferredTransport(BluetoothDevice device) { Log.d(TAG, "getPreferredTransport: device=" + device); @@ -1082,7 +875,6 @@ public class HidHostService extends ProfileService { } /* The following APIs regarding test app for compliance */ - @VisibleForTesting boolean getProtocolMode(BluetoothDevice device) { Log.d(TAG, "getProtocolMode: device=" + device); int state = this.getConnectionState(device); @@ -1094,7 +886,6 @@ public class HidHostService extends ProfileService { return true; } - @VisibleForTesting boolean virtualUnplug(BluetoothDevice device) { Log.d(TAG, "virtualUnplug: device=" + device); int state = this.getConnectionState(device); @@ -1106,7 +897,6 @@ public class HidHostService extends ProfileService { return true; } - @VisibleForTesting boolean setProtocolMode(BluetoothDevice device, int protocolMode) { Log.d(TAG, "setProtocolMode: device=" + device); int state = this.getConnectionState(device); @@ -1120,7 +910,6 @@ public class HidHostService extends ProfileService { return true; } - @VisibleForTesting boolean getReport(BluetoothDevice device, byte reportType, byte reportId, int bufferSize) { Log.d(TAG, "getReport: device=" + device); int state = this.getConnectionState(device); @@ -1138,7 +927,6 @@ public class HidHostService extends ProfileService { return true; } - @VisibleForTesting boolean setReport(BluetoothDevice device, byte reportType, String report) { Log.d(TAG, "setReport: device=" + device); int state = this.getConnectionState(device); @@ -1155,7 +943,6 @@ public class HidHostService extends ProfileService { return true; } - @VisibleForTesting boolean sendData(BluetoothDevice device, String report) { Log.d(TAG, "sendData: device=" + device); int state = this.getConnectionState(device); @@ -1393,8 +1180,9 @@ public class HidHostService extends ProfileService { // Allow this connection only if the device is bonded. Any attempt to connect // while bonding would potentially lead to an unauthorized connection. if (bondState != BluetoothDevice.BOND_BONDED) { - Log.w(TAG, "okToConnect: return false, device=" + device + " bondState=" - + bondState); + Log.w( + TAG, + "okToConnect: return false, device=" + device + " bondState=" + bondState); return false; } } diff --git a/android/app/src/com/android/bluetooth/hid/HidHostServiceBinder.java b/android/app/src/com/android/bluetooth/hid/HidHostServiceBinder.java new file mode 100644 index 0000000000..3295c83f01 --- /dev/null +++ b/android/app/src/com/android/bluetooth/hid/HidHostServiceBinder.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2025 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.hid; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; +import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.IBluetoothHidHost; +import android.content.AttributionSource; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Collections; +import java.util.List; + +class HidHostServiceBinder extends IBluetoothHidHost.Stub implements IProfileServiceBinder { + private static final String TAG = HidHostServiceBinder.class.getSimpleName(); + + private HidHostService mService; + + HidHostServiceBinder(HidHostService svc) { + mService = svc; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private HidHostService getService(AttributionSource source) { + HidHostService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + + return service; + } + + @Override + public boolean connect(BluetoothDevice device, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return false; + } + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return false; + } + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.disconnect(device); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return STATE_DISCONNECTED; + } + return service.getConnectionState(device); + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + return getDevicesMatchingConnectionStates(new int[] {STATE_CONNECTED}, source); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return false; + } + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return CONNECTION_POLICY_UNKNOWN; + } + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectionPolicy(device); + } + + @Override + public boolean setPreferredTransport( + BluetoothDevice device, int transport, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return false; + } + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.setPreferredTransport(device, transport); + } + + @Override + public int getPreferredTransport(BluetoothDevice device, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return BluetoothDevice.TRANSPORT_AUTO; + } + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getPreferredTransport(device); + } + + /* The following APIs regarding test app for compliance */ + @Override + public boolean getProtocolMode(BluetoothDevice device, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return false; + } + return service.getProtocolMode(device); + } + + @Override + public boolean virtualUnplug(BluetoothDevice device, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return false; + } + return service.virtualUnplug(device); + } + + @Override + public boolean setProtocolMode( + BluetoothDevice device, int protocolMode, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return false; + } + return service.setProtocolMode(device, protocolMode); + } + + @Override + public boolean getReport( + BluetoothDevice device, + byte reportType, + byte reportId, + int bufferSize, + AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return false; + } + return service.getReport(device, reportType, reportId, bufferSize); + } + + @Override + public boolean setReport( + BluetoothDevice device, byte reportType, String report, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return false; + } + return service.setReport(device, reportType, report); + } + + @Override + public boolean sendData(BluetoothDevice device, String report, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return false; + } + return service.sendData(device, report); + } + + @Override + public boolean setIdleTime(BluetoothDevice device, byte idleTime, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return false; + } + return service.setIdleTime(device, idleTime); + } + + @Override + public boolean getIdleTime(BluetoothDevice device, AttributionSource source) { + HidHostService service = getService(source); + if (service == null) { + return false; + } + return service.getIdleTime(device); + } +} diff --git a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java index de152f0ae1..4f0d215e5b 100644 --- a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java @@ -36,7 +36,6 @@ import static com.android.modules.utils.build.SdkLevel.isAtLeastU; import static java.util.Objects.requireNonNull; -import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.bluetooth.BluetoothAdapter; @@ -61,7 +60,6 @@ import android.bluetooth.le.IScannerCallback; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; -import android.content.AttributionSource; import android.content.Context; import android.content.Intent; import android.media.AudioDeviceCallback; @@ -621,7 +619,7 @@ public class LeAudioService extends ProfileService { @Override protected IProfileServiceBinder initBinder() { - return new BluetoothLeAudioBinder(this); + return new LeAudioServiceBinder(this); } public static boolean isEnabled() { @@ -805,12 +803,12 @@ public class LeAudioService extends ProfileService { mLeAudioBroadcasterNativeInterface.ifPresent(i -> i.cleanup()); - try { - mStateMachinesThread.quitSafely(); - mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS); - } catch (InterruptedException e) { - // Do not rethrow as we are shutting down anyway - } + try { + mStateMachinesThread.quitSafely(); + mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS); + } catch (InterruptedException e) { + // Do not rethrow as we are shutting down anyway + } mAudioManager.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback); @@ -1834,8 +1832,10 @@ public class LeAudioService extends ProfileService { if (device != null && mActiveAudioInDevice != null) { LeAudioDeviceDescriptor deviceDescriptor = getDeviceDescriptor(mActiveAudioInDevice); if (deviceDescriptor == null) { - Log.e(TAG, "updateActiveInDevice: No valid descriptor for device: " - + mActiveAudioInDevice); + Log.e( + TAG, + "updateActiveInDevice: No valid descriptor for device: " + + mActiveAudioInDevice); return false; } @@ -1900,8 +1900,10 @@ public class LeAudioService extends ProfileService { if (device != null && mActiveAudioOutDevice != null) { LeAudioDeviceDescriptor deviceDescriptor = getDeviceDescriptor(mActiveAudioOutDevice); if (deviceDescriptor == null) { - Log.e(TAG, "updateActiveOutDevice: No valid descriptor for device: " - + mActiveAudioOutDevice); + Log.e( + TAG, + "updateActiveOutDevice: No valid descriptor for device: " + + mActiveAudioOutDevice); return false; } @@ -2129,9 +2131,7 @@ public class LeAudioService extends ProfileService { Log.d(TAG, "Scanner is not running (mScannerId=" + mScannerId + ")"); return; } - mAdapterService - .getBluetoothScanController() - .stopScanInternal(mScannerId); + mAdapterService.getBluetoothScanController().stopScanInternal(mScannerId); mAdapterService.getBluetoothScanController().unregisterScannerInternal(mScannerId); mScannerId = SCANNER_NOT_INITIALIZED; @@ -3226,8 +3226,10 @@ public class LeAudioService extends ProfileService { boolean ringtoneContextAvailable = false; if (groupDescriptor.mAvailableContexts != null) { - ringtoneContextAvailable = ((groupDescriptor.mAvailableContexts & - BluetoothLeAudio.CONTEXT_TYPE_RINGTONE) != 0); + ringtoneContextAvailable = + ((groupDescriptor.mAvailableContexts + & BluetoothLeAudio.CONTEXT_TYPE_RINGTONE) + != 0); } /* Enables in-band ringtone only for the currently active device or @@ -3724,9 +3726,7 @@ public class LeAudioService extends ProfileService { } else if (isInitial) { Log.i( TAG, - " New group " - + groupId - + " with no context types available"); + " New group " + groupId + " with no context types available"); descriptor.mInactivatedDueToContextType = true; } return; @@ -3794,9 +3794,7 @@ public class LeAudioService extends ProfileService { { LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId); if (descriptor == null) { - Log.e( - TAG, - "deviceDisconnected: no descriptors for group: " + groupId); + Log.e(TAG, "deviceDisconnected: no descriptors for group: " + groupId); return; } @@ -4349,7 +4347,8 @@ public class LeAudioService extends ProfileService { if (getConnectedPeerDevices(groupId).isEmpty()) { descriptor.mIsConnected = false; descriptor.mAutoActiveModeEnabled = true; - descriptor.mAvailableContexts = Flags.leaudioUnicastNoAvailableContexts() ? null : 0; + descriptor.mAvailableContexts = + Flags.leaudioUnicastNoAvailableContexts() ? null : 0; if (descriptor.isActive()) { /* Notify Native layer */ removeActiveDevice(hasFallbackDevice); @@ -5524,12 +5523,15 @@ public class LeAudioService extends ProfileService { * device should be removed from active devices. */ int newDirection = AUDIO_DIRECTION_NONE; - int oldDirection = oldFallbackGroupDescriptor != null - ? oldFallbackGroupDescriptor.mDirection : AUDIO_DIRECTION_NONE; + int oldDirection = + oldFallbackGroupDescriptor != null + ? oldFallbackGroupDescriptor.mDirection + : AUDIO_DIRECTION_NONE; boolean notifyAndUpdateInactiveOutDeviceOnly = false; - boolean hasFallbackDeviceWhenGettingInactive = oldFallbackGroupDescriptor != null - ? oldFallbackGroupDescriptor.mHasFallbackDeviceWhenGettingInactive - : false; + boolean hasFallbackDeviceWhenGettingInactive = + oldFallbackGroupDescriptor != null + ? oldFallbackGroupDescriptor.mHasFallbackDeviceWhenGettingInactive + : false; if (groupId != LE_AUDIO_GROUP_ID_INVALID) { newDirection = AUDIO_DIRECTION_INPUT_BIT; notifyAndUpdateInactiveOutDeviceOnly = true; @@ -5723,525 +5725,6 @@ public class LeAudioService extends ProfileService { } } - /** Binder object: must be a static class or memory leak may occur */ - @VisibleForTesting - static class BluetoothLeAudioBinder extends IBluetoothLeAudio.Stub - implements IProfileServiceBinder { - private LeAudioService mService; - - BluetoothLeAudioBinder(LeAudioService svc) { - mService = svc; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private LeAudioService getServiceAndEnforceConnect(AttributionSource source) { - requireNonNull(source); - // Cache mService because it can change while getService is called - LeAudioService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - return service; - } - - private LeAudioService getService() { - // Cache mService because it can change while getService is called - LeAudioService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG)) { - return null; - } - return service; - } - - @Override - public boolean connect(BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - return service.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - return service.disconnect(device); - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getConnectedDevices(); - } - - @Override - public BluetoothDevice getConnectedGroupLeadDevice(int groupId, AttributionSource source) { - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return null; - } - - return service.getConnectedGroupLeadDevice(groupId); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return STATE_DISCONNECTED; - } - - return service.getConnectionState(device); - } - - @Override - public boolean setActiveDevice(BluetoothDevice device, AttributionSource source) { - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - if (device == null) { - return service.removeActiveDevice(true); - } else { - return service.setActiveDevice(device); - } - } - - @Override - public List<BluetoothDevice> getActiveDevices(AttributionSource source) { - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getActiveDevices(); - } - - @Override - public int getAudioLocation(BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return BluetoothLeAudio.AUDIO_LOCATION_INVALID; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getAudioLocation(device); - } - - @Override - public boolean isInbandRingtoneEnabled(AttributionSource source, int groupId) { - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.isInbandRingtoneEnabled(groupId); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - requireNonNull(device); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return CONNECTION_POLICY_UNKNOWN; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getConnectionPolicy(device); - } - - @Override - public void setCcidInformation( - ParcelUuid userUuid, int ccid, int contextType, AttributionSource source) { - requireNonNull(userUuid); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.setCcidInformation(userUuid, ccid, contextType); - } - - @Override - public int getGroupId(BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return LE_AUDIO_GROUP_ID_INVALID; - } - - return service.getGroupId(device); - } - - @Override - public boolean groupAddNode(int groupId, BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.groupAddNode(groupId, device); - } - - @Override - public void setInCall(boolean inCall, AttributionSource source) { - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.setInCall(inCall); - } - - @Override - public void setInactiveForHfpHandover( - BluetoothDevice hfpHandoverDevice, AttributionSource source) { - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.setInactiveForHfpHandover(hfpHandoverDevice); - } - - @Override - public boolean groupRemoveNode( - int groupId, BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.groupRemoveNode(groupId, device); - } - - @Override - public void setVolume(int volume, AttributionSource source) { - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.setVolume(volume); - } - - @Override - public void registerCallback(IBluetoothLeAudioCallback callback, AttributionSource source) { - requireNonNull(callback); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - service.registerCallback(callback); - } - - @Override - public void unregisterCallback( - IBluetoothLeAudioCallback callback, AttributionSource source) { - requireNonNull(callback); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - service.unregisterCallback(callback); - } - - @Override - public void registerLeBroadcastCallback( - IBluetoothLeBroadcastCallback callback, AttributionSource source) { - requireNonNull(callback); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.registerLeBroadcastCallback(callback); - } - - @Override - public void unregisterLeBroadcastCallback( - IBluetoothLeBroadcastCallback callback, AttributionSource source) { - requireNonNull(callback); - requireNonNull(source); - - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.unregisterLeBroadcastCallback(callback); - } - - @Override - public void startBroadcast( - BluetoothLeBroadcastSettings broadcastSettings, AttributionSource source) { - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.createBroadcast(broadcastSettings); - } - - @Override - public void stopBroadcast(int broadcastId, AttributionSource source) { - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.stopBroadcast(broadcastId); - } - - @Override - public void updateBroadcast( - int broadcastId, - BluetoothLeBroadcastSettings broadcastSettings, - AttributionSource source) { - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.updateBroadcast(broadcastId, broadcastSettings); - } - - @Override - public boolean isPlaying(int broadcastId, AttributionSource source) { - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.isPlaying(broadcastId); - } - - @Override - public List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata( - AttributionSource source) { - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return Collections.emptyList(); - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getAllBroadcastMetadata(); - } - - @Override - public int getMaximumNumberOfBroadcasts() { - LeAudioService service = getService(); - if (service == null) { - return 0; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getMaximumNumberOfBroadcasts(); - } - - @Override - public int getMaximumStreamsPerBroadcast() { - LeAudioService service = getService(); - if (service == null) { - return 0; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getMaximumStreamsPerBroadcast(); - } - - @Override - public int getMaximumSubgroupsPerBroadcast() { - LeAudioService service = getService(); - if (service == null) { - return 0; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getMaximumSubgroupsPerBroadcast(); - } - - @Override - public BluetoothLeAudioCodecStatus getCodecStatus(int groupId, AttributionSource source) { - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return null; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getCodecStatus(groupId); - } - - @Override - public void setCodecConfigPreference( - int groupId, - BluetoothLeAudioCodecConfig inputCodecConfig, - BluetoothLeAudioCodecConfig outputCodecConfig, - AttributionSource source) { - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.setCodecConfigPreference(groupId, inputCodecConfig, outputCodecConfig); - } - - @Override - public void setBroadcastToUnicastFallbackGroup(int groupId, AttributionSource source) { - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.setBroadcastToUnicastFallbackGroup(groupId); - } - - @Override - public int getBroadcastToUnicastFallbackGroup(AttributionSource source) { - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return LE_AUDIO_GROUP_ID_INVALID; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getBroadcastToUnicastFallbackGroup(); - } - - @Override - public boolean isBroadcastActive(AttributionSource source) { - LeAudioService service = getServiceAndEnforceConnect(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.isBroadcastActive(); - } - } - @Override public void dump(StringBuilder sb) { super.dump(sb); diff --git a/android/app/src/com/android/bluetooth/le_audio/LeAudioServiceBinder.java b/android/app/src/com/android/bluetooth/le_audio/LeAudioServiceBinder.java new file mode 100644 index 0000000000..e774c8fe33 --- /dev/null +++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioServiceBinder.java @@ -0,0 +1,548 @@ +/* + * Copyright (C) 2025 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.le_audio; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; +import static android.bluetooth.IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID; + +import static java.util.Objects.requireNonNull; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeAudio; +import android.bluetooth.BluetoothLeAudioCodecConfig; +import android.bluetooth.BluetoothLeAudioCodecStatus; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastSettings; +import android.bluetooth.IBluetoothLeAudio; +import android.bluetooth.IBluetoothLeAudioCallback; +import android.bluetooth.IBluetoothLeBroadcastCallback; +import android.content.AttributionSource; +import android.os.ParcelUuid; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Collections; +import java.util.List; + +class LeAudioServiceBinder extends IBluetoothLeAudio.Stub implements IProfileServiceBinder { + private static final String TAG = LeAudioServiceBinder.class.getSimpleName(); + + private LeAudioService mService; + + LeAudioServiceBinder(LeAudioService svc) { + mService = svc; + } + + @Override + public void cleanup() { + mService = null; + } + + private LeAudioService getService() { + LeAudioService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG)) { + return null; + } + return service; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private LeAudioService getServiceAndEnforceConnect(AttributionSource source) { + requireNonNull(source); + LeAudioService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + return service; + } + + @Override + public boolean connect(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + + return service.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + return service.disconnect(device); + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getConnectedDevices(); + } + + @Override + public BluetoothDevice getConnectedGroupLeadDevice(int groupId, AttributionSource source) { + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return null; + } + return service.getConnectedGroupLeadDevice(groupId); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return STATE_DISCONNECTED; + } + return service.getConnectionState(device); + } + + @Override + public boolean setActiveDevice(BluetoothDevice device, AttributionSource source) { + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + + if (device == null) { + return service.removeActiveDevice(true); + } else { + return service.setActiveDevice(device); + } + } + + @Override + public List<BluetoothDevice> getActiveDevices(AttributionSource source) { + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getActiveDevices(); + } + + @Override + public int getAudioLocation(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return BluetoothLeAudio.AUDIO_LOCATION_INVALID; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getAudioLocation(device); + } + + @Override + public boolean isInbandRingtoneEnabled(AttributionSource source, int groupId) { + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.isInbandRingtoneEnabled(groupId); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + requireNonNull(device); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return CONNECTION_POLICY_UNKNOWN; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectionPolicy(device); + } + + @Override + public void setCcidInformation( + ParcelUuid userUuid, int ccid, int contextType, AttributionSource source) { + requireNonNull(userUuid); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.setCcidInformation(userUuid, ccid, contextType); + } + + @Override + public int getGroupId(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return LE_AUDIO_GROUP_ID_INVALID; + } + return service.getGroupId(device); + } + + @Override + public boolean groupAddNode(int groupId, BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.groupAddNode(groupId, device); + } + + @Override + public void setInCall(boolean inCall, AttributionSource source) { + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.setInCall(inCall); + } + + @Override + public void setInactiveForHfpHandover( + BluetoothDevice hfpHandoverDevice, AttributionSource source) { + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.setInactiveForHfpHandover(hfpHandoverDevice); + } + + @Override + public boolean groupRemoveNode(int groupId, BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.groupRemoveNode(groupId, device); + } + + @Override + public void setVolume(int volume, AttributionSource source) { + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.setVolume(volume); + } + + @Override + public void registerCallback(IBluetoothLeAudioCallback callback, AttributionSource source) { + requireNonNull(callback); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.registerCallback(callback); + } + + @Override + public void unregisterCallback(IBluetoothLeAudioCallback callback, AttributionSource source) { + requireNonNull(callback); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.unregisterCallback(callback); + } + + @Override + public void registerLeBroadcastCallback( + IBluetoothLeBroadcastCallback callback, AttributionSource source) { + requireNonNull(callback); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.registerLeBroadcastCallback(callback); + } + + @Override + public void unregisterLeBroadcastCallback( + IBluetoothLeBroadcastCallback callback, AttributionSource source) { + requireNonNull(callback); + requireNonNull(source); + + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.unregisterLeBroadcastCallback(callback); + } + + @Override + public void startBroadcast( + BluetoothLeBroadcastSettings broadcastSettings, AttributionSource source) { + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.createBroadcast(broadcastSettings); + } + + @Override + public void stopBroadcast(int broadcastId, AttributionSource source) { + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.stopBroadcast(broadcastId); + } + + @Override + public void updateBroadcast( + int broadcastId, + BluetoothLeBroadcastSettings broadcastSettings, + AttributionSource source) { + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.updateBroadcast(broadcastId, broadcastSettings); + } + + @Override + public boolean isPlaying(int broadcastId, AttributionSource source) { + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.isPlaying(broadcastId); + } + + @Override + public List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata(AttributionSource source) { + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return Collections.emptyList(); + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getAllBroadcastMetadata(); + } + + @Override + public int getMaximumNumberOfBroadcasts() { + LeAudioService service = getService(); + if (service == null) { + return 0; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getMaximumNumberOfBroadcasts(); + } + + @Override + public int getMaximumStreamsPerBroadcast() { + LeAudioService service = getService(); + if (service == null) { + return 0; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getMaximumStreamsPerBroadcast(); + } + + @Override + public int getMaximumSubgroupsPerBroadcast() { + LeAudioService service = getService(); + if (service == null) { + return 0; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getMaximumSubgroupsPerBroadcast(); + } + + @Override + public BluetoothLeAudioCodecStatus getCodecStatus(int groupId, AttributionSource source) { + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return null; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getCodecStatus(groupId); + } + + @Override + public void setCodecConfigPreference( + int groupId, + BluetoothLeAudioCodecConfig inputCodecConfig, + BluetoothLeAudioCodecConfig outputCodecConfig, + AttributionSource source) { + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.setCodecConfigPreference(groupId, inputCodecConfig, outputCodecConfig); + } + + @Override + public void setBroadcastToUnicastFallbackGroup(int groupId, AttributionSource source) { + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.setBroadcastToUnicastFallbackGroup(groupId); + } + + @Override + public int getBroadcastToUnicastFallbackGroup(AttributionSource source) { + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return LE_AUDIO_GROUP_ID_INVALID; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getBroadcastToUnicastFallbackGroup(); + } + + @Override + public boolean isBroadcastActive(AttributionSource source) { + LeAudioService service = getServiceAndEnforceConnect(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.isBroadcastActive(); + } +} diff --git a/android/app/src/com/android/bluetooth/map/BluetoothMapService.java b/android/app/src/com/android/bluetooth/map/BluetoothMapService.java index bac8bdc247..b43af84dcc 100644 --- a/android/app/src/com/android/bluetooth/map/BluetoothMapService.java +++ b/android/app/src/com/android/bluetooth/map/BluetoothMapService.java @@ -18,7 +18,6 @@ package com.android.bluetooth.map; 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.CONNECTION_POLICY_UNKNOWN; import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; @@ -33,9 +32,7 @@ import android.bluetooth.BluetoothMap; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProtoEnums; import android.bluetooth.BluetoothUuid; -import android.bluetooth.IBluetoothMap; import android.bluetooth.SdpMnsRecord; -import android.content.AttributionSource; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -66,7 +63,6 @@ import com.android.internal.annotations.VisibleForTesting; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -149,7 +145,6 @@ public class BluetoothMapService extends ProfileService { private static BluetoothMapService sBluetoothMapService; - private static final ParcelUuid[] MAP_UUIDS = { BluetoothUuid.MAP, BluetoothUuid.MNS, }; @@ -705,7 +700,7 @@ public class BluetoothMapService extends ProfileService { @Override protected IProfileServiceBinder initBinder() { - return new BluetoothMapBinder(this); + return new BluetoothMapServiceBinder(this); } /** @@ -1190,227 +1185,6 @@ public class BluetoothMapService extends ProfileService { } } - // Binder object: Must be static class or memory leak may occur - - /** - * This class implements the IBluetoothMap interface - or actually it validates the - * preconditions for calling the actual functionality in the MapService, and calls it. - */ - @VisibleForTesting - static class BluetoothMapBinder extends IBluetoothMap.Stub implements IProfileServiceBinder { - private BluetoothMapService mService; - - BluetoothMapBinder(BluetoothMapService service) { - mService = service; - } - - @Override - public synchronized void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private BluetoothMapService getService(AttributionSource source) { - // Cache mService because it can change while getService is called - BluetoothMapService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - return service; - } - - @Override - public int getState(AttributionSource source) { - Log.v(TAG, "getState()"); - try { - BluetoothMapService service = getService(source); - if (service == null) { - return BluetoothMap.STATE_DISCONNECTED; - } - - return service.getState(); - } catch (RuntimeException e) { - ContentProfileErrorReportUtils.report( - BluetoothProfile.MAP, - BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, - BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, - 16); - throw e; - } - } - - @Override - public BluetoothDevice getClient(AttributionSource source) { - Log.v(TAG, "getClient()"); - try { - BluetoothMapService service = getService(source); - if (service == null) { - Log.v(TAG, "getClient() - no service - returning " + null); - return null; - } - BluetoothDevice client = service.getRemoteDevice(); - Log.v(TAG, "getClient() - returning " + client); - return client; - } catch (RuntimeException e) { - ContentProfileErrorReportUtils.report( - BluetoothProfile.MAP, - BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, - BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, - 17); - throw e; - } - } - - @Override - public boolean isConnected(BluetoothDevice device, AttributionSource source) { - Log.v(TAG, "isConnected()"); - try { - BluetoothMapService service = getService(source); - if (service == null) { - return false; - } - - return service.getConnectionState(device) == STATE_CONNECTED; - } catch (RuntimeException e) { - ContentProfileErrorReportUtils.report( - BluetoothProfile.MAP, - BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, - BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, - 18); - throw e; - } - } - - @Override - public boolean disconnect(BluetoothDevice device, AttributionSource source) { - Log.v(TAG, "disconnect()"); - try { - BluetoothMapService service = getService(source); - if (service == null) { - return false; - } - - service.disconnect(device); - return true; - } catch (RuntimeException e) { - ContentProfileErrorReportUtils.report( - BluetoothProfile.MAP, - BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, - BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, - 19); - throw e; - } - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - Log.v(TAG, "getConnectedDevices()"); - try { - BluetoothMapService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getConnectedDevices(); - } catch (RuntimeException e) { - ContentProfileErrorReportUtils.report( - BluetoothProfile.MAP, - BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, - BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, - 20); - throw e; - } - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - Log.v(TAG, "getDevicesMatchingConnectionStates()"); - try { - BluetoothMapService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getDevicesMatchingConnectionStates(states); - } catch (RuntimeException e) { - ContentProfileErrorReportUtils.report( - BluetoothProfile.MAP, - BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, - BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, - 21); - throw e; - } - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - Log.v(TAG, "getConnectionState()"); - try { - BluetoothMapService service = getService(source); - if (service == null) { - return STATE_DISCONNECTED; - } - - return service.getConnectionState(device); - } catch (RuntimeException e) { - ContentProfileErrorReportUtils.report( - BluetoothProfile.MAP, - BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, - BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, - 22); - throw e; - } - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - try { - BluetoothMapService service = getService(source); - if (service == null) { - return false; - } - - return service.setConnectionPolicy(device, connectionPolicy); - } catch (RuntimeException e) { - ContentProfileErrorReportUtils.report( - BluetoothProfile.MAP, - BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, - BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, - 23); - throw e; - } - } - - @Override - public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { - try { - BluetoothMapService service = getService(source); - if (service == null) { - return CONNECTION_POLICY_UNKNOWN; - } - - return service.getConnectionPolicy(device); - } catch (RuntimeException e) { - ContentProfileErrorReportUtils.report( - BluetoothProfile.MAP, - BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, - BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, - 24); - throw e; - } - } - } - @Override public void dump(StringBuilder sb) { super.dump(sb); diff --git a/android/app/src/com/android/bluetooth/map/BluetoothMapServiceBinder.java b/android/app/src/com/android/bluetooth/map/BluetoothMapServiceBinder.java new file mode 100644 index 0000000000..0706bcab6f --- /dev/null +++ b/android/app/src/com/android/bluetooth/map/BluetoothMapServiceBinder.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2025 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.map; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; +import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothMap; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothProtoEnums; +import android.bluetooth.IBluetoothMap; +import android.content.AttributionSource; +import android.util.Log; + +import com.android.bluetooth.BluetoothStatsLog; +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; +import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils; + +import java.util.Collections; +import java.util.List; + +/** + * This class implements the IBluetoothMap interface - or actually it validates the preconditions + * for calling the actual functionality in the MapService, and calls it. + */ +class BluetoothMapServiceBinder extends IBluetoothMap.Stub implements IProfileServiceBinder { + private static final String TAG = BluetoothMapServiceBinder.class.getSimpleName(); + + private BluetoothMapService mService; + + BluetoothMapServiceBinder(BluetoothMapService service) { + mService = service; + } + + @Override + public synchronized void cleanup() { + mService = null; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private BluetoothMapService getService(AttributionSource source) { + BluetoothMapService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + return service; + } + + @Override + public int getState(AttributionSource source) { + Log.v(TAG, "getState()"); + try { + BluetoothMapService service = getService(source); + if (service == null) { + return BluetoothMap.STATE_DISCONNECTED; + } + return service.getState(); + } catch (RuntimeException e) { + ContentProfileErrorReportUtils.report( + BluetoothProfile.MAP, + BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, + BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, + 16); + throw e; + } + } + + @Override + public BluetoothDevice getClient(AttributionSource source) { + Log.v(TAG, "getClient()"); + try { + BluetoothMapService service = getService(source); + if (service == null) { + Log.v(TAG, "getClient() - no service - returning " + null); + return null; + } + BluetoothDevice client = service.getRemoteDevice(); + Log.v(TAG, "getClient() - returning " + client); + return client; + } catch (RuntimeException e) { + ContentProfileErrorReportUtils.report( + BluetoothProfile.MAP, + BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, + BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, + 17); + throw e; + } + } + + @Override + public boolean isConnected(BluetoothDevice device, AttributionSource source) { + Log.v(TAG, "isConnected()"); + try { + BluetoothMapService service = getService(source); + if (service == null) { + return false; + } + return service.getConnectionState(device) == STATE_CONNECTED; + } catch (RuntimeException e) { + ContentProfileErrorReportUtils.report( + BluetoothProfile.MAP, + BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, + BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, + 18); + throw e; + } + } + + @Override + public boolean disconnect(BluetoothDevice device, AttributionSource source) { + Log.v(TAG, "disconnect()"); + try { + BluetoothMapService service = getService(source); + if (service == null) { + return false; + } + service.disconnect(device); + return true; + } catch (RuntimeException e) { + ContentProfileErrorReportUtils.report( + BluetoothProfile.MAP, + BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, + BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, + 19); + throw e; + } + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + Log.v(TAG, "getConnectedDevices()"); + try { + BluetoothMapService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectedDevices(); + } catch (RuntimeException e) { + ContentProfileErrorReportUtils.report( + BluetoothProfile.MAP, + BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, + BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, + 20); + throw e; + } + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + Log.v(TAG, "getDevicesMatchingConnectionStates()"); + try { + BluetoothMapService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } catch (RuntimeException e) { + ContentProfileErrorReportUtils.report( + BluetoothProfile.MAP, + BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, + BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, + 21); + throw e; + } + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + Log.v(TAG, "getConnectionState()"); + try { + BluetoothMapService service = getService(source); + if (service == null) { + return STATE_DISCONNECTED; + } + return service.getConnectionState(device); + } catch (RuntimeException e) { + ContentProfileErrorReportUtils.report( + BluetoothProfile.MAP, + BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, + BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, + 22); + throw e; + } + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + try { + BluetoothMapService service = getService(source); + if (service == null) { + return false; + } + return service.setConnectionPolicy(device, connectionPolicy); + } catch (RuntimeException e) { + ContentProfileErrorReportUtils.report( + BluetoothProfile.MAP, + BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, + BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, + 23); + throw e; + } + } + + @Override + public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { + try { + BluetoothMapService service = getService(source); + if (service == null) { + return CONNECTION_POLICY_UNKNOWN; + } + return service.getConnectionPolicy(device); + } catch (RuntimeException e) { + ContentProfileErrorReportUtils.report( + BluetoothProfile.MAP, + BluetoothProtoEnums.BLUETOOTH_MAP_SERVICE, + BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, + 24); + throw e; + } + } +} diff --git a/android/app/src/com/android/bluetooth/mapclient/MapClientService.java b/android/app/src/com/android/bluetooth/mapclient/MapClientService.java index 5e2be59b11..fdecfddf15 100644 --- a/android/app/src/com/android/bluetooth/mapclient/MapClientService.java +++ b/android/app/src/com/android/bluetooth/mapclient/MapClientService.java @@ -16,11 +16,8 @@ package com.android.bluetooth.mapclient; -import static android.Manifest.permission.BLUETOOTH_CONNECT; -import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; -import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; import static android.bluetooth.BluetoothProfile.STATE_CONNECTING; import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; @@ -28,16 +25,12 @@ import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNullElseGet; -import android.Manifest; -import android.annotation.RequiresPermission; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; -import android.bluetooth.IBluetoothMapClient; import android.bluetooth.SdpMasRecord; -import android.content.AttributionSource; import android.net.Uri; import android.os.Handler; import android.os.Looper; @@ -46,7 +39,6 @@ import android.os.Parcelable; import android.sysprop.BluetoothProperties; import android.util.Log; -import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.storage.DatabaseManager; @@ -54,7 +46,6 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -307,7 +298,7 @@ public class MapClientService extends ProfileService { @Override public IProfileServiceBinder initBinder() { - return new Binder(this); + return new MapClientServiceBinder(this); } @Override @@ -408,172 +399,6 @@ public class MapClientService extends ProfileService { } } - // Binder object: Must be static class or memory leak may occur - - /** - * This class implements the IClient interface - or actually it validates the preconditions for - * calling the actual functionality in the MapClientService, and calls it. - */ - @VisibleForTesting - static class Binder extends IBluetoothMapClient.Stub implements IProfileServiceBinder { - private MapClientService mService; - - Binder(MapClientService service) { - mService = service; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private MapClientService getService(AttributionSource source) { - // Cache mService because it can change while getService is called - MapClientService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !(getCallingUserHandle().isSystem() - || Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG)) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - return service; - } - - @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) - private MapClientService getServiceAndEnforcePrivileged(AttributionSource source) { - // Cache mService because it can change while getService is called - MapClientService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !(getCallingUserHandle().isSystem() - || Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG)) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service; - } - - @Override - public boolean connect(BluetoothDevice device, AttributionSource source) { - Log.v(TAG, "connect()"); - - MapClientService service = getServiceAndEnforcePrivileged(source); - if (service == null) { - return false; - } - - return service.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device, AttributionSource source) { - Log.v(TAG, "disconnect()"); - - MapClientService service = getServiceAndEnforcePrivileged(source); - if (service == null) { - return false; - } - - return service.disconnect(device); - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - Log.v(TAG, "getConnectedDevices()"); - - MapClientService service = getServiceAndEnforcePrivileged(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getConnectedDevices(); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - Log.v(TAG, "getDevicesMatchingConnectionStates()"); - - MapClientService service = getServiceAndEnforcePrivileged(source); - if (service == null) { - return Collections.emptyList(); - } - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - Log.v(TAG, "getConnectionState()"); - - MapClientService service = getServiceAndEnforcePrivileged(source); - if (service == null) { - return STATE_DISCONNECTED; - } - - return service.getConnectionState(device); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - Log.v(TAG, "setConnectionPolicy()"); - - MapClientService service = getServiceAndEnforcePrivileged(source); - if (service == null) { - return false; - } - - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { - Log.v(TAG, "getConnectionPolicy()"); - - MapClientService service = getServiceAndEnforcePrivileged(source); - if (service == null) { - return CONNECTION_POLICY_UNKNOWN; - } - - return service.getConnectionPolicy(device); - } - - @Override - public boolean sendMessage( - BluetoothDevice device, - Uri[] contacts, - String message, - PendingIntent sentIntent, - PendingIntent deliveredIntent, - AttributionSource source) { - Log.v(TAG, "sendMessage()"); - - MapClientService service = getService(source); - if (service == null) { - return false; - } - - Log.d(TAG, "Checking Permission of sendMessage"); - mService.enforceCallingOrSelfPermission( - Manifest.permission.SEND_SMS, "Need SEND_SMS permission"); - - return service.sendMessage(device, contacts, message, sentIntent, deliveredIntent); - } - } - public void aclDisconnected(BluetoothDevice device, int transport) { mHandler.post(() -> handleAclDisconnected(device, transport)); } diff --git a/android/app/src/com/android/bluetooth/mapclient/MapClientServiceBinder.java b/android/app/src/com/android/bluetooth/mapclient/MapClientServiceBinder.java new file mode 100644 index 0000000000..d6be8975e8 --- /dev/null +++ b/android/app/src/com/android/bluetooth/mapclient/MapClientServiceBinder.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2025 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.mapclient; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import android.Manifest; +import android.annotation.RequiresPermission; +import android.app.PendingIntent; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.IBluetoothMapClient; +import android.content.AttributionSource; +import android.net.Uri; +import android.util.Log; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Collections; +import java.util.List; + +/** + * This class implements the IClient interface - or actually it validates the preconditions for + * calling the actual functionality in the MapClientService, and calls it. + */ +class MapClientServiceBinder extends IBluetoothMapClient.Stub implements IProfileServiceBinder { + private static final String TAG = MapClientServiceBinder.class.getSimpleName(); + + private MapClientService mService; + + MapClientServiceBinder(MapClientService service) { + mService = service; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private MapClientService getService(AttributionSource source) { + MapClientService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !(getCallingUserHandle().isSystem() + || Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG)) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + return service; + } + + @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) + private MapClientService getServiceAndEnforcePrivileged(AttributionSource source) { + MapClientService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !(getCallingUserHandle().isSystem() + || Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG)) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service; + } + + @Override + public boolean connect(BluetoothDevice device, AttributionSource source) { + Log.v(TAG, "connect()"); + + MapClientService service = getServiceAndEnforcePrivileged(source); + if (service == null) { + return false; + } + return service.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device, AttributionSource source) { + Log.v(TAG, "disconnect()"); + + MapClientService service = getServiceAndEnforcePrivileged(source); + if (service == null) { + return false; + } + return service.disconnect(device); + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + Log.v(TAG, "getConnectedDevices()"); + + MapClientService service = getServiceAndEnforcePrivileged(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getConnectedDevices(); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + Log.v(TAG, "getDevicesMatchingConnectionStates()"); + + MapClientService service = getServiceAndEnforcePrivileged(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + Log.v(TAG, "getConnectionState()"); + + MapClientService service = getServiceAndEnforcePrivileged(source); + if (service == null) { + return STATE_DISCONNECTED; + } + return service.getConnectionState(device); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + Log.v(TAG, "setConnectionPolicy()"); + + MapClientService service = getServiceAndEnforcePrivileged(source); + if (service == null) { + return false; + } + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { + Log.v(TAG, "getConnectionPolicy()"); + + MapClientService service = getServiceAndEnforcePrivileged(source); + if (service == null) { + return CONNECTION_POLICY_UNKNOWN; + } + return service.getConnectionPolicy(device); + } + + @Override + public boolean sendMessage( + BluetoothDevice device, + Uri[] contacts, + String message, + PendingIntent sentIntent, + PendingIntent deliveredIntent, + AttributionSource source) { + Log.v(TAG, "sendMessage()"); + + MapClientService service = getService(source); + if (service == null) { + return false; + } + + Log.d(TAG, "Checking Permission of sendMessage"); + mService.enforceCallingOrSelfPermission( + Manifest.permission.SEND_SMS, "Need SEND_SMS permission"); + + return service.sendMessage(device, contacts, message, sentIntent, deliveredIntent); + } +} diff --git a/android/app/src/com/android/bluetooth/mcp/McpService.java b/android/app/src/com/android/bluetooth/mcp/McpService.java index c0ba4f12bb..6f08be0061 100644 --- a/android/app/src/com/android/bluetooth/mcp/McpService.java +++ b/android/app/src/com/android/bluetooth/mcp/McpService.java @@ -17,12 +17,9 @@ package com.android.bluetooth.mcp; -import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import android.bluetooth.BluetoothDevice; -import android.bluetooth.IBluetoothMcpServiceManager; -import android.content.AttributionSource; import android.content.Context; import android.os.ParcelUuid; import android.sysprop.BluetoothProperties; @@ -88,7 +85,7 @@ public class McpService extends ProfileService { @Override protected IProfileServiceBinder initBinder() { - return new BluetoothMcpServiceBinder(this); + return new McpServiceBinder(this); } @Override @@ -198,41 +195,4 @@ public class McpService extends ProfileService { int ccid, BluetoothDevice device, ParcelUuid charUuid, boolean doNotify) { mGmcs.setNotificationSubscription(ccid, device, charUuid, doNotify); } - - /** Binder object: must be a static class or memory leak may occur */ - static class BluetoothMcpServiceBinder extends IBluetoothMcpServiceManager.Stub - implements IProfileServiceBinder { - private McpService mService; - - BluetoothMcpServiceBinder(McpService svc) { - mService = svc; - } - - private McpService getService() { - if (mService != null && mService.isAvailable()) { - return mService; - } - Log.e(TAG, "getService() - Service requested, but not available!"); - return null; - } - - @Override - public void setDeviceAuthorized( - BluetoothDevice device, boolean isAuthorized, AttributionSource source) { - McpService service = getService(); - if (service == null) { - return; - } - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.setDeviceAuthorized(device, isAuthorized); - } - - @Override - public void cleanup() { - if (mService != null) { - mService.cleanup(); - } - mService = null; - } - } } diff --git a/android/app/src/com/android/bluetooth/mcp/McpServiceBinder.java b/android/app/src/com/android/bluetooth/mcp/McpServiceBinder.java new file mode 100644 index 0000000000..b22e87cadf --- /dev/null +++ b/android/app/src/com/android/bluetooth/mcp/McpServiceBinder.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2025 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.mcp; + +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.IBluetoothMcpServiceManager; +import android.content.AttributionSource; +import android.util.Log; + +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +class McpServiceBinder extends IBluetoothMcpServiceManager.Stub implements IProfileServiceBinder { + private static final String TAG = McpServiceBinder.class.getSimpleName(); + + private McpService mService; + + McpServiceBinder(McpService svc) { + mService = svc; + } + + private McpService getService() { + if (mService != null && mService.isAvailable()) { + return mService; + } + Log.e(TAG, "getService() - Service requested, but not available!"); + return null; + } + + @Override + public void setDeviceAuthorized( + BluetoothDevice device, boolean isAuthorized, AttributionSource source) { + McpService service = getService(); + if (service == null) { + return; + } + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.setDeviceAuthorized(device, isAuthorized); + } + + @Override + public void cleanup() { + if (mService != null) { + mService.cleanup(); + } + mService = null; + } +} diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java index 048c134848..73cba4e954 100644 --- a/android/app/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java +++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java @@ -248,6 +248,12 @@ public class BluetoothOppLauncherActivity extends Activity { permittedUris, false /* isHandover */, true /* fromExternal */); + if (Flags.sendOppDevicePickerExtraIntent()) { + BluetoothOppUtility + .grantPermissionToNearbyComponent( + BluetoothOppLauncherActivity.this, + uris); + } // Done getting file info..Launch device picker // and finish this activity launchDevicePicker(); @@ -322,6 +328,11 @@ public class BluetoothOppLauncherActivity extends Activity { in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_PACKAGE, getPackageName()); in1.putExtra( BluetoothDevicePicker.EXTRA_LAUNCH_CLASS, BluetoothOppReceiver.class.getName()); + if (Flags.sendOppDevicePickerExtraIntent()) { + in1.putExtra( + BluetoothDevicePicker.EXTRA_DEVICE_PICKER_ORIGINAL_SEND_INTENT, + getIntent()); + } Log.v(TAG, "Launching " + BluetoothDevicePicker.ACTION_LAUNCH); startActivity(in1); } @@ -555,6 +566,10 @@ public class BluetoothOppLauncherActivity extends Activity { void sendFileInfo(String mimeType, String uriString, boolean isHandover, boolean fromExternal) { BluetoothOppManager manager = BluetoothOppManager.getInstance(getApplicationContext()); try { + if (Flags.sendOppDevicePickerExtraIntent()) { + BluetoothOppUtility.grantPermissionToNearbyComponent( + this, List.of(Uri.parse(uriString))); + } manager.saveSendingFileInfo(mimeType, uriString, isHandover, fromExternal); launchDevicePicker(); finish(); diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppUtility.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppUtility.java index d7b75bfde2..4e8892ab35 100644 --- a/android/app/src/com/android/bluetooth/opp/BluetoothOppUtility.java +++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppUtility.java @@ -38,6 +38,7 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProtoEnums; import android.content.ActivityNotFoundException; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; @@ -50,6 +51,7 @@ import android.net.Uri; import android.os.Environment; import android.os.ParcelFileDescriptor; import android.os.SystemProperties; +import android.provider.Settings; import android.util.EventLog; import android.util.Log; @@ -77,6 +79,9 @@ import java.util.concurrent.ConcurrentHashMap; public class BluetoothOppUtility { private static final String TAG = BluetoothOppUtility.class.getSimpleName(); + // TODO(b/398120192): use API instead of a hardcode string. + private static final String NEARBY_SHARING_COMPONENT = "nearby_sharing_component"; + /** Whether the device has the "nosdcard" characteristic, or null if not-yet-known. */ private static Boolean sNoSdCard = null; @@ -572,4 +577,30 @@ public class BluetoothOppUtility { NotificationManager nm = ctx.getSystemService(NotificationManager.class); nm.cancel(BluetoothOppNotification.NOTIFICATION_ID_PROGRESS); } + + /** Grants uri read permission to nearby sharing component. */ + static void grantPermissionToNearbyComponent(Context context, List<Uri> uris) { + String packageName = getNearbyComponentPackageName(context); + if (packageName == null) { + return; + } + for (Uri uri : uris) { + BluetoothMethodProxy.getInstance() + .grantUriPermission( + context, packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } + + private static String getNearbyComponentPackageName(Context context) { + String componentString = + Settings.Secure.getString(context.getContentResolver(), NEARBY_SHARING_COMPONENT); + if (componentString == null) { + return null; + } + ComponentName componentName = ComponentName.unflattenFromString(componentString); + if (componentName == null) { + return null; + } + return componentName.getPackageName(); + } } diff --git a/android/app/src/com/android/bluetooth/pan/PanService.java b/android/app/src/com/android/bluetooth/pan/PanService.java index 311e100b52..5c2dfdf2a7 100644 --- a/android/app/src/com/android/bluetooth/pan/PanService.java +++ b/android/app/src/com/android/bluetooth/pan/PanService.java @@ -17,8 +17,6 @@ package com.android.bluetooth.pan; import static android.Manifest.permission.BLUETOOTH_CONNECT; -import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; -import static android.Manifest.permission.TETHER_PRIVILEGED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; @@ -30,15 +28,12 @@ import static android.bluetooth.BluetoothUtils.logRemoteException; import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNullElseGet; -import android.annotation.RequiresPermission; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothPan; import android.bluetooth.BluetoothPan.LocalPanRole; import android.bluetooth.BluetoothPan.RemotePanRole; import android.bluetooth.BluetoothProfile; -import android.bluetooth.IBluetoothPan; import android.bluetooth.IBluetoothPanCallback; -import android.content.AttributionSource; import android.content.Intent; import android.content.res.Resources.NotFoundException; import android.net.TetheringInterface; @@ -60,7 +55,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.modules.utils.HandlerExecutor; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -154,7 +148,7 @@ public class PanService extends ProfileService { @Override public IProfileServiceBinder initBinder() { - return new BluetoothPanBinder(this); + return new PanServiceBinder(this); } public static synchronized PanService getPanService() { @@ -276,137 +270,6 @@ public class PanService extends ProfileService { } } - /** Handlers for incoming service calls */ - @VisibleForTesting - static class BluetoothPanBinder extends IBluetoothPan.Stub implements IProfileServiceBinder { - private PanService mService; - - BluetoothPanBinder(PanService svc) { - mService = svc; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private PanService getService(AttributionSource source) { - if (Utils.isInstrumentationTestMode()) { - return mService; - } - if (!Utils.checkServiceAvailable(mService, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(mService, TAG) - || !Utils.checkConnectPermissionForDataDelivery(mService, source, TAG)) { - return null; - } - return mService; - } - - @Override - public boolean connect(BluetoothDevice device, AttributionSource source) { - PanService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.connect(device); - } - - @Override - public boolean disconnect(BluetoothDevice device, AttributionSource source) { - PanService service = getService(source); - if (service == null) { - return false; - } - - return service.disconnect(device); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - PanService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - PanService service = getService(source); - if (service == null) { - return BluetoothPan.STATE_DISCONNECTED; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getConnectionState(device); - } - - @Override - public boolean isTetheringOn(AttributionSource source) { - // TODO(BT) have a variable marking the on/off state - PanService service = getService(source); - if (service == null) { - return false; - } - - return service.isTetheringOn(); - } - - @Override - public void setBluetoothTethering( - IBluetoothPanCallback callback, int id, boolean value, AttributionSource source) { - PanService service = getService(source); - if (service == null) { - return; - } - - Log.d( - TAG, - "setBluetoothTethering:" - + (" value=" + value) - + (" pkgName= " + source.getPackageName()) - + (" mTetherOn= " + service.mTetherOn)); - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.enforceCallingOrSelfPermission(TETHER_PRIVILEGED, null); - - service.setBluetoothTethering(callback, id, source.getUid(), value); - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - PanService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getConnectedDevices(); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - PanService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getDevicesMatchingConnectionStates(states); - } - } - public boolean connect(BluetoothDevice device) { if (mUserManager.isGuestUser()) { Log.w(TAG, "Guest user does not have the permission to change the WiFi network"); diff --git a/android/app/src/com/android/bluetooth/pan/PanServiceBinder.java b/android/app/src/com/android/bluetooth/pan/PanServiceBinder.java new file mode 100644 index 0000000000..e3baae21dc --- /dev/null +++ b/android/app/src/com/android/bluetooth/pan/PanServiceBinder.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2025 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.pan; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static android.Manifest.permission.TETHER_PRIVILEGED; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothPan; +import android.bluetooth.IBluetoothPan; +import android.bluetooth.IBluetoothPanCallback; +import android.content.AttributionSource; +import android.util.Log; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Collections; +import java.util.List; + +/** Handlers for incoming service calls */ +class PanServiceBinder extends IBluetoothPan.Stub implements IProfileServiceBinder { + private static final String TAG = PanServiceBinder.class.getSimpleName(); + + private PanService mService; + + PanServiceBinder(PanService svc) { + mService = svc; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private PanService getService(AttributionSource source) { + if (Utils.isInstrumentationTestMode()) { + return mService; + } + if (!Utils.checkServiceAvailable(mService, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(mService, TAG) + || !Utils.checkConnectPermissionForDataDelivery(mService, source, TAG)) { + return null; + } + return mService; + } + + @Override + public boolean connect(BluetoothDevice device, AttributionSource source) { + PanService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device, AttributionSource source) { + PanService service = getService(source); + if (service == null) { + return false; + } + return service.disconnect(device); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + PanService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + PanService service = getService(source); + if (service == null) { + return BluetoothPan.STATE_DISCONNECTED; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectionState(device); + } + + @Override + public boolean isTetheringOn(AttributionSource source) { + // TODO(BT) have a variable marking the on/off state + PanService service = getService(source); + if (service == null) { + return false; + } + return service.isTetheringOn(); + } + + @Override + public void setBluetoothTethering( + IBluetoothPanCallback callback, int id, boolean value, AttributionSource source) { + PanService service = getService(source); + if (service == null) { + return; + } + + Log.d( + TAG, + "setBluetoothTethering:" + + (" value=" + value) + + (" pkgName= " + source.getPackageName()) + + (" mTetherOn= " + service.isTetheringOn())); + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.enforceCallingOrSelfPermission(TETHER_PRIVILEGED, null); + + service.setBluetoothTethering(callback, id, source.getUid(), value); + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + PanService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectedDevices(); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + PanService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getDevicesMatchingConnectionStates(states); + } +} diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapActivity.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapActivity.java index 7223516e88..18fff92890 100644 --- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapActivity.java +++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapActivity.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapAuthenticator.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapAuthenticator.java index 55419908eb..2e788d424a 100644 --- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapAuthenticator.java +++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapAuthenticator.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapConfig.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapConfig.java index 6fec2895e6..391d759065 100644 --- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapConfig.java +++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapConfig.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java index 88f7023b03..c5bb01f426 100644 --- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java +++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapService.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapService.java index 00194f5d78..2d67fc9947 100644 --- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapService.java +++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapService.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -17,7 +17,6 @@ package com.android.bluetooth.pbap; import static android.Manifest.permission.BLUETOOTH_CONNECT; -import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; import static android.bluetooth.BluetoothDevice.ACCESS_ALLOWED; import static android.bluetooth.BluetoothDevice.ACCESS_REJECTED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; @@ -28,19 +27,15 @@ import static com.android.bluetooth.Utils.joinUninterruptibly; import static java.util.Objects.requireNonNull; -import android.annotation.RequiresPermission; import android.app.Activity; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; -import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProtoEnums; import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothUtils; -import android.bluetooth.IBluetoothPbap; -import android.content.AttributionSource; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -75,7 +70,6 @@ import com.android.bluetooth.util.DevicePolicyUtils; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -734,7 +728,7 @@ public class BluetoothPbapService extends ProfileService implements IObexConnect @Override protected IProfileServiceBinder initBinder() { - return new PbapBinder(this); + return new BluetoothPbapServiceBinder(this); } @Override @@ -775,97 +769,6 @@ public class BluetoothPbapService extends ProfileService implements IObexConnect sBluetoothPbapService = instance; } - @VisibleForTesting - static class PbapBinder extends IBluetoothPbap.Stub implements IProfileServiceBinder { - private BluetoothPbapService mService; - - PbapBinder(BluetoothPbapService service) { - Log.v(TAG, "PbapBinder()"); - mService = service; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private BluetoothPbapService getService(AttributionSource source) { - // Cache mService because it can change while getService is called - BluetoothPbapService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - - return service; - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - Log.d(TAG, "getConnectedDevices"); - BluetoothPbapService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - return service.getConnectedDevices(); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - Log.d(TAG, "getDevicesMatchingConnectionStates"); - BluetoothPbapService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - Log.d(TAG, "getConnectionState: " + device); - BluetoothPbapService service = getService(source); - if (service == null) { - return BluetoothAdapter.STATE_DISCONNECTED; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getConnectionState(device); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - Log.d(TAG, "setConnectionPolicy for device=" + device + " policy=" + connectionPolicy); - BluetoothPbapService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public void disconnect(BluetoothDevice device, AttributionSource source) { - Log.d(TAG, "disconnect"); - BluetoothPbapService service = getService(source); - if (service == null) { - return; - } - service.disconnect(device); - } - } - @Override public boolean onConnect(BluetoothDevice remoteDevice, BluetoothSocket socket) { if (remoteDevice == null || socket == null) { diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapServiceBinder.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapServiceBinder.java new file mode 100644 index 0000000000..67ad07169a --- /dev/null +++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapServiceBinder.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2025 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.pbap; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.IBluetoothPbap; +import android.content.AttributionSource; +import android.util.Log; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Collections; +import java.util.List; + +class BluetoothPbapServiceBinder extends IBluetoothPbap.Stub implements IProfileServiceBinder { + private static final String TAG = BluetoothPbapServiceBinder.class.getSimpleName(); + + private BluetoothPbapService mService; + + BluetoothPbapServiceBinder(BluetoothPbapService service) { + mService = service; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private BluetoothPbapService getService(AttributionSource source) { + BluetoothPbapService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + + return service; + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + Log.d(TAG, "getConnectedDevices"); + BluetoothPbapService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getConnectedDevices(); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + Log.d(TAG, "getDevicesMatchingConnectionStates"); + BluetoothPbapService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + Log.d(TAG, "getConnectionState: " + device); + BluetoothPbapService service = getService(source); + if (service == null) { + return BluetoothAdapter.STATE_DISCONNECTED; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectionState(device); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + Log.d(TAG, "setConnectionPolicy for device=" + device + " policy=" + connectionPolicy); + BluetoothPbapService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public void disconnect(BluetoothDevice device, AttributionSource source) { + Log.d(TAG, "disconnect"); + BluetoothPbapService service = getService(source); + if (service == null) { + return; + } + service.disconnect(device); + } +} diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java index db427991a2..c63ec28105 100644 --- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java +++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapSimVcardManager.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapUtils.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapUtils.java index e70706f84d..75a1b3d9f8 100644 --- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapUtils.java +++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapUtils.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java index 5fa66a785c..c3b08a2e89 100644 --- a/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java +++ b/android/app/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/android/app/src/com/android/bluetooth/pbapclient/PbapClientService.java b/android/app/src/com/android/bluetooth/pbapclient/PbapClientService.java index c83e856a72..9b75de98f0 100644 --- a/android/app/src/com/android/bluetooth/pbapclient/PbapClientService.java +++ b/android/app/src/com/android/bluetooth/pbapclient/PbapClientService.java @@ -173,7 +173,7 @@ public class PbapClientService extends ProfileService { @Override public IProfileServiceBinder initBinder() { - return new PbapClientBinder(this); + return new PbapClientServiceBinder(this); } @Override diff --git a/android/app/src/com/android/bluetooth/pbapclient/PbapClientBinder.java b/android/app/src/com/android/bluetooth/pbapclient/PbapClientServiceBinder.java index f3bcd1e7b5..5a45896f02 100644 --- a/android/app/src/com/android/bluetooth/pbapclient/PbapClientBinder.java +++ b/android/app/src/com/android/bluetooth/pbapclient/PbapClientServiceBinder.java @@ -35,12 +35,12 @@ import java.util.Collections; import java.util.List; /** Handler for incoming service calls destined for PBAP Client */ -class PbapClientBinder extends IBluetoothPbapClient.Stub implements IProfileServiceBinder { - private static final String TAG = PbapClientBinder.class.getSimpleName(); +class PbapClientServiceBinder extends IBluetoothPbapClient.Stub implements IProfileServiceBinder { + private static final String TAG = PbapClientServiceBinder.class.getSimpleName(); private PbapClientService mService; - PbapClientBinder(PbapClientService service) { + PbapClientServiceBinder(PbapClientService service) { mService = service; } diff --git a/android/app/src/com/android/bluetooth/sap/SapService.java b/android/app/src/com/android/bluetooth/sap/SapService.java index c8bda8e046..54391d24e3 100644 --- a/android/app/src/com/android/bluetooth/sap/SapService.java +++ b/android/app/src/com/android/bluetooth/sap/SapService.java @@ -20,7 +20,6 @@ import static android.Manifest.permission.BLUETOOTH_CONNECT; import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; -import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; @@ -37,8 +36,6 @@ import android.bluetooth.BluetoothSap; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothUuid; -import android.bluetooth.IBluetoothSap; -import android.content.AttributionSource; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -61,7 +58,6 @@ import com.android.internal.annotations.VisibleForTesting; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; public class SapService extends ProfileService implements AdapterService.BluetoothStateCallback { @@ -672,7 +668,7 @@ public class SapService extends ProfileService implements AdapterService.Bluetoo @Override protected IProfileServiceBinder initBinder() { - return new SapBinder(this); + return new SapServiceBinder(this); } @Override @@ -881,145 +877,4 @@ public class SapService extends ProfileService implements AdapterService.Bluetoo mSessionStatusHandler.sendEmptyMessage(MSG_SERVERSESSION_CLOSE); } } - - // Binder object: Must be static class or memory leak may occur - - /** - * This class implements the IBluetoothSap interface - or actually it validates the - * preconditions for calling the actual functionality in the SapService, and calls it. - */ - private static class SapBinder extends IBluetoothSap.Stub implements IProfileServiceBinder { - private SapService mService; - - SapBinder(SapService service) { - Log.v(TAG, "SapBinder()"); - mService = service; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private SapService getService(AttributionSource source) { - // Cache mService because it can change while getService is called - SapService service = mService; - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - - return service; - } - - @Override - public int getState(AttributionSource source) { - Log.v(TAG, "getState()"); - - SapService service = getService(source); - if (service == null) { - return BluetoothSap.STATE_DISCONNECTED; - } - - return service.getState(); - } - - @Override - public BluetoothDevice getClient(AttributionSource source) { - Log.v(TAG, "getClient()"); - - SapService service = getService(source); - if (service == null) { - return null; - } - - Log.v(TAG, "getClient() - returning " + service.getRemoteDevice()); - return service.getRemoteDevice(); - } - - @Override - public boolean isConnected(BluetoothDevice device, AttributionSource source) { - Log.v(TAG, "isConnected()"); - - SapService service = getService(source); - if (service == null) { - return false; - } - - return service.getConnectionState(device) == STATE_CONNECTED; - } - - @Override - public boolean disconnect(BluetoothDevice device, AttributionSource source) { - Log.v(TAG, "disconnect()"); - - SapService service = getService(source); - if (service == null) { - return false; - } - - return service.disconnect(device); - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - Log.v(TAG, "getConnectedDevices()"); - - SapService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getConnectedDevices(); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - Log.v(TAG, "getDevicesMatchingConnectionStates()"); - - SapService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - Log.v(TAG, "getConnectionState()"); - - SapService service = getService(source); - if (service == null) { - return STATE_DISCONNECTED; - } - - return service.getConnectionState(device); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - SapService service = getService(source); - if (service == null) { - return false; - } - - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { - SapService service = getService(source); - if (service == null) { - return CONNECTION_POLICY_UNKNOWN; - } - - return service.getConnectionPolicy(device); - } - } } diff --git a/android/app/src/com/android/bluetooth/sap/SapServiceBinder.java b/android/app/src/com/android/bluetooth/sap/SapServiceBinder.java new file mode 100644 index 0000000000..4c8a96901a --- /dev/null +++ b/android/app/src/com/android/bluetooth/sap/SapServiceBinder.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2025 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.sap; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; +import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSap; +import android.bluetooth.IBluetoothSap; +import android.content.AttributionSource; +import android.util.Log; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.Collections; +import java.util.List; + +/** + * This class implements the IBluetoothSap interface - or actually it validates the preconditions + * for calling the actual functionality in the SapService, and calls it. + */ +class SapServiceBinder extends IBluetoothSap.Stub implements IProfileServiceBinder { + private static final String TAG = SapServiceBinder.class.getSimpleName(); + + private SapService mService; + + SapServiceBinder(SapService service) { + mService = service; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private SapService getService(AttributionSource source) { + SapService service = mService; + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + + return service; + } + + @Override + public int getState(AttributionSource source) { + Log.v(TAG, "getState()"); + + SapService service = getService(source); + if (service == null) { + return BluetoothSap.STATE_DISCONNECTED; + } + return service.getState(); + } + + @Override + public BluetoothDevice getClient(AttributionSource source) { + Log.v(TAG, "getClient()"); + + SapService service = getService(source); + if (service == null) { + return null; + } + + Log.v(TAG, "getClient() - returning " + service.getRemoteDevice()); + return service.getRemoteDevice(); + } + + @Override + public boolean isConnected(BluetoothDevice device, AttributionSource source) { + Log.v(TAG, "isConnected()"); + + SapService service = getService(source); + if (service == null) { + return false; + } + return service.getConnectionState(device) == STATE_CONNECTED; + } + + @Override + public boolean disconnect(BluetoothDevice device, AttributionSource source) { + Log.v(TAG, "disconnect()"); + + SapService service = getService(source); + if (service == null) { + return false; + } + return service.disconnect(device); + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + Log.v(TAG, "getConnectedDevices()"); + + SapService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getConnectedDevices(); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + Log.v(TAG, "getDevicesMatchingConnectionStates()"); + + SapService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + Log.v(TAG, "getConnectionState()"); + + SapService service = getService(source); + if (service == null) { + return STATE_DISCONNECTED; + } + return service.getConnectionState(device); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + SapService service = getService(source); + if (service == null) { + return false; + } + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { + SapService service = getService(source); + if (service == null) { + return CONNECTION_POLICY_UNKNOWN; + } + return service.getConnectionPolicy(device); + } +} diff --git a/android/app/src/com/android/bluetooth/tbs/TbsService.java b/android/app/src/com/android/bluetooth/tbs/TbsService.java index ebd25f8229..3bb2fa45e7 100644 --- a/android/app/src/com/android/bluetooth/tbs/TbsService.java +++ b/android/app/src/com/android/bluetooth/tbs/TbsService.java @@ -17,19 +17,13 @@ package com.android.bluetooth.tbs; -import static android.Manifest.permission.BLUETOOTH_CONNECT; -import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import static java.util.Objects.requireNonNull; -import android.annotation.RequiresPermission; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeCall; -import android.bluetooth.IBluetoothLeCallControl; import android.bluetooth.IBluetoothLeCallControlCallback; -import android.content.AttributionSource; -import android.os.ParcelUuid; import android.os.RemoteException; import android.sysprop.BluetoothProperties; import android.util.Log; @@ -38,7 +32,6 @@ import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.le_audio.LeAudioService; -import com.android.internal.annotations.VisibleForTesting; import java.util.HashMap; import java.util.List; @@ -68,7 +61,7 @@ public class TbsService extends ProfileService { @Override protected IProfileServiceBinder initBinder() { - return new TbsServerBinder(this); + return new TbsServiceBinder(this); } @Override @@ -206,105 +199,6 @@ public class TbsService extends ProfileService { mTbsGeneric.clearInbandRingtoneSupport(device); } - /** Binder object: must be a static class or memory leak may occur */ - @VisibleForTesting - static class TbsServerBinder extends IBluetoothLeCallControl.Stub - implements IProfileServiceBinder { - private TbsService mService; - - TbsServerBinder(TbsService service) { - mService = service; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) - private TbsService getService(AttributionSource source) { - // Cache mService because it can change while getService is called - TbsService service = mService; - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service; - } - - @Override - public void registerBearer( - String token, - IBluetoothLeCallControlCallback callback, - String uci, - List<String> uriSchemes, - int capabilities, - String providerName, - int technology, - AttributionSource source) { - TbsService service = getService(source); - if (service != null) { - service.registerBearer( - token, callback, uci, uriSchemes, capabilities, providerName, technology); - } - } - - @Override - public void unregisterBearer(String token, AttributionSource source) { - TbsService service = getService(source); - if (service != null) { - service.unregisterBearer(token); - } - } - - @Override - public void requestResult(int ccid, int requestId, int result, AttributionSource source) { - TbsService service = getService(source); - if (service != null) { - service.requestResult(ccid, requestId, result); - } - } - - @Override - public void callAdded(int ccid, BluetoothLeCall call, AttributionSource source) { - TbsService service = getService(source); - if (service != null) { - service.callAdded(ccid, call); - } - } - - @Override - public void callRemoved(int ccid, ParcelUuid callId, int reason, AttributionSource source) { - TbsService service = getService(source); - if (service != null) { - service.callRemoved(ccid, callId.getUuid(), reason); - } - } - - @Override - public void callStateChanged( - int ccid, ParcelUuid callId, int state, AttributionSource source) { - TbsService service = getService(source); - if (service != null) { - service.callStateChanged(ccid, callId.getUuid(), state); - } - } - - @Override - public void currentCallsList( - int ccid, List<BluetoothLeCall> calls, AttributionSource source) { - TbsService service = getService(source); - if (service != null) { - service.currentCallsList(ccid, calls); - } - } - } - - @VisibleForTesting void registerBearer( String token, IBluetoothLeCallControlCallback callback, @@ -335,42 +229,36 @@ public class TbsService extends ProfileService { Log.d(TAG, "registerBearer: token=" + token + " success=" + success); } - @VisibleForTesting void unregisterBearer(String token) { Log.d(TAG, "unregisterBearer: token=" + token); mTbsGeneric.removeBearer(token); } - @VisibleForTesting public void requestResult(int ccid, int requestId, int result) { Log.d(TAG, "requestResult: ccid=" + ccid + " requestId=" + requestId + " result=" + result); mTbsGeneric.requestResult(ccid, requestId, result); } - @VisibleForTesting void callAdded(int ccid, BluetoothLeCall call) { Log.d(TAG, "callAdded: ccid=" + ccid + " call=" + call); mTbsGeneric.callAdded(ccid, call); } - @VisibleForTesting void callRemoved(int ccid, UUID callId, int reason) { Log.d(TAG, "callRemoved: ccid=" + ccid + " callId=" + callId + " reason=" + reason); mTbsGeneric.callRemoved(ccid, callId, reason); } - @VisibleForTesting void callStateChanged(int ccid, UUID callId, int state) { Log.d(TAG, "callStateChanged: ccid=" + ccid + " callId=" + callId + " state=" + state); mTbsGeneric.callStateChanged(ccid, callId, state); } - @VisibleForTesting void currentCallsList(int ccid, List<BluetoothLeCall> calls) { Log.d(TAG, "currentCallsList: ccid=" + ccid + " calls=" + calls); diff --git a/android/app/src/com/android/bluetooth/tbs/TbsServiceBinder.java b/android/app/src/com/android/bluetooth/tbs/TbsServiceBinder.java new file mode 100644 index 0000000000..88a1a3435c --- /dev/null +++ b/android/app/src/com/android/bluetooth/tbs/TbsServiceBinder.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2025 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.tbs; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; + +import android.annotation.RequiresPermission; +import android.bluetooth.BluetoothLeCall; +import android.bluetooth.IBluetoothLeCallControl; +import android.bluetooth.IBluetoothLeCallControlCallback; +import android.content.AttributionSource; +import android.os.ParcelUuid; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +import java.util.List; + +class TbsServiceBinder extends IBluetoothLeCallControl.Stub implements IProfileServiceBinder { + private static final String TAG = TbsServiceBinder.class.getSimpleName(); + + private TbsService mService; + + TbsServiceBinder(TbsService service) { + mService = service; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) + private TbsService getService(AttributionSource source) { + TbsService service = mService; + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service; + } + + @Override + public void registerBearer( + String token, + IBluetoothLeCallControlCallback callback, + String uci, + List<String> uriSchemes, + int capabilities, + String providerName, + int technology, + AttributionSource source) { + TbsService service = getService(source); + if (service != null) { + service.registerBearer( + token, callback, uci, uriSchemes, capabilities, providerName, technology); + } + } + + @Override + public void unregisterBearer(String token, AttributionSource source) { + TbsService service = getService(source); + if (service != null) { + service.unregisterBearer(token); + } + } + + @Override + public void requestResult(int ccid, int requestId, int result, AttributionSource source) { + TbsService service = getService(source); + if (service != null) { + service.requestResult(ccid, requestId, result); + } + } + + @Override + public void callAdded(int ccid, BluetoothLeCall call, AttributionSource source) { + TbsService service = getService(source); + if (service != null) { + service.callAdded(ccid, call); + } + } + + @Override + public void callRemoved(int ccid, ParcelUuid callId, int reason, AttributionSource source) { + TbsService service = getService(source); + if (service != null) { + service.callRemoved(ccid, callId.getUuid(), reason); + } + } + + @Override + public void callStateChanged(int ccid, ParcelUuid callId, int state, AttributionSource source) { + TbsService service = getService(source); + if (service != null) { + service.callStateChanged(ccid, callId.getUuid(), state); + } + } + + @Override + public void currentCallsList(int ccid, List<BluetoothLeCall> calls, AttributionSource source) { + TbsService service = getService(source); + if (service != null) { + service.currentCallsList(ccid, calls); + } + } +} diff --git a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java index 628a36b6e8..b21d4e19b3 100644 --- a/android/app/src/com/android/bluetooth/vc/VolumeControlService.java +++ b/android/app/src/com/android/bluetooth/vc/VolumeControlService.java @@ -17,8 +17,6 @@ package com.android.bluetooth.vc; -import static android.Manifest.permission.BLUETOOTH_CONNECT; -import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; import static android.bluetooth.BluetoothDevice.BOND_BONDED; import static android.bluetooth.BluetoothDevice.BOND_NONE; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; @@ -36,19 +34,14 @@ import static com.android.bluetooth.flags.Flags.vcpDeviceVolumeApiImprovements; import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNullElseGet; -import android.annotation.RequiresPermission; import android.bluetooth.AudioInputControl.AudioInputStatus; import android.bluetooth.AudioInputControl.AudioInputType; import android.bluetooth.AudioInputControl.GainMode; import android.bluetooth.AudioInputControl.Mute; -import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; -import android.bluetooth.IAudioInputCallback; -import android.bluetooth.IBluetoothVolumeControl; import android.bluetooth.IBluetoothVolumeControlCallback; -import android.content.AttributionSource; import android.media.AudioManager; import android.os.Handler; import android.os.HandlerThread; @@ -70,22 +63,13 @@ import com.android.bluetooth.le_audio.LeAudioService; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import libcore.util.SneakyThrow; - import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.FutureTask; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Function; public class VolumeControlService extends ProfileService { private static final String TAG = VolumeControlService.class.getSimpleName(); @@ -166,7 +150,7 @@ public class VolumeControlService extends ProfileService { @Override protected IProfileServiceBinder initBinder() { - return new BluetoothVolumeControlBinder(this); + return new VolumeControlServiceBinder(this); } @Override @@ -209,6 +193,14 @@ public class VolumeControlService extends ProfileService { } } + Handler getHandler() { + return mHandler; + } + + Map<BluetoothDevice, VolumeControlInputDescriptor> getAudioInputs() { + return mAudioInputs; + } + /** * Get the VolumeControlService instance * @@ -534,7 +526,6 @@ public class VolumeControlService extends ProfileService { mNativeInterface.setExtAudioOutVolumeOffset(device, instanceId, volumeOffset); } - @VisibleForTesting synchronized void setDeviceVolume(BluetoothDevice device, int volume, boolean isGroupOp) { Log.d( TAG, @@ -1685,501 +1676,6 @@ public class VolumeControlService extends ProfileService { BluetoothProfile.VOLUME_CONTROL, device, fromState, toState); } - /** Binder object: must be a static class or memory leak may occur */ - @VisibleForTesting - static class BluetoothVolumeControlBinder extends IBluetoothVolumeControl.Stub - implements IProfileServiceBinder { - @VisibleForTesting boolean mIsTesting = false; - private VolumeControlService mService; - - BluetoothVolumeControlBinder(VolumeControlService svc) { - mService = svc; - } - - @Override - public void cleanup() { - mService = null; - } - - @RequiresPermission(BLUETOOTH_CONNECT) - private VolumeControlService getService(AttributionSource source) { - requireNonNull(source); - - // Cache mService because it can change while getService is called - VolumeControlService service = mService; - - if (Utils.isInstrumentationTestMode()) { - return service; - } - - if (!Utils.checkServiceAvailable(service, TAG) - || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) - || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { - return null; - } - - return service; - } - - @Override - public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { - VolumeControlService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getConnectedDevices(); - } - - @Override - public List<BluetoothDevice> getDevicesMatchingConnectionStates( - int[] states, AttributionSource source) { - VolumeControlService service = getService(source); - if (service == null) { - return Collections.emptyList(); - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - return service.getDevicesMatchingConnectionStates(states); - } - - @Override - public int getConnectionState(BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - - VolumeControlService service = getService(source); - if (service == null) { - return STATE_DISCONNECTED; - } - - return service.getConnectionState(device); - } - - @Override - public boolean setConnectionPolicy( - BluetoothDevice device, int connectionPolicy, AttributionSource source) { - requireNonNull(device); - - VolumeControlService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.setConnectionPolicy(device, connectionPolicy); - } - - @Override - public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - - VolumeControlService service = getService(source); - if (service == null) { - return CONNECTION_POLICY_UNKNOWN; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getConnectionPolicy(device); - } - - @Override - public boolean isVolumeOffsetAvailable(BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - - VolumeControlService service = getService(source); - if (service == null) { - return false; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.isVolumeOffsetAvailable(device); - } - - @Override - public int getNumberOfVolumeOffsetInstances( - BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - - VolumeControlService service = getService(source); - if (service == null) { - return 0; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - return service.getNumberOfVolumeOffsetInstances(device); - } - - @Override - public void setVolumeOffset( - BluetoothDevice device, - int instanceId, - int volumeOffset, - AttributionSource source) { - requireNonNull(device); - - VolumeControlService service = getService(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.setVolumeOffset(device, instanceId, volumeOffset); - } - - @Override - public void setDeviceVolume( - BluetoothDevice device, int volume, boolean isGroupOp, AttributionSource source) { - requireNonNull(device); - - VolumeControlService service = getService(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - service.setDeviceVolume(device, volume, isGroupOp); - } - - @Override - public void setGroupVolume(int groupId, int volume, AttributionSource source) { - VolumeControlService service = getService(source); - if (service == null) { - return; - } - - service.setGroupVolume(groupId, volume); - } - - @Override - public int getGroupVolume(int groupId, AttributionSource source) { - VolumeControlService service = getService(source); - if (service == null) { - return 0; - } - - return service.getGroupVolume(groupId); - } - - @Override - public void setGroupActive(int groupId, boolean active, AttributionSource source) { - VolumeControlService service = getService(source); - if (service == null) { - return; - } - - service.setGroupActive(groupId, active); - } - - @Override - public void mute(BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - - VolumeControlService service = getService(source); - if (service == null) { - return; - } - - service.mute(device); - } - - @Override - public void muteGroup(int groupId, AttributionSource source) { - VolumeControlService service = getService(source); - if (service == null) { - return; - } - - service.muteGroup(groupId); - } - - @Override - public void unmute(BluetoothDevice device, AttributionSource source) { - requireNonNull(device); - - VolumeControlService service = getService(source); - if (service == null) { - return; - } - - service.unmute(device); - } - - @Override - public void unmuteGroup(int groupId, AttributionSource source) { - VolumeControlService service = getService(source); - if (service == null) { - return; - } - - service.unmuteGroup(groupId); - } - - private static void postAndWait(Handler handler, Runnable runnable) { - FutureTask<Void> task = new FutureTask(Executors.callable(runnable)); - - handler.post(task); - try { - task.get(1, TimeUnit.SECONDS); - } catch (TimeoutException | InterruptedException e) { - SneakyThrow.sneakyThrow(e); - } catch (ExecutionException e) { - SneakyThrow.sneakyThrow(e.getCause()); - } - } - - @Override - public void registerCallback( - IBluetoothVolumeControlCallback callback, AttributionSource source) { - requireNonNull(callback); - - VolumeControlService service = getService(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - postAndWait(service.mHandler, () -> service.registerCallback(callback)); - } - - @Override - public void unregisterCallback( - IBluetoothVolumeControlCallback callback, AttributionSource source) { - requireNonNull(callback); - - VolumeControlService service = getService(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - postAndWait(service.mHandler, () -> service.unregisterCallback(callback)); - } - - @Override - public void notifyNewRegisteredCallback( - IBluetoothVolumeControlCallback callback, AttributionSource source) { - requireNonNull(callback); - - VolumeControlService service = getService(source); - if (service == null) { - return; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - postAndWait(service.mHandler, () -> service.notifyNewRegisteredCallback(callback)); - } - - private static void validateBluetoothDevice(BluetoothDevice device) { - requireNonNull(device); - String address = device.getAddress(); - if (!BluetoothAdapter.checkBluetoothAddress(address)) { - throw new IllegalArgumentException("Invalid device address: " + address); - } - } - - @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) - private <R> R aicsWrapper( - AttributionSource source, - BluetoothDevice device, - Function<VolumeControlInputDescriptor, R> fn, - R defaultValue) { - validateBluetoothDevice(device); - - VolumeControlService service = getService(source); - if (service == null) { - return defaultValue; - } - - service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); - - VolumeControlInputDescriptor inputs = service.mAudioInputs.get(device); - if (inputs == null) { - Log.w(TAG, "No audio inputs for " + device); - return defaultValue; - } - - return fn.apply(inputs); - } - - @Override - public int getNumberOfAudioInputControlServices( - AttributionSource source, BluetoothDevice device) { - validateBluetoothDevice(device); - Log.d(TAG, "getNumberOfAudioInputControlServices(" + device + ")"); - return aicsWrapper(source, device, i -> i.size(), 0); - } - - @Override - public void registerAudioInputControlCallback( - AttributionSource source, - BluetoothDevice device, - int instanceId, - IAudioInputCallback callback) { - requireNonNull(callback); - Log.d( - TAG, - "registerAudioInputControlCallback(" - + (device + ", " + instanceId + ", " + callback) - + ")"); - aicsWrapper( - source, - device, - i -> { - i.registerCallback(instanceId, callback); - return null; - }, - null); - } - - @Override - public void unregisterAudioInputControlCallback( - AttributionSource source, - BluetoothDevice device, - int instanceId, - IAudioInputCallback callback) { - requireNonNull(callback); - Log.d( - TAG, - "unregisterAudioInputControlCallback(" - + (device + ", " + instanceId + ", " + callback) - + ")"); - aicsWrapper( - source, - device, - i -> { - i.unregisterCallback(instanceId, callback); - return null; - }, - null); - } - - @Override - public int getAudioInputGainSettingUnit( - AttributionSource source, BluetoothDevice device, int instanceId) { - Log.d(TAG, "getAudioInputGainSettingUnit(" + device + ", " + instanceId + ")"); - return aicsWrapper(source, device, i -> i.getGainSettingUnit(instanceId), 0); - } - - @Override - public int getAudioInputGainSettingMin( - AttributionSource source, BluetoothDevice device, int instanceId) { - Log.d(TAG, "getAudioInputGainSettingMin(" + device + ", " + instanceId + ")"); - return aicsWrapper(source, device, i -> i.getGainSettingMin(instanceId), 0); - } - - @Override - public int getAudioInputGainSettingMax( - AttributionSource source, BluetoothDevice device, int instanceId) { - Log.d(TAG, "getAudioInputGainSettingMax(" + device + ", " + instanceId + ")"); - return aicsWrapper(source, device, i -> i.getGainSettingMax(instanceId), 0); - } - - @Override - public String getAudioInputDescription( - AttributionSource source, BluetoothDevice device, int instanceId) { - Log.d(TAG, "getAudioInputDescription(" + device + ", " + instanceId + ")"); - return aicsWrapper(source, device, i -> i.getDescription(instanceId), ""); - } - - @Override - public boolean isAudioInputDescriptionWritable( - AttributionSource source, BluetoothDevice device, int instanceId) { - Log.d(TAG, "isAudioInputDescriptionWritable(" + device + ", " + instanceId + ")"); - return aicsWrapper(source, device, i -> i.isDescriptionWritable(instanceId), false); - } - - @Override - public boolean setAudioInputDescription( - AttributionSource source, - BluetoothDevice device, - int instanceId, - String description) { - requireNonNull(description); - Log.d(TAG, "setAudioInputDescription(" + device + ", " + instanceId + ")"); - return aicsWrapper( - source, device, i -> i.setDescription(instanceId, description), false); - } - - @Override - public @AudioInputStatus int getAudioInputStatus( - AttributionSource source, BluetoothDevice device, int instanceId) { - Log.d(TAG, "getAudioInputStatus(" + device + ", " + instanceId + ")"); - return aicsWrapper( - source, - device, - i -> i.getStatus(instanceId), - (int) bluetooth.constants.aics.AudioInputStatus.INACTIVE); - } - - @Override - public @AudioInputType int getAudioInputType( - AttributionSource source, BluetoothDevice device, int instanceId) { - Log.d(TAG, "getAudioInputType(" + device + ", " + instanceId + ")"); - return aicsWrapper( - source, - device, - i -> i.getType(instanceId), - bluetooth.constants.AudioInputType.UNSPECIFIED); - } - - @Override - public int getAudioInputGainSetting( - AttributionSource source, BluetoothDevice device, int instanceId) { - Log.d(TAG, "getAudioInputGainSetting(" + device + ", " + instanceId + ")"); - return aicsWrapper(source, device, i -> i.getGainSetting(instanceId), 0); - } - - @Override - public boolean setAudioInputGainSetting( - AttributionSource source, BluetoothDevice device, int instanceId, int gainSetting) { - Log.d(TAG, "setAudioInputGainSetting(" + device + ", " + instanceId + ")"); - return aicsWrapper( - source, device, i -> i.setGainSetting(instanceId, gainSetting), false); - } - - @Override - public @GainMode int getAudioInputGainMode( - AttributionSource source, BluetoothDevice device, int instanceId) { - Log.d(TAG, "getAudioInputGainMode(" + device + ", " + instanceId + ")"); - return aicsWrapper( - source, - device, - i -> i.getGainMode(instanceId), - (int) bluetooth.constants.aics.GainMode.AUTOMATIC_ONLY); - } - - @Override - public boolean setAudioInputGainMode( - AttributionSource source, - BluetoothDevice device, - int instanceId, - @GainMode int gainMode) { - Log.d(TAG, "setAudioInputGainMode(" + device + ", " + instanceId + ")"); - return aicsWrapper(source, device, i -> i.setGainMode(instanceId, gainMode), false); - } - - @Override - public @Mute int getAudioInputMute( - AttributionSource source, BluetoothDevice device, int instanceId) { - Log.d(TAG, "getAudioInputMute(" + device + ", " + instanceId + ")"); - return aicsWrapper( - source, - device, - i -> i.getMute(instanceId), - (int) bluetooth.constants.aics.Mute.DISABLED); - } - - @Override - public boolean setAudioInputMute( - AttributionSource source, BluetoothDevice device, int instanceId, @Mute int mute) { - Log.d(TAG, "setAudioInputMute(" + device + ", " + instanceId + ")"); - return aicsWrapper(source, device, i -> i.setMute(instanceId, mute), false); - } - } - @Override public void dump(StringBuilder sb) { super.dump(sb); diff --git a/android/app/src/com/android/bluetooth/vc/VolumeControlServiceBinder.java b/android/app/src/com/android/bluetooth/vc/VolumeControlServiceBinder.java new file mode 100644 index 0000000000..7438ab14cd --- /dev/null +++ b/android/app/src/com/android/bluetooth/vc/VolumeControlServiceBinder.java @@ -0,0 +1,528 @@ +/* + * Copyright (C) 2025 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.vc; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import static java.util.Objects.requireNonNull; + +import android.annotation.RequiresPermission; +import android.bluetooth.AudioInputControl.AudioInputStatus; +import android.bluetooth.AudioInputControl.AudioInputType; +import android.bluetooth.AudioInputControl.GainMode; +import android.bluetooth.AudioInputControl.Mute; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.IAudioInputCallback; +import android.bluetooth.IBluetoothVolumeControl; +import android.bluetooth.IBluetoothVolumeControlCallback; +import android.content.AttributionSource; +import android.os.Handler; +import android.util.Log; + +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; +import com.android.internal.annotations.VisibleForTesting; + +import libcore.util.SneakyThrow; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; + +class VolumeControlServiceBinder extends IBluetoothVolumeControl.Stub + implements IProfileServiceBinder { + private static final String TAG = VolumeControlServiceBinder.class.getSimpleName(); + + @VisibleForTesting boolean mIsTesting = false; + private VolumeControlService mService; + + VolumeControlServiceBinder(VolumeControlService svc) { + mService = svc; + } + + @Override + public void cleanup() { + mService = null; + } + + @RequiresPermission(BLUETOOTH_CONNECT) + private VolumeControlService getService(AttributionSource source) { + requireNonNull(source); + + VolumeControlService service = mService; + + if (Utils.isInstrumentationTestMode()) { + return service; + } + + if (!Utils.checkServiceAvailable(service, TAG) + || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) + || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { + return null; + } + + return service; + } + + @Override + public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { + VolumeControlService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectedDevices(); + } + + @Override + public List<BluetoothDevice> getDevicesMatchingConnectionStates( + int[] states, AttributionSource source) { + VolumeControlService service = getService(source); + if (service == null) { + return Collections.emptyList(); + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getDevicesMatchingConnectionStates(states); + } + + @Override + public int getConnectionState(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + + VolumeControlService service = getService(source); + if (service == null) { + return STATE_DISCONNECTED; + } + return service.getConnectionState(device); + } + + @Override + public boolean setConnectionPolicy( + BluetoothDevice device, int connectionPolicy, AttributionSource source) { + requireNonNull(device); + + VolumeControlService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.setConnectionPolicy(device, connectionPolicy); + } + + @Override + public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + + VolumeControlService service = getService(source); + if (service == null) { + return CONNECTION_POLICY_UNKNOWN; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getConnectionPolicy(device); + } + + @Override + public boolean isVolumeOffsetAvailable(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + + VolumeControlService service = getService(source); + if (service == null) { + return false; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.isVolumeOffsetAvailable(device); + } + + @Override + public int getNumberOfVolumeOffsetInstances(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + + VolumeControlService service = getService(source); + if (service == null) { + return 0; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + return service.getNumberOfVolumeOffsetInstances(device); + } + + @Override + public void setVolumeOffset( + BluetoothDevice device, int instanceId, int volumeOffset, AttributionSource source) { + requireNonNull(device); + + VolumeControlService service = getService(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.setVolumeOffset(device, instanceId, volumeOffset); + } + + @Override + public void setDeviceVolume( + BluetoothDevice device, int volume, boolean isGroupOp, AttributionSource source) { + requireNonNull(device); + + VolumeControlService service = getService(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + service.setDeviceVolume(device, volume, isGroupOp); + } + + @Override + public void setGroupVolume(int groupId, int volume, AttributionSource source) { + VolumeControlService service = getService(source); + if (service == null) { + return; + } + service.setGroupVolume(groupId, volume); + } + + @Override + public int getGroupVolume(int groupId, AttributionSource source) { + VolumeControlService service = getService(source); + if (service == null) { + return 0; + } + return service.getGroupVolume(groupId); + } + + @Override + public void setGroupActive(int groupId, boolean active, AttributionSource source) { + VolumeControlService service = getService(source); + if (service == null) { + return; + } + service.setGroupActive(groupId, active); + } + + @Override + public void mute(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + + VolumeControlService service = getService(source); + if (service == null) { + return; + } + service.mute(device); + } + + @Override + public void muteGroup(int groupId, AttributionSource source) { + VolumeControlService service = getService(source); + if (service == null) { + return; + } + service.muteGroup(groupId); + } + + @Override + public void unmute(BluetoothDevice device, AttributionSource source) { + requireNonNull(device); + + VolumeControlService service = getService(source); + if (service == null) { + return; + } + service.unmute(device); + } + + @Override + public void unmuteGroup(int groupId, AttributionSource source) { + VolumeControlService service = getService(source); + if (service == null) { + return; + } + service.unmuteGroup(groupId); + } + + private static void postAndWait(Handler handler, Runnable runnable) { + FutureTask<Void> task = new FutureTask(Executors.callable(runnable)); + + handler.post(task); + try { + task.get(1, TimeUnit.SECONDS); + } catch (TimeoutException | InterruptedException e) { + SneakyThrow.sneakyThrow(e); + } catch (ExecutionException e) { + SneakyThrow.sneakyThrow(e.getCause()); + } + } + + @Override + public void registerCallback( + IBluetoothVolumeControlCallback callback, AttributionSource source) { + requireNonNull(callback); + + VolumeControlService service = getService(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + postAndWait(service.getHandler(), () -> service.registerCallback(callback)); + } + + @Override + public void unregisterCallback( + IBluetoothVolumeControlCallback callback, AttributionSource source) { + requireNonNull(callback); + + VolumeControlService service = getService(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + postAndWait(service.getHandler(), () -> service.unregisterCallback(callback)); + } + + @Override + public void notifyNewRegisteredCallback( + IBluetoothVolumeControlCallback callback, AttributionSource source) { + requireNonNull(callback); + + VolumeControlService service = getService(source); + if (service == null) { + return; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + postAndWait(service.getHandler(), () -> service.notifyNewRegisteredCallback(callback)); + } + + private static void validateBluetoothDevice(BluetoothDevice device) { + requireNonNull(device); + String address = device.getAddress(); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + throw new IllegalArgumentException("Invalid device address: " + address); + } + } + + @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) + private <R> R aicsWrapper( + AttributionSource source, + BluetoothDevice device, + Function<VolumeControlInputDescriptor, R> fn, + R defaultValue) { + validateBluetoothDevice(device); + + VolumeControlService service = getService(source); + if (service == null) { + return defaultValue; + } + + service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null); + + VolumeControlInputDescriptor inputs = service.getAudioInputs().get(device); + if (inputs == null) { + Log.w(TAG, "No audio inputs for " + device); + return defaultValue; + } + + return fn.apply(inputs); + } + + @Override + public int getNumberOfAudioInputControlServices( + AttributionSource source, BluetoothDevice device) { + validateBluetoothDevice(device); + Log.d(TAG, "getNumberOfAudioInputControlServices(" + device + ")"); + return aicsWrapper(source, device, i -> i.size(), 0); + } + + @Override + public void registerAudioInputControlCallback( + AttributionSource source, + BluetoothDevice device, + int instanceId, + IAudioInputCallback callback) { + requireNonNull(callback); + Log.d( + TAG, + "registerAudioInputControlCallback(" + + (device + ", " + instanceId + ", " + callback) + + ")"); + aicsWrapper( + source, + device, + i -> { + i.registerCallback(instanceId, callback); + return null; + }, + null); + } + + @Override + public void unregisterAudioInputControlCallback( + AttributionSource source, + BluetoothDevice device, + int instanceId, + IAudioInputCallback callback) { + requireNonNull(callback); + Log.d( + TAG, + "unregisterAudioInputControlCallback(" + + (device + ", " + instanceId + ", " + callback) + + ")"); + aicsWrapper( + source, + device, + i -> { + i.unregisterCallback(instanceId, callback); + return null; + }, + null); + } + + @Override + public int getAudioInputGainSettingUnit( + AttributionSource source, BluetoothDevice device, int instanceId) { + Log.d(TAG, "getAudioInputGainSettingUnit(" + device + ", " + instanceId + ")"); + return aicsWrapper(source, device, i -> i.getGainSettingUnit(instanceId), 0); + } + + @Override + public int getAudioInputGainSettingMin( + AttributionSource source, BluetoothDevice device, int instanceId) { + Log.d(TAG, "getAudioInputGainSettingMin(" + device + ", " + instanceId + ")"); + return aicsWrapper(source, device, i -> i.getGainSettingMin(instanceId), 0); + } + + @Override + public int getAudioInputGainSettingMax( + AttributionSource source, BluetoothDevice device, int instanceId) { + Log.d(TAG, "getAudioInputGainSettingMax(" + device + ", " + instanceId + ")"); + return aicsWrapper(source, device, i -> i.getGainSettingMax(instanceId), 0); + } + + @Override + public String getAudioInputDescription( + AttributionSource source, BluetoothDevice device, int instanceId) { + Log.d(TAG, "getAudioInputDescription(" + device + ", " + instanceId + ")"); + return aicsWrapper(source, device, i -> i.getDescription(instanceId), ""); + } + + @Override + public boolean isAudioInputDescriptionWritable( + AttributionSource source, BluetoothDevice device, int instanceId) { + Log.d(TAG, "isAudioInputDescriptionWritable(" + device + ", " + instanceId + ")"); + return aicsWrapper(source, device, i -> i.isDescriptionWritable(instanceId), false); + } + + @Override + public boolean setAudioInputDescription( + AttributionSource source, BluetoothDevice device, int instanceId, String description) { + requireNonNull(description); + Log.d(TAG, "setAudioInputDescription(" + device + ", " + instanceId + ")"); + return aicsWrapper(source, device, i -> i.setDescription(instanceId, description), false); + } + + @Override + public @AudioInputStatus int getAudioInputStatus( + AttributionSource source, BluetoothDevice device, int instanceId) { + Log.d(TAG, "getAudioInputStatus(" + device + ", " + instanceId + ")"); + return aicsWrapper( + source, + device, + i -> i.getStatus(instanceId), + (int) bluetooth.constants.aics.AudioInputStatus.INACTIVE); + } + + @Override + public @AudioInputType int getAudioInputType( + AttributionSource source, BluetoothDevice device, int instanceId) { + Log.d(TAG, "getAudioInputType(" + device + ", " + instanceId + ")"); + return aicsWrapper( + source, + device, + i -> i.getType(instanceId), + bluetooth.constants.AudioInputType.UNSPECIFIED); + } + + @Override + public int getAudioInputGainSetting( + AttributionSource source, BluetoothDevice device, int instanceId) { + Log.d(TAG, "getAudioInputGainSetting(" + device + ", " + instanceId + ")"); + return aicsWrapper(source, device, i -> i.getGainSetting(instanceId), 0); + } + + @Override + public boolean setAudioInputGainSetting( + AttributionSource source, BluetoothDevice device, int instanceId, int gainSetting) { + Log.d(TAG, "setAudioInputGainSetting(" + device + ", " + instanceId + ")"); + return aicsWrapper(source, device, i -> i.setGainSetting(instanceId, gainSetting), false); + } + + @Override + public @GainMode int getAudioInputGainMode( + AttributionSource source, BluetoothDevice device, int instanceId) { + Log.d(TAG, "getAudioInputGainMode(" + device + ", " + instanceId + ")"); + return aicsWrapper( + source, + device, + i -> i.getGainMode(instanceId), + (int) bluetooth.constants.aics.GainMode.AUTOMATIC_ONLY); + } + + @Override + public boolean setAudioInputGainMode( + AttributionSource source, + BluetoothDevice device, + int instanceId, + @GainMode int gainMode) { + Log.d(TAG, "setAudioInputGainMode(" + device + ", " + instanceId + ")"); + return aicsWrapper(source, device, i -> i.setGainMode(instanceId, gainMode), false); + } + + @Override + public @Mute int getAudioInputMute( + AttributionSource source, BluetoothDevice device, int instanceId) { + Log.d(TAG, "getAudioInputMute(" + device + ", " + instanceId + ")"); + return aicsWrapper( + source, + device, + i -> i.getMute(instanceId), + (int) bluetooth.constants.aics.Mute.DISABLED); + } + + @Override + public boolean setAudioInputMute( + AttributionSource source, BluetoothDevice device, int instanceId, @Mute int mute) { + Log.d(TAG, "setAudioInputMute(" + device + ", " + instanceId + ")"); + return aicsWrapper(source, device, i -> i.setMute(instanceId, mute), false); + } +} diff --git a/android/app/tests/instrumentation/com/android/bluetooth/opp/BluetoothOppLauncherActivityTest.java b/android/app/tests/instrumentation/com/android/bluetooth/opp/BluetoothOppLauncherActivityTest.java index fd9531105b..7bb7bbfbc3 100644 --- a/android/app/tests/instrumentation/com/android/bluetooth/opp/BluetoothOppLauncherActivityTest.java +++ b/android/app/tests/instrumentation/com/android/bluetooth/opp/BluetoothOppLauncherActivityTest.java @@ -46,6 +46,7 @@ import android.net.Uri; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; +import android.provider.Settings; import android.sysprop.BluetoothProperties; import androidx.lifecycle.Lifecycle; @@ -382,6 +383,30 @@ public class BluetoothOppLauncherActivityTest { assertThat(argument.getValue().getData()).isEqualTo(Uri.EMPTY); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_SEND_OPP_DEVICE_PICKER_EXTRA_INTENT) + public void onCreate_withActionSend_grantUriPermissionToNearbyComponent() { + doReturn(true).when(mMethodProxy).bluetoothAdapterIsEnabled(any()); + doReturn(PackageManager.PERMISSION_GRANTED) + .when(mMethodProxy) + .componentCallerCheckContentUriPermission(any(), any(), anyInt()); + String uriString = "content://test.provider/1"; + Settings.Secure.putString( + mTargetContext.getContentResolver(), + "nearby_sharing_component", + "com.example/.BComponent"); + + ActivityScenario<BluetoothOppLauncherActivity> unused = + ActivityScenario.launch(createSendIntent(uriString)); + + verify(mMethodProxy) + .grantUriPermission( + any(), + eq("com.example"), + eq(Uri.parse(uriString)), + eq(Intent.FLAG_GRANT_READ_URI_PERMISSION)); + } + @Ignore("b/263724420") @Test public void launchDevicePicker_bluetoothNotEnabled_launchEnableActivity() throws Exception { diff --git a/android/app/tests/unit/Android.bp b/android/app/tests/unit/Android.bp index 01d195cc43..a5b7dc36c8 100644 --- a/android/app/tests/unit/Android.bp +++ b/android/app/tests/unit/Android.bp @@ -35,6 +35,7 @@ android_test { "androidx.room_room-runtime", "androidx.room_room-testing", "androidx.test.espresso.intents", + "androidx.test.ext.junit", "androidx.test.ext.truth", "androidx.test.rules", "androidx.test.uiautomator_uiautomator", @@ -45,6 +46,7 @@ android_test { "gson", "guava-android-testlib", "mmslib", + "mockito-kotlin2", "mockito-target-extended", "modules-utils-handlerexecutor", "platform-parametric-runner-lib", @@ -61,8 +63,11 @@ android_test { jarjar_rules: ":bluetooth-jarjar-rules", asset_dirs: ["src/com/android/bluetooth/btservice/storage/schemas"], - // Include all test java files. - srcs: ["src/**/*.java"], + // Include all test java and kotlin files. + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], jacoco: { include_filter: ["android.bluetooth.*"], exclude_filter: [], diff --git a/android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceBinderTest.java index 23e7a5ed1c..65d00773ba 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceBinderTest.java @@ -36,15 +36,20 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.platform.test.flag.junit.SetFlagsRule; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; +/** Test cases for {@link A2dpServiceBinder} */ +@SmallTest +@RunWith(AndroidJUnit4.class) public class A2dpServiceBinderTest { - private static final AttributionSource sSource = new AttributionSource.Builder(0).build(); - private static final BluetoothDevice sDevice = getTestDevice(0); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -53,7 +58,10 @@ public class A2dpServiceBinderTest { @Mock private A2dpService mA2dpService; @Mock private PackageManager mPackageManager; - private A2dpService.BluetoothA2dpBinder mBinder; + private final AttributionSource sSource = new AttributionSource.Builder(0).build(); + private final BluetoothDevice sDevice = getTestDevice(0); + + private A2dpServiceBinder mBinder; @Before public void setUp() throws Exception { @@ -62,7 +70,7 @@ public class A2dpServiceBinderTest { appInfo.targetSdkVersion = android.os.Build.VERSION_CODES.CUR_DEVELOPMENT; doReturn(appInfo).when(mPackageManager).getApplicationInfo(any(), anyInt()); - mBinder = new A2dpService.BluetoothA2dpBinder(mA2dpService); + mBinder = new A2dpServiceBinder(mA2dpService); } @After diff --git a/android/app/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceBinderTest.java index aad4f1019a..6f7723caa9 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceBinderTest.java @@ -27,21 +27,29 @@ import static org.mockito.Mockito.verify; import android.bluetooth.BluetoothDevice; import android.content.AttributionSource; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; +/** Test cases for {@link A2dpSinkServiceBinder} */ +@SmallTest +@RunWith(AndroidJUnit4.class) public class A2dpSinkServiceBinderTest { + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @Mock private A2dpSinkService mService; - private A2dpSinkService.A2dpSinkServiceBinder mBinder; + private A2dpSinkServiceBinder mBinder; @Before public void setUp() throws Exception { - mBinder = new A2dpSinkService.A2dpSinkServiceBinder(mService); + mBinder = new A2dpSinkServiceBinder(mService); } @After diff --git a/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerServiceBinderTest.java index 2cbd8f356a..efd371dfdd 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerServiceBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerServiceBinderTest.java @@ -34,41 +34,41 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +/** Test cases for {@link AvrcpControllerServiceBinder} */ @SmallTest @RunWith(AndroidJUnit4.class) public class AvrcpControllerServiceBinderTest { + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @Mock private AvrcpControllerService mService; private final BluetoothDevice mDevice = getTestDevice(49); - AvrcpControllerService.AvrcpControllerServiceBinder mBinder; + private AvrcpControllerServiceBinder mBinder; @Before public void setUp() throws Exception { - mBinder = new AvrcpControllerService.AvrcpControllerServiceBinder(mService); + mBinder = new AvrcpControllerServiceBinder(mService); } @Test public void getConnectedDevices_callsServiceMethod() { mBinder.getConnectedDevices(null); - verify(mService).getConnectedDevices(); } @Test public void getDevicesMatchingConnectionStates_callsServiceMethod() { int[] states = new int[] {STATE_CONNECTED}; - mBinder.getDevicesMatchingConnectionStates(states, null); + mBinder.getDevicesMatchingConnectionStates(states, null); verify(mService).getDevicesMatchingConnectionStates(states); } @Test public void getConnectionState_callsServiceMethod() { mBinder.getConnectionState(mDevice, null); - verify(mService).getConnectionState(mDevice); } diff --git a/android/app/tests/unit/src/com/android/bluetooth/bass_client/BleBroadcastAssistantBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceBinderTest.java index ae402d7d0f..584f0cdeea 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/bass_client/BleBroadcastAssistantBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceBinderTest.java @@ -36,21 +36,24 @@ import android.bluetooth.IBluetoothLeBroadcastAssistantCallback; import android.bluetooth.le.ScanFilter; import android.content.AttributionSource; +import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; import org.mockito.Mock; import org.mockito.Mockito; import java.util.Collections; import java.util.List; -@RunWith(JUnit4.class) -public class BleBroadcastAssistantBinderTest { +/** Test cases for {@link BassClientServiceBinder} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class BassClientServiceBinderTest { @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @@ -64,17 +67,16 @@ public class BleBroadcastAssistantBinderTest { @Mock private BassClientService mService; - private BassClientService.BluetoothLeBroadcastAssistantBinder mBinder; + private BassClientServiceBinder mBinder; @Before public void setUp() { - mBinder = new BassClientService.BluetoothLeBroadcastAssistantBinder(mService); + mBinder = new BassClientServiceBinder(mService); } @Test public void cleanUp() { mBinder.cleanup(); - assertThat(mBinder.mService).isNull(); } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceBinderTest.java index 955f579ce4..7e143a2851 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceBinderTest.java @@ -30,15 +30,21 @@ import android.bluetooth.IBluetoothOobDataCallback; import android.content.AttributionSource; import android.os.ParcelUuid; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import java.io.FileDescriptor; -/** Test cases for {@link AdapterServiceBinder}. */ +/** Test cases for {@link AdapterServiceBinder} */ +@SmallTest +@RunWith(AndroidJUnit4.class) public class AdapterServiceBinderTest { @Rule public final MockitoRule mMockitoRule = new MockitoRule(); diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/MetricsLoggerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/MetricsLoggerTest.java index 1b057b3a7e..ae3db21382 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/btservice/MetricsLoggerTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/MetricsLoggerTest.java @@ -20,12 +20,24 @@ import static com.android.bluetooth.TestUtils.getTestDevice; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + import android.bluetooth.BluetoothDevice; +import android.content.ContentResolver; +import android.provider.Settings; +import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.MediumTest; import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.BluetoothMetricsProto.BluetoothRemoteDeviceInformation; +import com.android.bluetooth.BluetoothStatsLog; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; @@ -37,9 +49,12 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.HashMap; import java.util.Map; @@ -236,6 +251,51 @@ public class MetricsLoggerTest { assertThat(mTestableMetricsLogger.logAllowlistedDeviceNameHash(1, "")).isEmpty(); } + @Test + public void testUpdateHearingDeviceActiveTime() { + BluetoothDevice bluetoothDevice = getTestDevice(0); + int day = BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__DAY; + int week = BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__WEEK; + int month = BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__MONTH; + doReturn(ApplicationProvider.getApplicationContext().getContentResolver()) + .when(mAdapterService) + .getContentResolver(); + + // last active time is 2 days ago, should update last active day + TestableMetricsLogger logger = spy(mTestableMetricsLogger); + prepareLastActiveTimeDaysAgo(2); + logger.updateHearingDeviceActiveTime(bluetoothDevice, 1); + verify(logger).logHearingDeviceActiveEvent(any(), anyInt(), eq(day)); + verify(logger, never()).logHearingDeviceActiveEvent(any(), anyInt(), eq(week)); + verify(logger, never()).logHearingDeviceActiveEvent(any(), anyInt(), eq(month)); + + // last active time is 8 days ago, should update last active day and week + Mockito.reset(logger); + prepareLastActiveTimeDaysAgo(8); + logger.updateHearingDeviceActiveTime(bluetoothDevice, 1); + verify(logger).logHearingDeviceActiveEvent(any(), anyInt(), eq(day)); + verify(logger).logHearingDeviceActiveEvent(any(), anyInt(), eq(week)); + verify(logger, never()).logHearingDeviceActiveEvent(any(), anyInt(), eq(month)); + + // last active time is 60 days ago, should update last active day, week and month + Mockito.reset(logger); + prepareLastActiveTimeDaysAgo(60); + logger.updateHearingDeviceActiveTime(bluetoothDevice, 1); + verify(logger).logHearingDeviceActiveEvent(any(), anyInt(), eq(day)); + verify(logger).logHearingDeviceActiveEvent(any(), anyInt(), eq(week)); + verify(logger).logHearingDeviceActiveEvent(any(), anyInt(), eq(month)); + } + + private static void prepareLastActiveTimeDaysAgo(int days) { + final ContentResolver contentResolver = + ApplicationProvider.getApplicationContext().getContentResolver(); + final LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault()); + final String lastActive = now.minusDays(days).toString(); + Settings.Secure.putString(contentResolver, "last_active_day", lastActive); + Settings.Secure.putString(contentResolver, "last_active_week", lastActive); + Settings.Secure.putString(contentResolver, "last_active_month", lastActive); + } + private void initTestingBloomfilter() throws IOException { byte[] bloomfilterData = DeviceBloomfilterGenerator.hexStringToByteArray( diff --git a/android/app/tests/unit/src/com/android/bluetooth/csip/BluetoothCsisBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/csip/CsipSetCoordinatorServiceBinderTest.java index 9233602c47..81adae18f4 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/csip/BluetoothCsisBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/csip/CsipSetCoordinatorServiceBinderTest.java @@ -30,12 +30,20 @@ import android.bluetooth.IBluetoothCsipSetCoordinatorLockCallback; import android.content.AttributionSource; import android.os.ParcelUuid; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; -public class BluetoothCsisBinderTest { +/** Test cases for {@link CsipSetCoordinatorServiceBinder} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class CsipSetCoordinatorServiceBinderTest { + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @Mock private CsipSetCoordinatorService mService; @@ -43,11 +51,11 @@ public class BluetoothCsisBinderTest { private final BluetoothDevice mDevice = getTestDevice(45); private final AttributionSource mAttributionSource = new AttributionSource.Builder(1).build(); - private CsipSetCoordinatorService.BluetoothCsisBinder mBinder; + private CsipSetCoordinatorServiceBinder mBinder; @Before public void setUp() throws Exception { - mBinder = new CsipSetCoordinatorService.BluetoothCsisBinder(mService); + mBinder = new CsipSetCoordinatorServiceBinder(mService); } @Test @@ -59,6 +67,7 @@ public class BluetoothCsisBinderTest { @Test public void getDevicesMatchingConnectionStates() { int[] states = new int[] {STATE_CONNECTED}; + mBinder.getDevicesMatchingConnectionStates(states, mAttributionSource); verify(mService).getDevicesMatchingConnectionStates(states); } @@ -72,6 +81,7 @@ public class BluetoothCsisBinderTest { @Test public void setConnectionPolicy() { int connectionPolicy = CONNECTION_POLICY_ALLOWED; + mBinder.setConnectionPolicy(mDevice, connectionPolicy, mAttributionSource); verify(mService).setConnectionPolicy(mDevice, connectionPolicy); } @@ -87,6 +97,7 @@ public class BluetoothCsisBinderTest { int groupId = 100; IBluetoothCsipSetCoordinatorLockCallback cb = mock(IBluetoothCsipSetCoordinatorLockCallback.class); + mBinder.lockGroup(groupId, cb, mAttributionSource); verify(mService).lockGroup(groupId, cb); } @@ -94,6 +105,7 @@ public class BluetoothCsisBinderTest { @Test public void unlockGroup() { ParcelUuid uuid = ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB"); + mBinder.unlockGroup(uuid, mAttributionSource); verify(mService).unlockGroup(uuid.getUuid()); } @@ -101,6 +113,7 @@ public class BluetoothCsisBinderTest { @Test public void getAllGroupIds() { ParcelUuid uuid = ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB"); + mBinder.getAllGroupIds(uuid, mAttributionSource); verify(mService).getAllGroupIds(uuid); } diff --git a/android/app/tests/unit/src/com/android/bluetooth/csip/CsipSetCoordinatorServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/csip/CsipSetCoordinatorServiceTest.java index ac64b097ae..72fbd125b6 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/csip/CsipSetCoordinatorServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/csip/CsipSetCoordinatorServiceTest.java @@ -130,7 +130,6 @@ public class CsipSetCoordinatorServiceTest { new CsipSetCoordinatorService( mAdapterService, mLooper.getLooper(), mNativeInterface, mServiceFactory); mService.setAvailable(true); - } @After diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.java deleted file mode 100644 index c78c9ec6db..0000000000 --- a/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2025 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.gatt; - -import static com.android.bluetooth.TestUtils.MockitoRule; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import android.bluetooth.BluetoothManager; -import android.bluetooth.le.AdvertiseData; -import android.bluetooth.le.AdvertisingSetParameters; -import android.bluetooth.le.IAdvertisingSetCallback; -import android.bluetooth.le.PeriodicAdvertisingParameters; -import android.content.AttributionSource; - -import androidx.test.filters.SmallTest; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import com.android.bluetooth.btservice.AdapterService; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; - -/** Test cases for {@link AdvertiseBinder}. */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class AdvertiseBinderTest { - @Rule public final MockitoRule mMockitoRule = new MockitoRule(); - - @Mock private AdapterService mAdapterService; - @Mock private AdvertiseManager mAdvertiseManager; - - private final AttributionSource mAttributionSource = - InstrumentationRegistry.getInstrumentation() - .getTargetContext() - .getSystemService(BluetoothManager.class) - .getAdapter() - .getAttributionSource(); - private AdvertiseBinder mBinder; - - @Before - public void setUp() { - doAnswer( - invocation -> { - ((Runnable) invocation.getArgument(0)).run(); - return null; - }) - .when(mAdvertiseManager) - .doOnAdvertiseThread(any()); - mBinder = new AdvertiseBinder(mAdapterService, mAdvertiseManager); - } - - @Test - public void startAdvertisingSet() { - AdvertisingSetParameters parameters = new AdvertisingSetParameters.Builder().build(); - AdvertiseData advertiseData = new AdvertiseData.Builder().build(); - AdvertiseData scanResponse = new AdvertiseData.Builder().build(); - PeriodicAdvertisingParameters periodicParameters = - new PeriodicAdvertisingParameters.Builder().build(); - AdvertiseData periodicData = new AdvertiseData.Builder().build(); - int duration = 1; - int maxExtAdvEvents = 2; - int serverIf = 3; - IAdvertisingSetCallback callback = mock(IAdvertisingSetCallback.class); - - mBinder.startAdvertisingSet( - parameters, - advertiseData, - scanResponse, - periodicParameters, - periodicData, - duration, - maxExtAdvEvents, - serverIf, - callback, - mAttributionSource); - - verify(mAdvertiseManager) - .startAdvertisingSet( - parameters, - advertiseData, - scanResponse, - periodicParameters, - periodicData, - duration, - maxExtAdvEvents, - serverIf, - callback, - mAttributionSource); - } - - @Test - public void stopAdvertisingSet() { - IAdvertisingSetCallback callback = mock(IAdvertisingSetCallback.class); - - mBinder.stopAdvertisingSet(callback, mAttributionSource); - - verify(mAdvertiseManager).stopAdvertisingSet(callback); - } - - @Test - public void setAdvertisingData() { - int advertiserId = 1; - AdvertiseData data = new AdvertiseData.Builder().build(); - - mBinder.setAdvertisingData(advertiserId, data, mAttributionSource); - verify(mAdvertiseManager).setAdvertisingData(advertiserId, data); - } - - @Test - public void setAdvertisingParameters() { - int advertiserId = 1; - AdvertisingSetParameters parameters = new AdvertisingSetParameters.Builder().build(); - - mBinder.setAdvertisingParameters(advertiserId, parameters, mAttributionSource); - verify(mAdvertiseManager).setAdvertisingParameters(advertiserId, parameters); - } - - @Test - public void setPeriodicAdvertisingData() { - int advertiserId = 1; - AdvertiseData data = new AdvertiseData.Builder().build(); - - mBinder.setPeriodicAdvertisingData(advertiserId, data, mAttributionSource); - verify(mAdvertiseManager).setPeriodicAdvertisingData(advertiserId, data); - } - - @Test - public void setPeriodicAdvertisingEnable() { - int advertiserId = 1; - boolean enable = true; - - mBinder.setPeriodicAdvertisingEnable(advertiserId, enable, mAttributionSource); - verify(mAdvertiseManager).setPeriodicAdvertisingEnable(advertiserId, enable); - } - - @Test - public void setPeriodicAdvertisingParameters() { - int advertiserId = 1; - PeriodicAdvertisingParameters parameters = - new PeriodicAdvertisingParameters.Builder().build(); - - mBinder.setPeriodicAdvertisingParameters(advertiserId, parameters, mAttributionSource); - verify(mAdvertiseManager).setPeriodicAdvertisingParameters(advertiserId, parameters); - } - - @Test - public void setScanResponseData() { - int advertiserId = 1; - AdvertiseData data = new AdvertiseData.Builder().build(); - - mBinder.setScanResponseData(advertiserId, data, mAttributionSource); - verify(mAdvertiseManager).setScanResponseData(advertiserId, data); - } - - @Test - public void getOwnAddress() { - int advertiserId = 1; - - mBinder.getOwnAddress(advertiserId, mAttributionSource); - verify(mAdvertiseManager).getOwnAddress(advertiserId); - } - - @Test - public void enableAdvertisingSet() { - int advertiserId = 1; - boolean enable = true; - int duration = 3; - int maxExtAdvEvents = 4; - - mBinder.enableAdvertisingSet( - advertiserId, enable, duration, maxExtAdvEvents, mAttributionSource); - verify(mAdvertiseManager) - .enableAdvertisingSet(advertiserId, enable, duration, maxExtAdvEvents); - } - - @Test - public void cleanUp_doesNotCrash() { - mBinder.cleanup(); - } -} diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.kt b/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.kt new file mode 100644 index 0000000000..6b038678f0 --- /dev/null +++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2025 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.gatt + +import android.bluetooth.BluetoothManager +import android.bluetooth.le.AdvertiseData +import android.bluetooth.le.AdvertisingSetParameters +import android.bluetooth.le.IAdvertisingSetCallback +import android.bluetooth.le.PeriodicAdvertisingParameters +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.bluetooth.TestUtils.MockitoRule +import com.android.bluetooth.btservice.AdapterService +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.whenever + +/** Test cases for [AdvertiseBinder] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class AdvertiseBinderTest { + + @get:Rule val mockitoRule = MockitoRule() + + @Mock private lateinit var adapterService: AdapterService + @Mock private lateinit var advertiseManager: AdvertiseManager + + private val attributionSource = + InstrumentationRegistry.getInstrumentation() + .targetContext + .getSystemService(BluetoothManager::class.java) + .adapter + .attributionSource + + private lateinit var binder: AdvertiseBinder + + @Before + fun setUp() { + doAnswer { invocation -> + (invocation.getArgument(0) as Runnable).run() + null + } + .whenever(advertiseManager) + .doOnAdvertiseThread(any()) + binder = AdvertiseBinder(adapterService, advertiseManager) + } + + @Test + fun startAdvertisingSet() { + val parameters = AdvertisingSetParameters.Builder().build() + val advertiseData = AdvertiseData.Builder().build() + val scanResponse = AdvertiseData.Builder().build() + val periodicParameters = PeriodicAdvertisingParameters.Builder().build() + val periodicData = AdvertiseData.Builder().build() + val duration = 1 + val maxExtAdvEvents = 2 + val serverIf = 3 + val callback = mock(IAdvertisingSetCallback::class.java) + + binder.startAdvertisingSet( + parameters, + advertiseData, + scanResponse, + periodicParameters, + periodicData, + duration, + maxExtAdvEvents, + serverIf, + callback, + attributionSource, + ) + verify(advertiseManager) + .startAdvertisingSet( + parameters, + advertiseData, + scanResponse, + periodicParameters, + periodicData, + duration, + maxExtAdvEvents, + serverIf, + callback, + attributionSource, + ) + } + + @Test + fun stopAdvertisingSet() { + val callback = mock(IAdvertisingSetCallback::class.java) + + binder.stopAdvertisingSet(callback, attributionSource) + verify(advertiseManager).stopAdvertisingSet(callback) + } + + @Test + fun setAdvertisingData() { + val advertiserId = 1 + val data = AdvertiseData.Builder().build() + + binder.setAdvertisingData(advertiserId, data, attributionSource) + verify(advertiseManager).setAdvertisingData(advertiserId, data) + } + + @Test + fun setAdvertisingParameters() { + val advertiserId = 1 + val parameters = AdvertisingSetParameters.Builder().build() + + binder.setAdvertisingParameters(advertiserId, parameters, attributionSource) + verify(advertiseManager).setAdvertisingParameters(advertiserId, parameters) + } + + @Test + fun setPeriodicAdvertisingData() { + val advertiserId = 1 + val data = AdvertiseData.Builder().build() + + binder.setPeriodicAdvertisingData(advertiserId, data, attributionSource) + verify(advertiseManager).setPeriodicAdvertisingData(advertiserId, data) + } + + @Test + fun setPeriodicAdvertisingEnable() { + val advertiserId = 1 + val enable = true + + binder.setPeriodicAdvertisingEnable(advertiserId, enable, attributionSource) + verify(advertiseManager).setPeriodicAdvertisingEnable(advertiserId, enable) + } + + @Test + fun setPeriodicAdvertisingParameters() { + val advertiserId = 1 + val parameters = PeriodicAdvertisingParameters.Builder().build() + + binder.setPeriodicAdvertisingParameters(advertiserId, parameters, attributionSource) + verify(advertiseManager).setPeriodicAdvertisingParameters(advertiserId, parameters) + } + + @Test + fun setScanResponseData() { + val advertiserId = 1 + val data = AdvertiseData.Builder().build() + + binder.setScanResponseData(advertiserId, data, attributionSource) + verify(advertiseManager).setScanResponseData(advertiserId, data) + } + + @Test + fun getOwnAddress() { + val advertiserId = 1 + + binder.getOwnAddress(advertiserId, attributionSource) + verify(advertiseManager).getOwnAddress(advertiserId) + } + + @Test + fun enableAdvertisingSet() { + val advertiserId = 1 + val enable = true + val duration = 3 + val maxExtAdvEvents = 4 + + binder.enableAdvertisingSet( + advertiserId, + enable, + duration, + maxExtAdvEvents, + attributionSource, + ) + verify(advertiseManager) + .enableAdvertisingSet(advertiserId, enable, duration, maxExtAdvEvents) + } + + @Test + fun cleanup_doesNotCrash() { + binder.cleanup() + } +} diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/DistanceMeasurementBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/DistanceMeasurementBinderTest.java deleted file mode 100644 index a65c805a47..0000000000 --- a/android/app/tests/unit/src/com/android/bluetooth/gatt/DistanceMeasurementBinderTest.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2025 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.gatt; - -import static com.android.bluetooth.TestUtils.MockitoRule; -import static com.android.bluetooth.TestUtils.getTestDevice; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothManager; -import android.bluetooth.le.DistanceMeasurementMethod; -import android.bluetooth.le.DistanceMeasurementParams; -import android.bluetooth.le.IDistanceMeasurementCallback; -import android.content.AttributionSource; -import android.os.ParcelUuid; - -import androidx.test.filters.SmallTest; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import com.android.bluetooth.btservice.AdapterService; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; - -import java.util.Collections; -import java.util.UUID; - -/** Test cases for {@link DistanceMeasurementBinder}. */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class DistanceMeasurementBinderTest { - @Rule public final MockitoRule mMockitoRule = new MockitoRule(); - - @Mock private DistanceMeasurementManager mDistanceMeasurementManager; - @Mock private AdapterService mAdapterService; - - private final AttributionSource mAttributionSource = - InstrumentationRegistry.getInstrumentation() - .getTargetContext() - .getSystemService(BluetoothManager.class) - .getAdapter() - .getAttributionSource(); - - private DistanceMeasurementBinder mBinder; - - @Before - public void setUp() throws Throwable { - mBinder = new DistanceMeasurementBinder(mAdapterService, mDistanceMeasurementManager); - when(mDistanceMeasurementManager.getSupportedDistanceMeasurementMethods()) - .thenReturn(Collections.emptyList()); - when(mDistanceMeasurementManager.runOnDistanceMeasurementThreadAndWaitForResult(any())) - .thenAnswer( - invocationOnMock -> { - DistanceMeasurementManager.GetResultTask task = - invocationOnMock.getArgument(0); - return task.getResult(); - }); - doAnswer( - invocation -> { - ((Runnable) (invocation.getArgument(0))).run(); - return null; - }) - .when(mDistanceMeasurementManager) - .postOnDistanceMeasurementThread(any()); - } - - @Test - public void getSupportedDistanceMeasurementMethods() { - mBinder.getSupportedDistanceMeasurementMethods(mAttributionSource); - verify(mDistanceMeasurementManager).getSupportedDistanceMeasurementMethods(); - } - - @Test - public void startDistanceMeasurement() { - UUID uuid = UUID.randomUUID(); - BluetoothDevice device = getTestDevice(3); - DistanceMeasurementParams params = - new DistanceMeasurementParams.Builder(device) - .setDurationSeconds(123) - .setFrequency(DistanceMeasurementParams.REPORT_FREQUENCY_LOW) - .build(); - IDistanceMeasurementCallback callback = mock(IDistanceMeasurementCallback.class); - mBinder.startDistanceMeasurement( - new ParcelUuid(uuid), params, callback, mAttributionSource); - verify(mDistanceMeasurementManager).startDistanceMeasurement(uuid, params, callback); - } - - @Test - public void stopDistanceMeasurement() { - UUID uuid = UUID.randomUUID(); - BluetoothDevice device = getTestDevice(3); - int method = DistanceMeasurementMethod.DISTANCE_MEASUREMENT_METHOD_RSSI; - mBinder.stopDistanceMeasurement(new ParcelUuid(uuid), device, method, mAttributionSource); - verify(mDistanceMeasurementManager).stopDistanceMeasurement(uuid, device, method, false); - } -} diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/DistanceMeasurementBinderTest.kt b/android/app/tests/unit/src/com/android/bluetooth/gatt/DistanceMeasurementBinderTest.kt new file mode 100644 index 0000000000..b20eda9a17 --- /dev/null +++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/DistanceMeasurementBinderTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2025 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.gatt + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager +import android.bluetooth.le.DistanceMeasurementMethod +import android.bluetooth.le.DistanceMeasurementParams +import android.bluetooth.le.IDistanceMeasurementCallback +import android.os.ParcelUuid +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.bluetooth.TestUtils.MockitoRule +import com.android.bluetooth.TestUtils.getTestDevice +import com.android.bluetooth.btservice.AdapterService +import com.android.bluetooth.gatt.DistanceMeasurementManager.GetResultTask +import java.util.UUID +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.whenever + +/** Test cases for [DistanceMeasurementBinder] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class DistanceMeasurementBinderTest { + + @get:Rule val mockitoRule = MockitoRule() + + @Mock private lateinit var distanceMeasurementManager: DistanceMeasurementManager + @Mock private lateinit var adapterService: AdapterService + + private val attributionSource = + InstrumentationRegistry.getInstrumentation() + .targetContext + .getSystemService(BluetoothManager::class.java) + .adapter + .attributionSource + + private lateinit var binder: DistanceMeasurementBinder + + @Before + fun setUp() { + binder = DistanceMeasurementBinder(adapterService, distanceMeasurementManager) + doReturn(emptyList<DistanceMeasurementMethod>()) + .whenever(distanceMeasurementManager) + .getSupportedDistanceMeasurementMethods() + doAnswer { invocationOnMock -> + val task = invocationOnMock.getArgument<GetResultTask<*>>(0) + task.result + } + .whenever(distanceMeasurementManager) + .runOnDistanceMeasurementThreadAndWaitForResult(any<GetResultTask<*>>()) + doAnswer { invocation -> + (invocation.getArgument(0) as Runnable).run() + null + } + .whenever(distanceMeasurementManager) + .postOnDistanceMeasurementThread(any()) + } + + @Test + fun getSupportedDistanceMeasurementMethods() { + binder.getSupportedDistanceMeasurementMethods(attributionSource) + verify(distanceMeasurementManager).supportedDistanceMeasurementMethods + } + + @Test + fun startDistanceMeasurement() { + val uuid = UUID.randomUUID() + val device: BluetoothDevice = getTestDevice(3) + val params = + DistanceMeasurementParams.Builder(device) + .setDurationSeconds(123) + .setFrequency(DistanceMeasurementParams.REPORT_FREQUENCY_LOW) + .build() + val callback = mock(IDistanceMeasurementCallback::class.java) + binder.startDistanceMeasurement(ParcelUuid(uuid), params, callback, attributionSource) + verify(distanceMeasurementManager).startDistanceMeasurement(uuid, params, callback) + } + + @Test + fun stopDistanceMeasurement() { + val uuid = UUID.randomUUID() + val device: BluetoothDevice = getTestDevice(3) + val method = DistanceMeasurementMethod.DISTANCE_MEASUREMENT_METHOD_RSSI + binder.stopDistanceMeasurement(ParcelUuid(uuid), device, method, attributionSource) + verify(distanceMeasurementManager).stopDistanceMeasurement(uuid, device, method, false) + } +} diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceBinderTest.java index 1a340840bc..f9bb1d556d 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceBinderTest.java @@ -42,6 +42,7 @@ import org.mockito.Mock; import java.util.UUID; +/** Test cases for {@link GattServiceBinder} */ @SmallTest @RunWith(AndroidJUnit4.class) public class GattServiceBinderTest { diff --git a/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientServiceBinderTest.java index dd5ede57b2..3544578497 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientServiceBinderTest.java @@ -44,9 +44,10 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; +/** Test cases for {@link HapClientServiceBinder} */ @MediumTest @RunWith(AndroidJUnit4.class) -public class HapClientBinderTest { +public class HapClientServiceBinderTest { @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @Mock private HapClientService mHapClientService; @@ -59,11 +60,11 @@ public class HapClientBinderTest { private final AttributionSource mAttributionSource = mAdapter.getAttributionSource(); private final BluetoothDevice mDevice = getTestDevice(0); - private HapClientBinder mBinder; + private HapClientServiceBinder mBinder; @Before public void setUp() throws Exception { - mBinder = new HapClientBinder(mHapClientService); + mBinder = new HapClientServiceBinder(mHapClientService); } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceBinderTest.java new file mode 100644 index 0000000000..8dae3bf594 --- /dev/null +++ b/android/app/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceBinderTest.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2025 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.hearingaid; + +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; +import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import static com.android.bluetooth.TestUtils.MockitoRule; +import static com.android.bluetooth.TestUtils.getTestDevice; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHearingAid; +import android.bluetooth.BluetoothManager; +import android.content.AttributionSource; +import android.content.Context; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.util.ArrayList; +import java.util.List; + +/** Test cases for {@link HearingAidServiceBinder} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class HearingAidServiceBinderTest { + + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); + + @Mock private HearingAidService mService; + + private HearingAidServiceBinder mBinder; + private AttributionSource mAttributionSource; + private BluetoothDevice mTestDevice; + + @Before + public void setUp() throws Exception { + when(mService.isAvailable()).thenReturn(true); + mBinder = new HearingAidServiceBinder(mService); + Context context = InstrumentationRegistry.getTargetContext(); + mAttributionSource = + context.getSystemService(BluetoothManager.class) + .getAdapter() + .getAttributionSource(); + mTestDevice = getTestDevice(0); + } + + @Test + public void connect() { + mBinder.connect(mTestDevice, mAttributionSource); + verify(mService).connect(mTestDevice); + } + + @Test + public void disconnect() { + mBinder.disconnect(mTestDevice, mAttributionSource); + verify(mService).disconnect(mTestDevice); + } + + @Test + public void getConnectedDevices() { + List<BluetoothDevice> connectedDevices = new ArrayList<>(); + connectedDevices.add(mTestDevice); + when(mService.getConnectedDevices()).thenReturn(connectedDevices); + + mBinder.getConnectedDevices(mAttributionSource); + verify(mService).getConnectedDevices(); + } + + @Test + public void getDevicesMatchingConnectionStates() { + int[] states = new int[] {STATE_CONNECTED, STATE_DISCONNECTED}; + List<BluetoothDevice> devices = new ArrayList<>(); + devices.add(mTestDevice); + when(mService.getDevicesMatchingConnectionStates(states)).thenReturn(devices); + + mBinder.getDevicesMatchingConnectionStates(states, mAttributionSource); + verify(mService).getDevicesMatchingConnectionStates(states); + } + + @Test + public void getConnectionState() { + when(mService.getConnectionState(mTestDevice)).thenReturn(STATE_CONNECTED); + + mBinder.getConnectionState(mTestDevice, mAttributionSource); + verify(mService).getConnectionState(mTestDevice); + } + + @Test + public void setActiveDevice() { + mBinder.setActiveDevice(mTestDevice, mAttributionSource); + verify(mService).setActiveDevice(mTestDevice); + } + + @Test + public void removeActiveDevice() { + mBinder.setActiveDevice(null, mAttributionSource); + verify(mService).removeActiveDevice(false); + } + + @Test + public void getActiveDevices() { + List<BluetoothDevice> activeDevices = new ArrayList<>(); + activeDevices.add(mTestDevice); + when(mService.getActiveDevices()).thenReturn(activeDevices); + + mBinder.getActiveDevices(mAttributionSource); + verify(mService).getActiveDevices(); + } + + @Test + public void setConnectionPolicy() { + mBinder.setConnectionPolicy(mTestDevice, CONNECTION_POLICY_ALLOWED, mAttributionSource); + verify(mService).setConnectionPolicy(mTestDevice, CONNECTION_POLICY_ALLOWED); + } + + @Test + public void getConnectionPolicy() { + when(mService.getConnectionPolicy(mTestDevice)).thenReturn(CONNECTION_POLICY_FORBIDDEN); + + mBinder.getConnectionPolicy(mTestDevice, mAttributionSource); + verify(mService).getConnectionPolicy(mTestDevice); + } + + @Test + public void setVolume() { + int volume = 50; + + mBinder.setVolume(volume, mAttributionSource); + verify(mService).setVolume(volume); + } + + @Test + public void getHiSyncId() { + long hiSyncId = 1234567890L; + when(mService.getHiSyncId(mTestDevice)).thenReturn(hiSyncId); + + mBinder.getHiSyncId(mTestDevice, mAttributionSource); + verify(mService).getHiSyncId(mTestDevice); + } + + @Test + public void getDeviceSide() { + int side = BluetoothHearingAid.SIDE_LEFT; + when(mService.getCapabilities(mTestDevice)).thenReturn(side); + + mBinder.getDeviceSide(mTestDevice, mAttributionSource); + verify(mService).getCapabilities(mTestDevice); + } + + @Test + public void getDeviceMode() { + int mode = BluetoothHearingAid.MODE_BINAURAL; + when(mService.getCapabilities(mTestDevice)).thenReturn(mode << 1); + + mBinder.getDeviceMode(mTestDevice, mAttributionSource); + verify(mService).getCapabilities(mTestDevice); + } + + @Test + public void getAdvertisementServiceData() { + BluetoothHearingAid.AdvertisementServiceData data = + new BluetoothHearingAid.AdvertisementServiceData(0, 0); + when(mService.getAdvertisementServiceData(mTestDevice)).thenReturn(data); + + mBinder.getAdvertisementServiceData(mTestDevice, mAttributionSource); + verify(mService).getAdvertisementServiceData(mTestDevice); + } + + @Test + public void cleanup_doesNotCrash() { + mBinder.cleanup(); + } +} diff --git a/android/app/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java index 757a6488be..a284d6a414 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java @@ -97,7 +97,7 @@ public class HearingAidServiceTest { private final BluetoothDevice mSingleDevice = getTestDevice(13); private HearingAidService mService; - private HearingAidService.BluetoothHearingAidBinder mBinder; + private HearingAidServiceBinder mBinder; private InOrder mInOrder; private TestLooper mLooper; @@ -124,7 +124,7 @@ public class HearingAidServiceTest { mService = new HearingAidService(mAdapterService, mLooper.getLooper(), mNativeInterface); mService.setAvailable(true); - mBinder = (HearingAidService.BluetoothHearingAidBinder) mService.initBinder(); + mBinder = (HearingAidServiceBinder) mService.initBinder(); } @After diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfp/BluetoothHeadsetBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceBinderTest.java index d125fbee4c..73f3610505 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/hfp/BluetoothHeadsetBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceBinderTest.java @@ -27,12 +27,20 @@ import static org.mockito.Mockito.verify; import android.bluetooth.BluetoothDevice; import android.content.AttributionSource; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; -public class BluetoothHeadsetBinderTest { +/** Test cases for {@link HeadsetServiceBinder} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class HeadsetServiceBinderTest { + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @Mock private HeadsetService mService; @@ -40,11 +48,11 @@ public class BluetoothHeadsetBinderTest { private final AttributionSource mAttributionSource = new AttributionSource.Builder(1).build(); private final BluetoothDevice mDevice = getTestDevice(39); - private HeadsetService.BluetoothHeadsetBinder mBinder; + private HeadsetServiceBinder mBinder; @Before public void setUp() throws Exception { - mBinder = new HeadsetService.BluetoothHeadsetBinder(mService); + mBinder = new HeadsetServiceBinder(mService); } @Test @@ -68,6 +76,7 @@ public class BluetoothHeadsetBinderTest { @Test public void getDevicesMatchingConnectionStates() { int[] states = new int[] {STATE_CONNECTED}; + mBinder.getDevicesMatchingConnectionStates(states, mAttributionSource); verify(mService).getDevicesMatchingConnectionStates(states); } @@ -142,6 +151,7 @@ public class BluetoothHeadsetBinderTest { @Test public void setAudioRouteAllowed() { boolean allowed = true; + mBinder.setAudioRouteAllowed(allowed, mAttributionSource); verify(mService).setAudioRouteAllowed(allowed); } @@ -155,6 +165,7 @@ public class BluetoothHeadsetBinderTest { @Test public void setForceScoAudio() { boolean forced = true; + mBinder.setForceScoAudio(forced, mAttributionSource); verify(mService).setForceScoAudio(forced); } diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceBinderTest.java index b3171bc191..3a1a31d085 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceBinderTest.java @@ -35,6 +35,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +/** Test cases for {@link HeadsetClientServiceBinder} */ @SmallTest @RunWith(AndroidJUnit4.class) public class HeadsetClientServiceBinderTest { @@ -44,11 +45,11 @@ public class HeadsetClientServiceBinderTest { private final BluetoothDevice mDevice = getTestDevice(54); - HeadsetClientService.BluetoothHeadsetClientBinder mBinder; + private HeadsetClientServiceBinder mBinder; @Before public void setUp() throws Exception { - mBinder = new HeadsetClientService.BluetoothHeadsetClientBinder(mService); + mBinder = new HeadsetClientServiceBinder(mService); } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/hid/BluetoothHidDeviceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/hid/HidDeviceServiceBinderTest.java index 8a03fe7186..4369e85310 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/hid/BluetoothHidDeviceBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/hid/HidDeviceServiceBinderTest.java @@ -33,12 +33,20 @@ import android.bluetooth.BluetoothHidDeviceAppSdpSettings; import android.bluetooth.IBluetoothHidDeviceCallback; import android.content.AttributionSource; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; -public class BluetoothHidDeviceBinderTest { +/** Test cases for {@link HidDeviceServiceBinder} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class HidDeviceServiceBinderTest { + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @Mock private HidDeviceService mService; @@ -46,12 +54,12 @@ public class BluetoothHidDeviceBinderTest { private final AttributionSource mAttributionSource = new AttributionSource.Builder(1).build(); private final BluetoothDevice mDevice = getTestDevice(29); - private HidDeviceService.BluetoothHidDeviceBinder mBinder; + private HidDeviceServiceBinder mBinder; @Before public void setUp() throws Exception { when(mService.isAvailable()).thenReturn(true); - mBinder = new HidDeviceService.BluetoothHidDeviceBinder(mService); + mBinder = new HidDeviceServiceBinder(mService); } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/hid/HidDeviceTest.java b/android/app/tests/unit/src/com/android/bluetooth/hid/HidDeviceServiceTest.java index 5ddb36e4c6..a242f054a7 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/hid/HidDeviceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/hid/HidDeviceServiceTest.java @@ -76,7 +76,7 @@ import org.mockito.hamcrest.MockitoHamcrest; @MediumTest @RunWith(AndroidJUnit4.class) -public class HidDeviceTest { +public class HidDeviceServiceTest { @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @Mock private AdapterService mAdapterService; diff --git a/android/app/tests/unit/src/com/android/bluetooth/hid/HidHostServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/hid/HidHostServiceBinderTest.java index 02bee889e8..e2b15ebdba 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/hid/HidHostServiceBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/hid/HidHostServiceBinderTest.java @@ -36,9 +36,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +/** Test cases for {@link HidHostServiceBinder}. */ @SmallTest @RunWith(AndroidJUnit4.class) public class HidHostServiceBinderTest { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @@ -46,98 +48,90 @@ public class HidHostServiceBinderTest { private final BluetoothDevice mDevice = getTestDevice(50); - private HidHostService.BluetoothHidHostBinder mBinder; + private HidHostServiceBinder mBinder; @Before public void setUp() { - mBinder = new HidHostService.BluetoothHidHostBinder(mService); + mBinder = new HidHostServiceBinder(mService); } @Test public void connect_callsServiceMethod() { mBinder.connect(mDevice, null); - verify(mService).connect(mDevice); } @Test public void disconnect_callsServiceMethod() { mBinder.disconnect(mDevice, null); - verify(mService).disconnect(mDevice); } @Test public void getConnectedDevices_callsServiceMethod() { mBinder.getConnectedDevices(null); - verify(mService).getDevicesMatchingConnectionStates(new int[] {STATE_CONNECTED}); } @Test public void getDevicesMatchingConnectionStates_callsServiceMethod() { int[] states = new int[] {STATE_CONNECTED}; - mBinder.getDevicesMatchingConnectionStates(states, null); + mBinder.getDevicesMatchingConnectionStates(states, null); verify(mService).getDevicesMatchingConnectionStates(states); } @Test public void getConnectionState_callsServiceMethod() { mBinder.getConnectionState(mDevice, null); - verify(mService).getConnectionState(mDevice); } @Test public void setConnectionPolicy_callsServiceMethod() { int connectionPolicy = CONNECTION_POLICY_ALLOWED; - mBinder.setConnectionPolicy(mDevice, connectionPolicy, null); + mBinder.setConnectionPolicy(mDevice, connectionPolicy, null); verify(mService).setConnectionPolicy(mDevice, connectionPolicy); } @Test public void getConnectionPolicy_callsServiceMethod() { mBinder.getConnectionPolicy(mDevice, null); - verify(mService).getConnectionPolicy(mDevice); } @Test public void setPreferredTransport_callsServiceMethod() { int preferredTransport = BluetoothDevice.TRANSPORT_AUTO; - mBinder.setPreferredTransport(mDevice, preferredTransport, null); + mBinder.setPreferredTransport(mDevice, preferredTransport, null); verify(mService).setPreferredTransport(mDevice, preferredTransport); } @Test public void getPreferredTransport_callsServiceMethod() { mBinder.getPreferredTransport(mDevice, null); - verify(mService).getPreferredTransport(mDevice); } @Test public void getProtocolMode_callsServiceMethod() { mBinder.getProtocolMode(mDevice, null); - verify(mService).getProtocolMode(mDevice); } @Test public void virtualUnplug_callsServiceMethod() { mBinder.virtualUnplug(mDevice, null); - verify(mService).virtualUnplug(mDevice); } @Test public void setProtocolMode_callsServiceMethod() { int protocolMode = 1; - mBinder.setProtocolMode(mDevice, protocolMode, null); + mBinder.setProtocolMode(mDevice, protocolMode, null); verify(mService).setProtocolMode(mDevice, protocolMode); } @@ -146,8 +140,8 @@ public class HidHostServiceBinderTest { byte reportType = 1; byte reportId = 2; int bufferSize = 16; - mBinder.getReport(mDevice, reportType, reportId, bufferSize, null); + mBinder.getReport(mDevice, reportType, reportId, bufferSize, null); verify(mService).getReport(mDevice, reportType, reportId, bufferSize); } @@ -155,31 +149,30 @@ public class HidHostServiceBinderTest { public void setReport_callsServiceMethod() { byte reportType = 1; String report = "test_report"; - mBinder.setReport(mDevice, reportType, report, null); + mBinder.setReport(mDevice, reportType, report, null); verify(mService).setReport(mDevice, reportType, report); } @Test public void sendData_callsServiceMethod() { String report = "test_report"; - mBinder.sendData(mDevice, report, null); + mBinder.sendData(mDevice, report, null); verify(mService).sendData(mDevice, report); } @Test public void setIdleTime_callsServiceMethod() { byte idleTime = 1; - mBinder.setIdleTime(mDevice, idleTime, null); + mBinder.setIdleTime(mDevice, idleTime, null); verify(mService).setIdleTime(mDevice, idleTime); } @Test public void getIdleTime_callsServiceMethod() { mBinder.getIdleTime(mDevice, null); - verify(mService).getIdleTime(mDevice); } diff --git a/android/app/tests/unit/src/com/android/bluetooth/hid/HidHostServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/hid/HidHostServiceTest.java index a41f93d5d6..10f5f3257f 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/hid/HidHostServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/hid/HidHostServiceTest.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.android.bluetooth.hid; import static android.bluetooth.BluetoothDevice.BOND_BONDED; @@ -53,6 +54,7 @@ import java.util.List; @MediumTest @RunWith(AndroidJUnit4.class) public class HidHostServiceTest { + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @Mock private AdapterService mAdapterService; diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceBinderTest.java index 15b3563df1..68677ffedb 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceBinderTest.java @@ -36,15 +36,23 @@ import android.content.AttributionSource; import android.os.ParcelUuid; import android.platform.test.flag.junit.SetFlagsRule; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import java.util.UUID; -public class LeAudioBinderTest { +/** Test cases for {@link LeAudioServiceBinder} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class LeAudioServiceBinderTest { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @@ -53,11 +61,11 @@ public class LeAudioBinderTest { private static final String TEST_BROADCAST_NAME = "TEST"; private static final int TEST_QUALITY = BluetoothLeBroadcastSubgroupSettings.QUALITY_STANDARD; - private LeAudioService.BluetoothLeAudioBinder mBinder; + private LeAudioServiceBinder mBinder; @Before public void setUp() { - mBinder = new LeAudioService.BluetoothLeAudioBinder(mService); + mBinder = new LeAudioServiceBinder(mService); } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanBinderTest.java deleted file mode 100644 index 61c7df48b8..0000000000 --- a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanBinderTest.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (C) 2025 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.le_scan; - -import static com.android.bluetooth.TestUtils.MockitoRule; -import static com.android.bluetooth.TestUtils.getTestDevice; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import android.app.PendingIntent; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothManager; -import android.bluetooth.le.IPeriodicAdvertisingCallback; -import android.bluetooth.le.IScannerCallback; -import android.bluetooth.le.ScanFilter; -import android.bluetooth.le.ScanResult; -import android.bluetooth.le.ScanSettings; -import android.content.AttributionSource; -import android.content.Intent; -import android.os.WorkSource; - -import androidx.test.filters.SmallTest; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; - -import java.util.ArrayList; -import java.util.List; - -/** Test cases for {@link ScanBinder}. */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class ScanBinderTest { - @Rule public final MockitoRule mMockitoRule = new MockitoRule(); - - @Mock private ScanController mScanController; - - private final AttributionSource mAttributionSource = - InstrumentationRegistry.getInstrumentation() - .getTargetContext() - .getSystemService(BluetoothManager.class) - .getAdapter() - .getAttributionSource(); - private final BluetoothDevice mDevice = getTestDevice(89); - private ScanBinder mBinder; - - @Before - public void setUp() { - mBinder = new ScanBinder(mScanController); - } - - @Test - public void registerScanner() { - IScannerCallback callback = mock(IScannerCallback.class); - WorkSource workSource = mock(WorkSource.class); - - mBinder.registerScanner(callback, workSource, mAttributionSource); - verify(mScanController).registerScanner(callback, workSource, mAttributionSource); - } - - @Test - public void unregisterScanner() { - int scannerId = 1; - - mBinder.unregisterScanner(scannerId, mAttributionSource); - verify(mScanController).unregisterScanner(scannerId, mAttributionSource); - } - - @Test - public void startScan() { - int scannerId = 1; - ScanSettings settings = new ScanSettings.Builder().build(); - List<ScanFilter> filters = new ArrayList<>(); - - mBinder.startScan(scannerId, settings, filters, mAttributionSource); - verify(mScanController).startScan(scannerId, settings, filters, mAttributionSource); - } - - @Test - public void startScanForIntent() { - PendingIntent intent = - PendingIntent.getBroadcast( - InstrumentationRegistry.getInstrumentation().getTargetContext(), - 0, - new Intent(), - PendingIntent.FLAG_IMMUTABLE); - ScanSettings settings = new ScanSettings.Builder().build(); - List<ScanFilter> filters = new ArrayList<>(); - - mBinder.startScanForIntent(intent, settings, filters, mAttributionSource); - verify(mScanController) - .registerPiAndStartScan(intent, settings, filters, mAttributionSource); - } - - @Test - public void stopScan_withScannerId() { - int scannerId = 1; - - mBinder.stopScan(scannerId, mAttributionSource); - verify(mScanController).stopScan(scannerId, mAttributionSource); - } - - @Test - public void stopScan_withIntent() { - PendingIntent intent = - PendingIntent.getBroadcast( - InstrumentationRegistry.getInstrumentation().getTargetContext(), - 0, - new Intent(), - PendingIntent.FLAG_IMMUTABLE); - - mBinder.stopScanForIntent(intent, mAttributionSource); - verify(mScanController).stopScan(intent, mAttributionSource); - } - - @Test - public void flushPendingBatchResults() { - int scannerId = 1; - - mBinder.flushPendingBatchResults(scannerId, mAttributionSource); - verify(mScanController).flushPendingBatchResults(scannerId, mAttributionSource); - } - - @Test - public void registerSync() { - ScanResult scanResult = new ScanResult(mDevice, null, 0, 0); - int skip = 1; - int timeout = 2; - IPeriodicAdvertisingCallback callback = mock(IPeriodicAdvertisingCallback.class); - - mBinder.registerSync(scanResult, skip, timeout, callback, mAttributionSource); - verify(mScanController) - .registerSync(scanResult, skip, timeout, callback, mAttributionSource); - } - - @Test - public void unregisterSync() { - IPeriodicAdvertisingCallback callback = mock(IPeriodicAdvertisingCallback.class); - - mBinder.unregisterSync(callback, mAttributionSource); - verify(mScanController).unregisterSync(callback, mAttributionSource); - } - - @Test - public void transferSync() { - int serviceData = 1; - int syncHandle = 2; - - mBinder.transferSync(mDevice, serviceData, syncHandle, mAttributionSource); - verify(mScanController).transferSync(mDevice, serviceData, syncHandle, mAttributionSource); - } - - @Test - public void transferSetInfo() { - int serviceData = 1; - int advHandle = 2; - IPeriodicAdvertisingCallback callback = mock(IPeriodicAdvertisingCallback.class); - - mBinder.transferSetInfo(mDevice, serviceData, advHandle, callback, mAttributionSource); - verify(mScanController) - .transferSetInfo(mDevice, serviceData, advHandle, callback, mAttributionSource); - } - - @Test - public void numHwTrackFiltersAvailable() { - mBinder.numHwTrackFiltersAvailable(mAttributionSource); - verify(mScanController).numHwTrackFiltersAvailable(mAttributionSource); - } - - @Test - public void cleanup_doesNotCrash() { - mBinder.cleanup(); - } -} diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanBinderTest.kt b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanBinderTest.kt new file mode 100644 index 0000000000..3424c48a54 --- /dev/null +++ b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanBinderTest.kt @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2025 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.le_scan + +import android.app.PendingIntent +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager +import android.bluetooth.le.IPeriodicAdvertisingCallback +import android.bluetooth.le.IScannerCallback +import android.bluetooth.le.ScanFilter +import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings +import android.content.Intent +import android.os.WorkSource +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.bluetooth.TestUtils.MockitoRule +import com.android.bluetooth.TestUtils.getTestDevice +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify + +/** Test cases for [ScanBinder] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class ScanBinderTest { + + @get:Rule val mockitoRule = MockitoRule() + + @Mock private lateinit var scanController: ScanController + + private val attributionSource = + InstrumentationRegistry.getInstrumentation() + .targetContext + .getSystemService(BluetoothManager::class.java) + .adapter + .attributionSource + private val device: BluetoothDevice = getTestDevice(89) + private lateinit var binder: ScanBinder + + @Before + fun setUp() { + binder = ScanBinder(scanController) + } + + @Test + fun registerScanner() { + val callback = mock(IScannerCallback::class.java) + val workSource = mock(WorkSource::class.java) + + binder.registerScanner(callback, workSource, attributionSource) + verify(scanController).registerScanner(callback, workSource, attributionSource) + } + + @Test + fun unregisterScanner() { + val scannerId = 1 + + binder.unregisterScanner(scannerId, attributionSource) + verify(scanController).unregisterScanner(scannerId, attributionSource) + } + + @Test + fun startScan() { + val scannerId = 1 + val settings = ScanSettings.Builder().build() + val filters = listOf<ScanFilter>() + + binder.startScan(scannerId, settings, filters, attributionSource) + verify(scanController).startScan(scannerId, settings, filters, attributionSource) + } + + @Test + fun startScanForIntent() { + val intent = + PendingIntent.getBroadcast( + InstrumentationRegistry.getInstrumentation().targetContext, + 0, + Intent(), + PendingIntent.FLAG_IMMUTABLE, + ) + val settings = ScanSettings.Builder().build() + val filters = listOf<ScanFilter>() + + binder.startScanForIntent(intent, settings, filters, attributionSource) + verify(scanController).registerPiAndStartScan(intent, settings, filters, attributionSource) + } + + @Test + fun stopScan_withScannerId() { + val scannerId = 1 + + binder.stopScan(scannerId, attributionSource) + verify(scanController).stopScan(scannerId, attributionSource) + } + + @Test + fun stopScan_withIntent() { + val intent = + PendingIntent.getBroadcast( + InstrumentationRegistry.getInstrumentation().targetContext, + 0, + Intent(), + PendingIntent.FLAG_IMMUTABLE, + ) + + binder.stopScanForIntent(intent, attributionSource) + verify(scanController).stopScan(intent, attributionSource) + } + + @Test + fun flushPendingBatchResults() { + val scannerId = 1 + + binder.flushPendingBatchResults(scannerId, attributionSource) + verify(scanController).flushPendingBatchResults(scannerId, attributionSource) + } + + @Test + fun registerSync() { + val scanResult = mock(ScanResult::class.java) + val skip = 1 + val timeout = 2 + val callback = mock(IPeriodicAdvertisingCallback::class.java) + + binder.registerSync(scanResult, skip, timeout, callback, attributionSource) + verify(scanController).registerSync(scanResult, skip, timeout, callback, attributionSource) + } + + @Test + fun unregisterSync() { + val callback = mock(IPeriodicAdvertisingCallback::class.java) + + binder.unregisterSync(callback, attributionSource) + verify(scanController).unregisterSync(callback, attributionSource) + } + + @Test + fun transferSync() { + val serviceData = 1 + val syncHandle = 2 + + binder.transferSync(device, serviceData, syncHandle, attributionSource) + verify(scanController).transferSync(device, serviceData, syncHandle, attributionSource) + } + + @Test + fun transferSetInfo() { + val serviceData = 1 + val advHandle = 2 + val callback = mock(IPeriodicAdvertisingCallback::class.java) + + binder.transferSetInfo(device, serviceData, advHandle, callback, attributionSource) + verify(scanController) + .transferSetInfo(device, serviceData, advHandle, callback, attributionSource) + } + + @Test + fun numHwTrackFiltersAvailable() { + binder.numHwTrackFiltersAvailable(attributionSource) + verify(scanController).numHwTrackFiltersAvailable(attributionSource) + } + + @Test + fun cleanup_doesNotCrash() { + binder.cleanup() + } +} diff --git a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapServiceBinderTest.java index fa0c3d3f35..b3125460c4 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapServiceBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapServiceBinderTest.java @@ -35,77 +35,73 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +/** Test cases for {@link BluetoothMapServiceBinder} */ @SmallTest @RunWith(AndroidJUnit4.class) public class BluetoothMapServiceBinderTest { + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @Mock private BluetoothMapService mService; private final BluetoothDevice mDevice = getTestDevice(98); - private BluetoothMapService.BluetoothMapBinder mBinder; + private BluetoothMapServiceBinder mBinder; @Before public void setUp() throws Exception { - mBinder = new BluetoothMapService.BluetoothMapBinder(mService); + mBinder = new BluetoothMapServiceBinder(mService); } @Test public void disconnect_callsServiceMethod() { mBinder.disconnect(mDevice, null); - verify(mService).disconnect(mDevice); } @Test public void getConnectedDevices_callsServiceMethod() { mBinder.getConnectedDevices(null); - verify(mService).getConnectedDevices(); } @Test public void getDevicesMatchingConnectionStates_callsServiceMethod() { int[] states = new int[] {STATE_CONNECTED}; - mBinder.getDevicesMatchingConnectionStates(states, null); + mBinder.getDevicesMatchingConnectionStates(states, null); verify(mService).getDevicesMatchingConnectionStates(states); } @Test public void getConnectionState_callsServiceMethod() { mBinder.getConnectionState(mDevice, null); - verify(mService).getConnectionState(mDevice); } @Test public void setConnectionPolicy_callsServiceMethod() { int connectionPolicy = CONNECTION_POLICY_ALLOWED; - mBinder.setConnectionPolicy(mDevice, connectionPolicy, null); + mBinder.setConnectionPolicy(mDevice, connectionPolicy, null); verify(mService).setConnectionPolicy(mDevice, connectionPolicy); } @Test public void getConnectionPolicy_callsServiceMethod() { mBinder.getConnectionPolicy(mDevice, null); - verify(mService).getConnectionPolicy(mDevice); } @Test public void getState_callsServiceMethod() { mBinder.getState(null); - verify(mService).getState(); } @Test public void isConnected_callsServiceStaticMethod() { mBinder.isConnected(mDevice, null); - verify(mService).getConnectionState(mDevice); } @@ -116,7 +112,7 @@ public class BluetoothMapServiceBinderTest { } @Test - public void cleanUp_doesNotCrash() { + public void cleanup_doesNotCrash() { mBinder.cleanup(); } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientServiceBinderTest.java index 0830f8b9f9..a67a5c3981 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientServiceBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientServiceBinderTest.java @@ -35,70 +35,67 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +/** Test cases for {@link MapClientServiceBinder} */ @MediumTest @RunWith(AndroidJUnit4.class) public class MapClientServiceBinderTest { + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @Mock private MapClientService mService; private final BluetoothDevice mDevice = getTestDevice(65); - MapClientService.Binder mBinder; + private MapClientServiceBinder mBinder; @Before public void setUp() { - mBinder = new MapClientService.Binder(mService); + mBinder = new MapClientServiceBinder(mService); } @Test public void connect_callsServiceMethod() { mBinder.connect(mDevice, null); - verify(mService).connect(mDevice); } @Test public void disconnect_callsServiceMethod() { mBinder.disconnect(mDevice, null); - verify(mService).disconnect(mDevice); } @Test public void getConnectedDevices_callsServiceMethod() { mBinder.getConnectedDevices(null); - verify(mService).getConnectedDevices(); } @Test public void getDevicesMatchingConnectionStates_callsServiceMethod() { int[] states = new int[] {STATE_CONNECTED}; - mBinder.getDevicesMatchingConnectionStates(states, null); + mBinder.getDevicesMatchingConnectionStates(states, null); verify(mService).getDevicesMatchingConnectionStates(states); } @Test public void getConnectionState_callsServiceMethod() { mBinder.getConnectionState(mDevice, null); - verify(mService).getConnectionState(mDevice); } @Test public void setConnectionPolicy_callsServiceMethod() { int connectionPolicy = CONNECTION_POLICY_ALLOWED; - mBinder.setConnectionPolicy(mDevice, connectionPolicy, null); + mBinder.setConnectionPolicy(mDevice, connectionPolicy, null); verify(mService).setConnectionPolicy(mDevice, connectionPolicy); } @Test public void getConnectionPolicy_callsServiceMethod() { mBinder.getConnectionPolicy(mDevice, null); - verify(mService).getConnectionPolicy(mDevice); } @@ -106,13 +103,13 @@ public class MapClientServiceBinderTest { public void sendMessage_callsServiceMethod() { Uri[] contacts = new Uri[] {}; String message = "test_message"; - mBinder.sendMessage(mDevice, contacts, message, null, null, null); + mBinder.sendMessage(mDevice, contacts, message, null, null, null); verify(mService).sendMessage(mDevice, contacts, message, null, null); } @Test - public void cleanUp_doesNotCrash() { + public void cleanup_doesNotCrash() { mBinder.cleanup(); } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/mcp/McpServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/mcp/McpServiceBinderTest.java new file mode 100644 index 0000000000..dc21e810b8 --- /dev/null +++ b/android/app/tests/unit/src/com/android/bluetooth/mcp/McpServiceBinderTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2025 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.mcp; + +import static com.android.bluetooth.TestUtils.MockitoRule; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothDevice; +import android.content.AttributionSource; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** Test cases for {@link McpServiceBinder} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class McpServiceBinderTest { + + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); + + @Mock private McpService mService; + + private AttributionSource mAttributionSource; + private McpServiceBinder mBinder; + + @Before + public void setUp() throws Exception { + when(mService.isAvailable()).thenReturn(true); + mBinder = new McpServiceBinder(mService); + mAttributionSource = new AttributionSource.Builder(1).build(); + } + + @Test + public void setDeviceAuthorized() { + BluetoothDevice device = mock(BluetoothDevice.class); + boolean isAuthorized = true; + + mBinder.setDeviceAuthorized(device, isAuthorized, mAttributionSource); + verify(mService).setDeviceAuthorized(device, isAuthorized); + } + + @Test + public void cleanup_doesNotCrash() { + mBinder.cleanup(); + } +} diff --git a/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppUtilityTest.java b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppUtilityTest.java index 705c91d518..cadff94862 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppUtilityTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppUtilityTest.java @@ -44,6 +44,7 @@ import android.content.pm.ResolveInfo; import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; +import android.provider.Settings; import androidx.test.platform.app.InstrumentationRegistry; @@ -432,4 +433,22 @@ public class BluetoothOppUtilityTest { assertWithMessage("Exception should not happen. " + e).fail(); } } + + @Test + public void grantPermissionToNearbyComponent() { + Uri originalUri = Uri.parse("content://test.provider/1"); + Settings.Secure.putString( + mContext.getContentResolver(), + "nearby_sharing_component", + "com.example/.BComponent"); + Context spiedContext = spy(new ContextWrapper(mContext)); + + BluetoothOppUtility.grantPermissionToNearbyComponent(spiedContext, List.of(originalUri)); + + verify(spiedContext) + .grantUriPermission( + eq("com.example"), + eq(originalUri), + eq(Intent.FLAG_GRANT_READ_URI_PERMISSION)); + } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/pan/PanServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/pan/PanServiceBinderTest.java index d798666437..1ba4881c90 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/pan/PanServiceBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/pan/PanServiceBinderTest.java @@ -35,75 +35,72 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +/** Test cases for {@link PanServiceBinder} */ @SmallTest @RunWith(AndroidJUnit4.class) public class PanServiceBinderTest { + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @Mock private PanService mService; private final BluetoothDevice mDevice = getTestDevice(64); - PanService.BluetoothPanBinder mBinder; + private PanServiceBinder mBinder; @Before public void setUp() throws Exception { - mBinder = new PanService.BluetoothPanBinder(mService); + mBinder = new PanServiceBinder(mService); } @Test public void connect_callsServiceMethod() { mBinder.connect(mDevice, null); - verify(mService).connect(mDevice); } @Test public void disconnect_callsServiceMethod() { mBinder.disconnect(mDevice, null); - verify(mService).disconnect(mDevice); } @Test public void getConnectedDevices_callsServiceMethod() { mBinder.getConnectedDevices(null); - verify(mService).getConnectedDevices(); } @Test public void getDevicesMatchingConnectionStates_callsServiceMethod() { int[] states = new int[] {STATE_CONNECTED}; - mBinder.getDevicesMatchingConnectionStates(states, null); + mBinder.getDevicesMatchingConnectionStates(states, null); verify(mService).getDevicesMatchingConnectionStates(states); } @Test public void getConnectionState_callsServiceMethod() { mBinder.getConnectionState(mDevice, null); - verify(mService).getConnectionState(mDevice); } @Test public void setConnectionPolicy_callsServiceMethod() { int connectionPolicy = CONNECTION_POLICY_ALLOWED; - mBinder.setConnectionPolicy(mDevice, connectionPolicy, null); + mBinder.setConnectionPolicy(mDevice, connectionPolicy, null); verify(mService).setConnectionPolicy(mDevice, connectionPolicy); } @Test public void isTetheringOn_callsServiceMethod() { mBinder.isTetheringOn(null); - verify(mService).isTetheringOn(); } @Test - public void cleanUp_doesNotCrash() { + public void cleanup_doesNotCrash() { mBinder.cleanup(); } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapServiceBinderTest.java index 95b2bb25ef..2397f3897a 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapServiceBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapServiceBinderTest.java @@ -35,20 +35,22 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +/** Test cases for {@link BluetoothPbapServiceBinder} */ @MediumTest @RunWith(AndroidJUnit4.class) public class BluetoothPbapServiceBinderTest { + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @Mock private BluetoothPbapService mService; private final BluetoothDevice mDevice = getTestDevice(60); - BluetoothPbapService.PbapBinder mBinder; + private BluetoothPbapServiceBinder mBinder; @Before public void setUp() { - mBinder = new BluetoothPbapService.PbapBinder(mService); + mBinder = new BluetoothPbapServiceBinder(mService); } @Test diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbapclient/PbapClientBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbapclient/PbapClientServiceBinderTest.java index aff8f250a7..94b51136dd 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/pbapclient/PbapClientBinderTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/pbapclient/PbapClientServiceBinderTest.java @@ -47,29 +47,30 @@ import org.mockito.Mock; import java.util.List; +/** Test cases for {@link PbapClientServiceBinder} */ @MediumTest @RunWith(AndroidJUnit4.class) -public class PbapClientBinderTest { +public class PbapClientServiceBinderTest { @Rule public final MockitoRule mMockitoRule = new MockitoRule(); @Mock private PbapClientService mMockService; private BluetoothDevice mTestDevice; private AttributionSource mAttributionSource; - private PbapClientBinder mPbapClientBinder; + private PbapClientServiceBinder mPbapClientServiceBinder; @Before public void setUp() throws Exception { mTestDevice = getTestDevice(1); mAttributionSource = new AttributionSource.Builder(1).build(); - mPbapClientBinder = new PbapClientBinder(mMockService); + mPbapClientServiceBinder = new PbapClientServiceBinder(mMockService); } @After public void tearDown() throws Exception { - if (mPbapClientBinder != null) { - mPbapClientBinder.cleanup(); - mPbapClientBinder = null; + if (mPbapClientServiceBinder != null) { + mPbapClientServiceBinder.cleanup(); + mPbapClientServiceBinder = null; } } @@ -79,45 +80,46 @@ public class PbapClientBinderTest { @Test public void testConnect() { - mPbapClientBinder.connect(mTestDevice, mAttributionSource); + mPbapClientServiceBinder.connect(mTestDevice, mAttributionSource); verify(mMockService).connect(eq(mTestDevice)); } @Test public void testDisconnect() { - mPbapClientBinder.disconnect(mTestDevice, mAttributionSource); + mPbapClientServiceBinder.disconnect(mTestDevice, mAttributionSource); verify(mMockService).disconnect(eq(mTestDevice)); } @Test public void testGetConnectedDevices() { - mPbapClientBinder.getConnectedDevices(mAttributionSource); + mPbapClientServiceBinder.getConnectedDevices(mAttributionSource); verify(mMockService).getConnectedDevices(); } @Test public void testGetDevicesMatchingConnectionStates() { int[] states = new int[] {STATE_CONNECTED}; - mPbapClientBinder.getDevicesMatchingConnectionStates(states, mAttributionSource); + mPbapClientServiceBinder.getDevicesMatchingConnectionStates(states, mAttributionSource); verify(mMockService).getDevicesMatchingConnectionStates(eq(states)); } @Test public void testGetConnectionState() { - mPbapClientBinder.getConnectionState(mTestDevice, mAttributionSource); + mPbapClientServiceBinder.getConnectionState(mTestDevice, mAttributionSource); verify(mMockService).getConnectionState(eq(mTestDevice)); } @Test public void testSetConnectionPolicy() { int connectionPolicy = CONNECTION_POLICY_ALLOWED; - mPbapClientBinder.setConnectionPolicy(mTestDevice, connectionPolicy, mAttributionSource); + mPbapClientServiceBinder.setConnectionPolicy( + mTestDevice, connectionPolicy, mAttributionSource); verify(mMockService).setConnectionPolicy(eq(mTestDevice), eq(connectionPolicy)); } @Test public void testGetConnectionPolicy() { - mPbapClientBinder.getConnectionPolicy(mTestDevice, mAttributionSource); + mPbapClientServiceBinder.getConnectionPolicy(mTestDevice, mAttributionSource); verify(mMockService).getConnectionPolicy(eq(mTestDevice)); } @@ -127,52 +129,54 @@ public class PbapClientBinderTest { @Test public void testConnect_afterCleanup_returnsFalse() { - mPbapClientBinder.cleanup(); - boolean result = mPbapClientBinder.connect(mTestDevice, mAttributionSource); + mPbapClientServiceBinder.cleanup(); + boolean result = mPbapClientServiceBinder.connect(mTestDevice, mAttributionSource); verify(mMockService, never()).connect(any(BluetoothDevice.class)); assertThat(result).isFalse(); } @Test public void testDisconnect_afterCleanup_returnsFalse() { - mPbapClientBinder.cleanup(); - boolean result = mPbapClientBinder.disconnect(mTestDevice, mAttributionSource); + mPbapClientServiceBinder.cleanup(); + boolean result = mPbapClientServiceBinder.disconnect(mTestDevice, mAttributionSource); verify(mMockService, never()).disconnect(any(BluetoothDevice.class)); assertThat(result).isFalse(); } @Test public void testGetConnectedDevices_afterCleanup_returnsEmptyList() { - mPbapClientBinder.cleanup(); - List<BluetoothDevice> devices = mPbapClientBinder.getConnectedDevices(mAttributionSource); + mPbapClientServiceBinder.cleanup(); + List<BluetoothDevice> devices = + mPbapClientServiceBinder.getConnectedDevices(mAttributionSource); verify(mMockService, never()).getConnectedDevices(); assertThat(devices).isEmpty(); } @Test public void testGetDevicesMatchingConnectionStates_afterCleanup_returnsEmptyList() { - mPbapClientBinder.cleanup(); + mPbapClientServiceBinder.cleanup(); int[] states = new int[] {STATE_CONNECTED}; List<BluetoothDevice> devices = - mPbapClientBinder.getDevicesMatchingConnectionStates(states, mAttributionSource); + mPbapClientServiceBinder.getDevicesMatchingConnectionStates( + states, mAttributionSource); verify(mMockService, never()).getDevicesMatchingConnectionStates(any(int[].class)); assertThat(devices).isEmpty(); } @Test public void testGetConnectionState_afterCleanup_returnsDisconnected() { - mPbapClientBinder.cleanup(); - int state = mPbapClientBinder.getConnectionState(mTestDevice, mAttributionSource); + mPbapClientServiceBinder.cleanup(); + int state = mPbapClientServiceBinder.getConnectionState(mTestDevice, mAttributionSource); verify(mMockService, never()).getConnectionState(any(BluetoothDevice.class)); assertThat(state).isEqualTo(STATE_DISCONNECTED); } @Test public void testSetConnectionPolicy_afterCleanup_returnsFalse() { - mPbapClientBinder.cleanup(); + mPbapClientServiceBinder.cleanup(); int connectionPolicy = CONNECTION_POLICY_ALLOWED; boolean result = - mPbapClientBinder.setConnectionPolicy( + mPbapClientServiceBinder.setConnectionPolicy( mTestDevice, connectionPolicy, mAttributionSource); verify(mMockService, never()).setConnectionPolicy(any(BluetoothDevice.class), anyInt()); assertThat(result).isFalse(); @@ -180,8 +184,8 @@ public class PbapClientBinderTest { @Test public void testGetConnectionPolicy_afterCleanup_returnsUnknown() { - mPbapClientBinder.cleanup(); - int result = mPbapClientBinder.getConnectionPolicy(mTestDevice, mAttributionSource); + mPbapClientServiceBinder.cleanup(); + int result = mPbapClientServiceBinder.getConnectionPolicy(mTestDevice, mAttributionSource); verify(mMockService, never()).getConnectionPolicy(any(BluetoothDevice.class)); assertThat(result).isEqualTo(CONNECTION_POLICY_UNKNOWN); } diff --git a/android/app/tests/unit/src/com/android/bluetooth/sap/SapServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/sap/SapServiceBinderTest.java new file mode 100644 index 0000000000..10d1a092e0 --- /dev/null +++ b/android/app/tests/unit/src/com/android/bluetooth/sap/SapServiceBinderTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2025 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.sap; + +import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; +import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; + +import static com.android.bluetooth.TestUtils.MockitoRule; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothDevice; +import android.content.AttributionSource; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** Test cases for {@link SapServiceBinder} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SapServiceBinderTest { + + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); + + @Mock private SapService mService; + + private AttributionSource mAttributionSource; + private SapServiceBinder mBinder; + + @Before + public void setUp() throws Exception { + when(mService.isAvailable()).thenReturn(true); + mBinder = new SapServiceBinder(mService); + mAttributionSource = new AttributionSource.Builder(1).build(); + } + + @Test + public void getState() { + mBinder.getState(mAttributionSource); + verify(mService).getState(); + } + + @Test + public void getClient() { + mBinder.getClient(mAttributionSource); + // times(2) due to the Log + verify(mService, times(2)).getRemoteDevice(); + } + + @Test + public void isConnected() { + BluetoothDevice device = mock(BluetoothDevice.class); + + mBinder.isConnected(device, mAttributionSource); + verify(mService).getConnectionState(device); + } + + @Test + public void disconnect() { + BluetoothDevice device = mock(BluetoothDevice.class); + + mBinder.disconnect(device, mAttributionSource); + verify(mService).disconnect(device); + } + + @Test + public void getConnectedDevices() { + mBinder.getConnectedDevices(mAttributionSource); + verify(mService).getConnectedDevices(); + } + + @Test + public void getDevicesMatchingConnectionStates() { + int[] states = new int[] {STATE_CONNECTED, STATE_DISCONNECTED}; + + mBinder.getDevicesMatchingConnectionStates(states, mAttributionSource); + verify(mService).getDevicesMatchingConnectionStates(states); + } + + @Test + public void getConnectionState() { + BluetoothDevice device = mock(BluetoothDevice.class); + + mBinder.getConnectionState(device, mAttributionSource); + verify(mService).getConnectionState(device); + } + + @Test + public void setConnectionPolicy() { + BluetoothDevice device = mock(BluetoothDevice.class); + int connectionPolicy = 1; + + mBinder.setConnectionPolicy(device, connectionPolicy, mAttributionSource); + verify(mService).setConnectionPolicy(device, connectionPolicy); + } + + @Test + public void getConnectionPolicy() { + BluetoothDevice device = mock(BluetoothDevice.class); + + mBinder.getConnectionPolicy(device, mAttributionSource); + verify(mService).getConnectionPolicy(device); + } + + @Test + public void cleanup_doesNotCrash() { + mBinder.cleanup(); + } +} diff --git a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsServiceBinderTest.java new file mode 100644 index 0000000000..7758d101b1 --- /dev/null +++ b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsServiceBinderTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2025 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.tbs; + +import static com.android.bluetooth.TestUtils.MockitoRule; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothLeCall; +import android.bluetooth.IBluetoothLeCallControlCallback; +import android.content.AttributionSource; +import android.os.ParcelUuid; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** Test cases for {@link TbsServiceBinder} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class TbsServiceBinderTest { + + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); + + @Mock private TbsService mService; + + private AttributionSource mAttributionSource; + private TbsServiceBinder mBinder; + + @Before + public void setUp() throws Exception { + when(mService.isAvailable()).thenReturn(true); + mBinder = new TbsServiceBinder(mService); + mAttributionSource = new AttributionSource.Builder(1).build(); + } + + @Test + public void registerBearer() { + String token = "token"; + IBluetoothLeCallControlCallback callback = mock(IBluetoothLeCallControlCallback.class); + String uci = "uci"; + List<String> uriSchemes = new ArrayList<>(); + int capabilities = 1; + String providerName = "providerName"; + int technology = 2; + + mBinder.registerBearer( + token, + callback, + uci, + uriSchemes, + capabilities, + providerName, + technology, + mAttributionSource); + verify(mService) + .registerBearer( + token, callback, uci, uriSchemes, capabilities, providerName, technology); + } + + @Test + public void unregisterBearer() { + String token = "token"; + + mBinder.unregisterBearer(token, mAttributionSource); + verify(mService).unregisterBearer(token); + } + + @Test + public void requestResult() { + int ccid = 1; + int requestId = 2; + int result = 3; + + mBinder.requestResult(ccid, requestId, result, mAttributionSource); + verify(mService).requestResult(ccid, requestId, result); + } + + @Test + public void callAdded() { + int ccid = 1; + BluetoothLeCall call = mock(BluetoothLeCall.class); + + mBinder.callAdded(ccid, call, mAttributionSource); + verify(mService).callAdded(ccid, call); + } + + @Test + public void callRemoved() { + int ccid = 1; + UUID callId = UUID.randomUUID(); + int reason = 2; + + mBinder.callRemoved(ccid, new ParcelUuid(callId), reason, mAttributionSource); + verify(mService).callRemoved(ccid, callId, reason); + } + + @Test + public void callStateChanged() { + int ccid = 1; + UUID callId = UUID.randomUUID(); + int state = 2; + + mBinder.callStateChanged(ccid, new ParcelUuid(callId), state, mAttributionSource); + verify(mService).callStateChanged(ccid, callId, state); + } + + @Test + public void currentCallsList() { + int ccid = 1; + List<BluetoothLeCall> calls = new ArrayList<>(); + + mBinder.currentCallsList(ccid, calls, mAttributionSource); + verify(mService).currentCallsList(ccid, calls); + } + + @Test + public void cleanup_doesNotCrash() { + mBinder.cleanup(); + } +} diff --git a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceBinderTest.java new file mode 100644 index 0000000000..32296381c8 --- /dev/null +++ b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceBinderTest.java @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2025 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.vc; + +import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; + +import static com.android.bluetooth.TestUtils.MockitoRule; +import static com.android.bluetooth.TestUtils.getTestDevice; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.IAudioInputCallback; +import android.content.AttributionSource; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** Test cases for {@link VolumeControlServiceBinder} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class VolumeControlServiceBinderTest { + + @Rule public final MockitoRule mMockitoRule = new MockitoRule(); + + @Mock private VolumeControlService mService; + + private final BluetoothDevice mDevice = getTestDevice(25); + + private AttributionSource mAttributionSource; + private VolumeControlServiceBinder mBinder; + + @Before + public void setUp() throws Exception { + when(mService.isAvailable()).thenReturn(true); + mBinder = new VolumeControlServiceBinder(mService); + mAttributionSource = new AttributionSource.Builder(1).build(); + } + + @Test + public void getConnectedDevices() { + mBinder.getConnectedDevices(mAttributionSource); + verify(mService).getConnectedDevices(); + } + + @Test + public void getDevicesMatchingConnectionStates() { + int[] states = new int[] {STATE_CONNECTED}; + + mBinder.getDevicesMatchingConnectionStates(states, mAttributionSource); + verify(mService).getDevicesMatchingConnectionStates(states); + } + + @Test + public void getConnectionState() { + mBinder.getConnectionState(mDevice, mAttributionSource); + verify(mService).getConnectionState(mDevice); + } + + @Test + public void setConnectionPolicy() { + int connectionPolicy = 1; + + mBinder.setConnectionPolicy(mDevice, connectionPolicy, mAttributionSource); + verify(mService).setConnectionPolicy(mDevice, connectionPolicy); + } + + @Test + public void getConnectionPolicy() { + mBinder.getConnectionPolicy(mDevice, mAttributionSource); + verify(mService).getConnectionPolicy(mDevice); + } + + @Test + public void isVolumeOffsetAvailable() { + mBinder.isVolumeOffsetAvailable(mDevice, mAttributionSource); + verify(mService).isVolumeOffsetAvailable(mDevice); + } + + @Test + public void getNumberOfVolumeOffsetInstances() { + mBinder.getNumberOfVolumeOffsetInstances(mDevice, mAttributionSource); + verify(mService).getNumberOfVolumeOffsetInstances(mDevice); + } + + @Test + public void setVolumeOffset() { + int instanceId = 1; + int volumeOffset = 2; + + mBinder.setVolumeOffset(mDevice, instanceId, volumeOffset, mAttributionSource); + verify(mService).setVolumeOffset(mDevice, instanceId, volumeOffset); + } + + @Test + public void setDeviceVolume() { + int volume = 1; + boolean isGroupOp = true; + + mBinder.setDeviceVolume(mDevice, volume, isGroupOp, mAttributionSource); + verify(mService).setDeviceVolume(mDevice, volume, isGroupOp); + } + + @Test + public void setGroupVolume() { + int groupId = 1; + int volume = 2; + + mBinder.setGroupVolume(groupId, volume, mAttributionSource); + verify(mService).setGroupVolume(groupId, volume); + } + + @Test + public void getGroupVolume() { + int groupId = 1; + + mBinder.getGroupVolume(groupId, mAttributionSource); + verify(mService).getGroupVolume(groupId); + } + + @Test + public void setGroupActive() { + int groupId = 1; + boolean active = true; + + mBinder.setGroupActive(groupId, active, mAttributionSource); + verify(mService).setGroupActive(groupId, active); + } + + @Test + public void mute() { + mBinder.mute(mDevice, mAttributionSource); + verify(mService).mute(mDevice); + } + + @Test + public void muteGroup() { + int groupId = 1; + mBinder.muteGroup(groupId, mAttributionSource); + verify(mService).muteGroup(groupId); + } + + @Test + public void unmute() { + mBinder.unmute(mDevice, mAttributionSource); + verify(mService).unmute(mDevice); + } + + @Test + public void unmuteGroup() { + int groupId = 1; + + mBinder.unmuteGroup(groupId, mAttributionSource); + verify(mService).unmuteGroup(groupId); + } + + @Test + public void getNumberOfAudioInputControlServices() { + mBinder.getNumberOfAudioInputControlServices(mAttributionSource, mDevice); + } + + @Test + public void registerAudioInputControlCallback() { + int instanceId = 1; + IAudioInputCallback callback = mock(IAudioInputCallback.class); + + mBinder.registerAudioInputControlCallback( + mAttributionSource, mDevice, instanceId, callback); + } + + @Test + public void unregisterAudioInputControlCallback() { + int instanceId = 1; + IAudioInputCallback callback = mock(IAudioInputCallback.class); + + mBinder.unregisterAudioInputControlCallback( + mAttributionSource, mDevice, instanceId, callback); + } + + @Test + public void getAudioInputGainSettingUnit() { + int instanceId = 1; + mBinder.getAudioInputGainSettingUnit(mAttributionSource, mDevice, instanceId); + } + + @Test + public void getAudioInputGainSettingMin() { + int instanceId = 1; + mBinder.getAudioInputGainSettingMin(mAttributionSource, mDevice, instanceId); + } + + @Test + public void getAudioInputGainSettingMax() { + int instanceId = 1; + mBinder.getAudioInputGainSettingMax(mAttributionSource, mDevice, instanceId); + } + + @Test + public void getAudioInputDescription() { + int instanceId = 1; + mBinder.getAudioInputDescription(mAttributionSource, mDevice, instanceId); + } + + @Test + public void isAudioInputDescriptionWritable() { + int instanceId = 1; + mBinder.isAudioInputDescriptionWritable(mAttributionSource, mDevice, instanceId); + } + + @Test + public void setAudioInputDescription() { + int instanceId = 1; + String description = "test"; + mBinder.setAudioInputDescription(mAttributionSource, mDevice, instanceId, description); + } + + @Test + public void getAudioInputStatus() { + int instanceId = 1; + mBinder.getAudioInputStatus(mAttributionSource, mDevice, instanceId); + } + + @Test + public void getAudioInputType() { + int instanceId = 1; + mBinder.getAudioInputType(mAttributionSource, mDevice, instanceId); + } + + @Test + public void getAudioInputGainSetting() { + int instanceId = 1; + mBinder.getAudioInputGainSetting(mAttributionSource, mDevice, instanceId); + } + + @Test + public void setAudioInputGainSetting() { + int instanceId = 1; + int gainSetting = 2; + mBinder.setAudioInputGainSetting(mAttributionSource, mDevice, instanceId, gainSetting); + } + + @Test + public void getAudioInputGainMode() { + int instanceId = 1; + mBinder.getAudioInputGainMode(mAttributionSource, mDevice, instanceId); + } + + @Test + public void setAudioInputGainMode() { + int instanceId = 1; + int gainMode = 2; + mBinder.setAudioInputGainMode(mAttributionSource, mDevice, instanceId, gainMode); + } + + @Test + public void getAudioInputMute() { + int instanceId = 1; + mBinder.getAudioInputMute(mAttributionSource, mDevice, instanceId); + } + + @Test + public void setAudioInputMute() { + int instanceId = 1; + int mute = 2; + mBinder.setAudioInputMute(mAttributionSource, mDevice, instanceId, mute); + } + + @Test + public void cleanup_doesNotCrash() { + mBinder.cleanup(); + } +} diff --git a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java index 35d8ee3ce1..9412ebfe40 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java @@ -128,7 +128,7 @@ public class VolumeControlServiceTest { private AttributionSource mAttributionSource; private VolumeControlService mService; - private VolumeControlService.BluetoothVolumeControlBinder mBinder; + private VolumeControlServiceBinder mBinder; private InOrder mInOrder; private TestLooper mLooper; @@ -174,7 +174,7 @@ public class VolumeControlServiceTest { mService.setAvailable(true); mService.mFactory = mServiceFactory; - mBinder = (VolumeControlService.BluetoothVolumeControlBinder) mService.initBinder(); + mBinder = (VolumeControlServiceBinder) mService.initBinder(); } @After diff --git a/flags/gap.aconfig b/flags/gap.aconfig index 4a03337332..edc7874fb5 100644 --- a/flags/gap.aconfig +++ b/flags/gap.aconfig @@ -97,13 +97,6 @@ flag { } flag { - name: "non_wake_alarm_for_rpa_rotation" - namespace: "bluetooth" - description: "Use non-wake alarm for LE RPA rotation. go/non-wake-alarm-for-rpa-rotation" - bug: "360743527" -} - -flag { name: "gatt_disconnect_fix" namespace: "bluetooth" description: "Fix GATT disconnect handling" @@ -199,16 +192,6 @@ flag { } flag { - name: "drop_acl_fragment_on_disconnect" - namespace: "bluetooth" - description: "Drop pending ACL packet fragments for disconnected connection" - bug: "376379859" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "configure_scan_on_resume" namespace: "bluetooth" description: "Configure scan paramters when start scanning from onResume" diff --git a/flags/hci.aconfig b/flags/hci.aconfig index cdd5ca3ab6..6f6b1e70df 100644 --- a/flags/hci.aconfig +++ b/flags/hci.aconfig @@ -2,13 +2,6 @@ package: "com.android.bluetooth.flags" container: "com.android.bt" flag { - name: "encryption_change_v2" - namespace: "bluetooth" - description: "Enable encryption change V2 event" - bug: "366018699" -} - -flag { name: "dont_send_hci_disconnect_repeatedly" namespace: "bluetooth" description: "Prevent BT from sending repeated HCI disconnect command" diff --git a/flags/hid.aconfig b/flags/hid.aconfig index d40e55821f..ee5728c94f 100644 --- a/flags/hid.aconfig +++ b/flags/hid.aconfig @@ -118,3 +118,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "hh_state_update_race_fix" + namespace: "bluetooth" + description: "Fix stuck in connecting state due to race when updating state" + bug: "403420458" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/flags/l2cap.aconfig b/flags/l2cap.aconfig index fec2edd6d5..f82b596674 100644 --- a/flags/l2cap.aconfig +++ b/flags/l2cap.aconfig @@ -9,16 +9,6 @@ flag { } flag { - name: "l2cap_le_do_not_adjust_min_interval" - namespace: "bluetooth" - description: "Do not adjust min_interval in connection update request" - bug: "346960036" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "l2cap_fcs_option_fix" namespace: "bluetooth" description: "Use fcs_option for non BASIC mode " diff --git a/flags/leaudio.aconfig b/flags/leaudio.aconfig index 9d2c86afa3..a2f77b5658 100644 --- a/flags/leaudio.aconfig +++ b/flags/leaudio.aconfig @@ -344,3 +344,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "leaudio_use_aggressive_params" + namespace: "bluetooth" + description: "use aggressive parameters for LE Audio device" + bug: "400607635" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/flags/opp.aconfig b/flags/opp.aconfig index 73604961fe..6d1a13bf21 100644 --- a/flags/opp.aconfig +++ b/flags/opp.aconfig @@ -41,3 +41,20 @@ flag { } } +flag { + name: "opp_device_picker_extra_intent_apis" + is_exported: true + namespace: "bluetooth" + description: "New API to get the original intent in Bluetooth Device Picker" + bug: "395796600" +} + +flag { + name: "send_opp_device_picker_extra_intent" + namespace: "bluetooth" + description: "Send the original intent when opening Bluetooth device picker" + bug: "397852103" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/flags/pairing.aconfig b/flags/pairing.aconfig index 28284f9ed8..f30410db17 100644 --- a/flags/pairing.aconfig +++ b/flags/pairing.aconfig @@ -102,16 +102,6 @@ flag { } flag { - name: "smp_state_machine_stuck_after_disconnection_fix" - namespace: "bluetooth" - description: "Fix state machine stuck after pairing device disconnection" - bug: "376306092" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "prevent_service_connections_on_remove_bond" namespace: "bluetooth" description: "Disable service connections on remove bond" diff --git a/flags/security.aconfig b/flags/security.aconfig index c83dc02b24..8d5a8c1f31 100644 --- a/flags/security.aconfig +++ b/flags/security.aconfig @@ -2,13 +2,6 @@ package: "com.android.bluetooth.flags" container: "com.android.bt" flag { - name: "key_missing_classic_device" - namespace: "bluetooth" - description: "Key missing broadcast for Classic devices" - bug: "333634398" -} - -flag { name: "key_missing_ble_peripheral" namespace: "bluetooth" description: "Key missing broadcast for LE devices in peripheral role" @@ -66,16 +59,6 @@ flag { } flag { - name: "sec_disconnect_on_le_key_missing" - namespace: "bluetooth" - description: "Disconnect LE link when keys are missing during encryption" - bug: "376680866" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "guard_bonded_device_properties" namespace: "bluetooth" description: "Don't update device properties for bonded devices from the device discovery results" diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt index cdb9bf0670..ef922c92e1 100644 --- a/framework/api/system-current.txt +++ b/framework/api/system-current.txt @@ -369,6 +369,7 @@ package android.bluetooth { public interface BluetoothDevicePicker { field @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public static final String ACTION_DEVICE_SELECTED = "android.bluetooth.devicepicker.action.DEVICE_SELECTED"; field @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public static final String ACTION_LAUNCH = "android.bluetooth.devicepicker.action.LAUNCH"; + field @FlaggedApi("com.android.bluetooth.flags.opp_device_picker_extra_intent_apis") public static final String EXTRA_DEVICE_PICKER_ORIGINAL_SEND_INTENT = "android.bluetooth.extra.DEVICE_PICKER_ORIGINAL_SEND_INTENT"; field public static final String EXTRA_FILTER_TYPE = "android.bluetooth.devicepicker.extra.FILTER_TYPE"; field public static final String EXTRA_LAUNCH_CLASS = "android.bluetooth.devicepicker.extra.DEVICE_PICKER_LAUNCH_CLASS"; field public static final String EXTRA_LAUNCH_PACKAGE = "android.bluetooth.devicepicker.extra.LAUNCH_PACKAGE"; diff --git a/framework/java/android/bluetooth/BluetoothDevicePicker.java b/framework/java/android/bluetooth/BluetoothDevicePicker.java index ee788dae18..7e7cd6d6b7 100644 --- a/framework/java/android/bluetooth/BluetoothDevicePicker.java +++ b/framework/java/android/bluetooth/BluetoothDevicePicker.java @@ -18,6 +18,7 @@ package android.bluetooth; import static android.Manifest.permission.BLUETOOTH_CONNECT; +import android.annotation.FlaggedApi; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; @@ -25,6 +26,8 @@ import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.bluetooth.annotations.RequiresBluetoothConnectPermission; +import com.android.bluetooth.flags.Flags; + /** * A helper to show a system "Device Picker" activity to the user. * @@ -64,6 +67,14 @@ public interface BluetoothDevicePicker { String EXTRA_LAUNCH_CLASS = "android.bluetooth.devicepicker.extra.DEVICE_PICKER_LAUNCH_CLASS"; /** + * Extra for the original ACTION_SEND or ACTION_SEND_MULTIPLE intent that triggered the BT + * sharing. + */ + @FlaggedApi(Flags.FLAG_OPP_DEVICE_PICKER_EXTRA_INTENT_APIS) + String EXTRA_DEVICE_PICKER_ORIGINAL_SEND_INTENT = + "android.bluetooth.extra.DEVICE_PICKER_ORIGINAL_SEND_INTENT"; + + /** * Broadcast when one BT device is selected from BT device picker screen. Selected {@link * BluetoothDevice} is returned in extra data named {@link BluetoothDevice#EXTRA_DEVICE}. */ diff --git a/system/bta/gatt/bta_gattc_utils.cc b/system/bta/gatt/bta_gattc_utils.cc index 23b150b829..1541f391ca 100644 --- a/system/bta/gatt/bta_gattc_utils.cc +++ b/system/bta/gatt/bta_gattc_utils.cc @@ -142,7 +142,7 @@ tBTA_GATTC_CLCB* bta_gattc_find_clcb_by_cif(uint8_t client_if, const RawAddress& tBTA_GATTC_CLCB* bta_gattc_find_clcb_by_conn_id(tCONN_ID conn_id) { if (com::android::bluetooth::flags::gatt_client_dynamic_allocation()) { for (auto& p_clcb : bta_gattc_cb.clcb_set) { - if (p_clcb->in_use && p_clcb->bta_conn_id == conn_id) { + if (p_clcb != NULL && p_clcb->in_use && p_clcb->bta_conn_id == conn_id) { return p_clcb.get(); } } diff --git a/system/btif/src/btif_dm.cc b/system/btif/src/btif_dm.cc index 44b876af4e..e10606b9dd 100644 --- a/system/btif/src/btif_dm.cc +++ b/system/btif/src/btif_dm.cc @@ -1486,7 +1486,7 @@ static void btif_dm_search_devices_evt(tBTA_DM_SEARCH_EVT event, tBTA_DM_SEARCH* auto triple = eir_uuids_cache.try_emplace(bdaddr, std::set<Uuid>{}); uuid_iter = std::get<0>(triple); } - log::info("EIR UUIDs for {}:", bdaddr); + log::info("EIR UUIDs for {}", bdaddr); for (int i = 0; i < num_uuids; ++i) { Uuid uuid = Uuid::From16Bit(p_uuid16[i]); log::info("{}", uuid.ToString()); @@ -1609,7 +1609,7 @@ static void btif_on_service_discovery_results(RawAddress bd_addr, if (results_for_bonding_device) { // success for SDP bluetooth::metrics::LogSDPComplete(bd_addr, tBTA_STATUS::BTA_SUCCESS); - log::info("SDP finished for {}:", bd_addr); + log::info("SDP finished for {}", bd_addr); pairing_cb.sdp_over_classic = btif_dm_pairing_cb_t::ServiceDiscoveryState::FINISHED; } @@ -1620,7 +1620,7 @@ static void btif_on_service_discovery_results(RawAddress bd_addr, bt_property_t& le_prop = uuid_props[1]; if ((result == BTA_SUCCESS) && !uuids_param.empty()) { - log::info("New UUIDs for {}:", bd_addr); + log::info("New UUIDs for {}", bd_addr); for (const auto& uuid : uuids_param) { if (btif_should_ignore_uuid(uuid)) { continue; @@ -1758,7 +1758,7 @@ static void btif_on_gatt_results(RawAddress bd_addr, std::vector<bluetooth::Uuid bool lea_supported = is_le_audio_capable_during_service_discovery(bd_addr); if (is_transport_le) { - log::info("New GATT over LE UUIDs for {}:", bd_addr); + log::info("New GATT over LE UUIDs for {}", bd_addr); BTM_LogHistory(kBtmLogTag, bd_addr, "Discovered GATT services using LE transport"); if (btif_is_gatt_service_discovery_post_pairing(bd_addr)) { pairing_cb.gatt_over_le = btif_dm_pairing_cb_t::ServiceDiscoveryState::FINISHED; @@ -1786,7 +1786,7 @@ static void btif_on_gatt_results(RawAddress bd_addr, std::vector<bluetooth::Uuid } } } else { - log::debug("New GATT over SDP UUIDs for {}:", bd_addr); + log::debug("New GATT over SDP UUIDs for {}", bd_addr); BTM_LogHistory(kBtmLogTag, bd_addr, "Discovered GATT services using SDP transport"); } diff --git a/system/gd/hal/hci_hal_android_test.cc b/system/gd/hal/hci_hal_android_test.cc index 247f15745f..f128f508cf 100644 --- a/system/gd/hal/hci_hal_android_test.cc +++ b/system/gd/hal/hci_hal_android_test.cc @@ -20,6 +20,7 @@ #include <queue> #include <thread> +#include "com_android_bluetooth_flags.h" #include "hal/hci_backend.h" #include "hal/hci_hal.h" #include "os/thread.h" @@ -79,10 +80,13 @@ protected: } void TearDown() override { - fake_registry_.StopAll(); handler_->Clear(); - delete thread_; + if (com::android::bluetooth::flags::same_handler_for_all_modules()) { + handler_->WaitUntilStopped(bluetooth::kHandlerStopTimeout); + } + fake_registry_.StopAll(); delete handler_; + delete thread_; } HciHal* hal; diff --git a/system/gd/hal/hci_hal_host_test.cc b/system/gd/hal/hci_hal_host_test.cc index 4f4c6bc6d0..d4539d6c3b 100644 --- a/system/gd/hal/hci_hal_host_test.cc +++ b/system/gd/hal/hci_hal_host_test.cc @@ -32,6 +32,7 @@ #include <utility> #include <vector> +#include "com_android_bluetooth_flags.h" #include "hal/hci_hal.h" #include "hal/serialize_packet.h" #include "os/thread.h" @@ -155,12 +156,15 @@ protected: void TearDown() override { hal_->unregisterIncomingPacketCallback(); + handler_->Clear(); + if (com::android::bluetooth::flags::same_handler_for_all_modules()) { + handler_->WaitUntilStopped(bluetooth::kHandlerStopTimeout); + } fake_registry_.StopAll(); + delete handler_; close(fake_server_socket_); - handler_->Clear(); delete fake_server_; delete thread_; - delete handler_; } void SetFakeServerSocketToBlocking() { diff --git a/system/gd/hci/acl_manager/round_robin_scheduler.cc b/system/gd/hci/acl_manager/round_robin_scheduler.cc index ad0e6dc621..89897a82e5 100644 --- a/system/gd/hci/acl_manager/round_robin_scheduler.cc +++ b/system/gd/hci/acl_manager/round_robin_scheduler.cc @@ -17,7 +17,6 @@ #include "hci/acl_manager/round_robin_scheduler.h" #include <bluetooth/log.h> -#include <com_android_bluetooth_flags.h> #include <memory> #include <utility> @@ -65,10 +64,8 @@ void RoundRobinScheduler::Unregister(uint16_t handle) { log::assert_that(acl_queue_handlers_.count(handle) == 1, "assert failed: acl_queue_handlers_.count(handle) == 1"); - if (com::android::bluetooth::flags::drop_acl_fragment_on_disconnect()) { - // Drop the pending fragments and recalculate number_of_sent_packets_ - drop_packet_fragments(handle); - } + // Drop the pending fragments and recalculate number_of_sent_packets_ + drop_packet_fragments(handle); auto& acl_queue_handler = acl_queue_handlers_.find(handle)->second; log::info("unregistering acl_queue handle={}, sent_packets={}", handle, @@ -94,8 +91,7 @@ void RoundRobinScheduler::Unregister(uint16_t handle) { starting_point_ = acl_queue_handlers_.begin(); // Restart sending packets if we got acl credits - if (com::android::bluetooth::flags::drop_acl_fragment_on_disconnect() && - credits_reclaimed_from_zero) { + if (credits_reclaimed_from_zero) { start_round_robin(); } } diff --git a/system/gd/hci/acl_manager/round_robin_scheduler_test.cc b/system/gd/hci/acl_manager/round_robin_scheduler_test.cc index 25bd2aff80..b990332c9e 100644 --- a/system/gd/hci/acl_manager/round_robin_scheduler_test.cc +++ b/system/gd/hci/acl_manager/round_robin_scheduler_test.cc @@ -16,7 +16,6 @@ #include "hci/acl_manager/round_robin_scheduler.h" -#include <com_android_bluetooth_flags.h> #include <gtest/gtest.h> #include "common/bidi_queue.h" @@ -422,8 +421,6 @@ TEST_F(RoundRobinSchedulerTest, receive_le_credit_when_next_fragment_is_classic) } TEST_F(RoundRobinSchedulerTest, unregister_reclaim_credits) { - com::android::bluetooth::flags::provider_->drop_acl_fragment_on_disconnect(true); - uint16_t handle = 0x01; auto connection_queue = std::make_shared<AclConnection::Queue>(20); auto new_connection_queue = std::make_shared<AclConnection::Queue>(20); diff --git a/system/gd/hci/controller.cc b/system/gd/hci/controller.cc index b14e434781..919643f14d 100644 --- a/system/gd/hci/controller.cc +++ b/system/gd/hci/controller.cc @@ -61,9 +61,7 @@ struct Controller::impl { handler->BindOn(this, &Controller::impl::NumberOfCompletedPackets)); set_event_mask(kDefaultEventMask); - if (com::android::bluetooth::flags::encryption_change_v2()) { - set_event_mask_page_2(kDefaultEventMaskPage2); - } + set_event_mask_page_2(kDefaultEventMaskPage2); write_le_host_support(Enable::ENABLED, Enable::DISABLED); hci_->EnqueueCommand( diff --git a/system/gd/hci/le_address_manager.cc b/system/gd/hci/le_address_manager.cc index 804f88985e..327ea0f0ca 100644 --- a/system/gd/hci/le_address_manager.cc +++ b/system/gd/hci/le_address_manager.cc @@ -165,12 +165,8 @@ void LeAddressManager::SetPrivacyPolicyForInitiatorAddress( min_seconds.count(), max_seconds.count()); enqueue_command_.Run(std::move(packet)); } else { - if (com::android::bluetooth::flags::non_wake_alarm_for_rpa_rotation()) { - address_rotation_wake_alarm_ = std::make_unique<os::Alarm>(handler_, true); - address_rotation_non_wake_alarm_ = std::make_unique<os::Alarm>(handler_, false); - } else { - address_rotation_wake_alarm_ = std::make_unique<os::Alarm>(handler_); - } + address_rotation_wake_alarm_ = std::make_unique<os::Alarm>(handler_, true); + address_rotation_non_wake_alarm_ = std::make_unique<os::Alarm>(handler_, false); } set_random_address(); break; @@ -229,12 +225,8 @@ void LeAddressManager::SetPrivacyPolicyForInitiatorAddressForTest( min_seconds.count(), max_seconds.count()); enqueue_command_.Run(std::move(packet)); } else { - if (com::android::bluetooth::flags::non_wake_alarm_for_rpa_rotation()) { - address_rotation_wake_alarm_ = std::make_unique<os::Alarm>(handler_, true); - address_rotation_non_wake_alarm_ = std::make_unique<os::Alarm>(handler_, false); - } else { - address_rotation_wake_alarm_ = std::make_unique<os::Alarm>(handler_); - } + address_rotation_wake_alarm_ = std::make_unique<os::Alarm>(handler_, true); + address_rotation_non_wake_alarm_ = std::make_unique<os::Alarm>(handler_, false); set_random_address(); } break; @@ -422,31 +414,25 @@ void LeAddressManager::prepare_to_rotate() { } void LeAddressManager::schedule_rotate_random_address() { - if (com::android::bluetooth::flags::non_wake_alarm_for_rpa_rotation()) { - std::string client_name = "LeAddressManager"; - auto privateAddressIntervalRange = GetNextPrivateAddressIntervalRange(client_name); - address_rotation_wake_alarm_->Schedule( - common::BindOnce( - []() { log::info("deadline wakeup in schedule_rotate_random_address"); }), - privateAddressIntervalRange.max); - address_rotation_non_wake_alarm_->Schedule( - common::BindOnce(&LeAddressManager::prepare_to_rotate, common::Unretained(this)), - privateAddressIntervalRange.min); - - auto now = std::chrono::system_clock::now(); - if (address_rotation_interval_min.has_value()) { - CheckAddressRotationHappenedInExpectedTimeInterval( - *address_rotation_interval_min, *address_rotation_interval_max, now, client_name); - } - - // Update the expected range here. - address_rotation_interval_min.emplace(now + privateAddressIntervalRange.min); - address_rotation_interval_max.emplace(now + privateAddressIntervalRange.max); - } else { - address_rotation_wake_alarm_->Schedule( - common::BindOnce(&LeAddressManager::prepare_to_rotate, common::Unretained(this)), - GetNextPrivateAddressIntervalMs()); + std::string client_name = "LeAddressManager"; + auto privateAddressIntervalRange = GetNextPrivateAddressIntervalRange(client_name); + address_rotation_wake_alarm_->Schedule( + common::BindOnce( + []() { log::info("deadline wakeup in schedule_rotate_random_address"); }), + privateAddressIntervalRange.max); + address_rotation_non_wake_alarm_->Schedule( + common::BindOnce(&LeAddressManager::prepare_to_rotate, common::Unretained(this)), + privateAddressIntervalRange.min); + + auto now = std::chrono::system_clock::now(); + if (address_rotation_interval_min.has_value()) { + CheckAddressRotationHappenedInExpectedTimeInterval( + *address_rotation_interval_min, *address_rotation_interval_max, now, client_name); } + + // Update the expected range here. + address_rotation_interval_min.emplace(now + privateAddressIntervalRange.min); + address_rotation_interval_max.emplace(now + privateAddressIntervalRange.max); } void LeAddressManager::set_random_address() { diff --git a/system/gd/hci/le_advertising_manager.cc b/system/gd/hci/le_advertising_manager.cc index f9a80f44f8..d626b2ee15 100644 --- a/system/gd/hci/le_advertising_manager.cc +++ b/system/gd/hci/le_advertising_manager.cc @@ -374,39 +374,29 @@ struct LeAdvertisingManager::impl : public bluetooth::hci::LeAddressManagerCallb log::info("Reenable advertising"); if (was_rotating_address) { log::info("Scheduling address rotation for advertiser_id={}", advertiser_id); - if (com::android::bluetooth::flags::non_wake_alarm_for_rpa_rotation()) { - advertising_sets_[advertiser_id].address_rotation_wake_alarm_ = - std::make_unique<os::Alarm>(module_handler_, true); - advertising_sets_[advertiser_id].address_rotation_non_wake_alarm_ = - std::make_unique<os::Alarm>(module_handler_, false); - - std::string client_name = "advertising_set_" + std::to_string(advertiser_id); - auto privateAddressIntervalRange = - le_address_manager_->GetNextPrivateAddressIntervalRange(client_name); - - advertising_sets_[advertiser_id].address_rotation_wake_alarm_->Schedule( - common::BindOnce( - []() { log::info("deadline wakeup in handle_set_terminated"); }), - privateAddressIntervalRange.max); - advertising_sets_[advertiser_id].address_rotation_non_wake_alarm_->Schedule( - common::BindOnce(&impl::set_advertising_set_random_address_on_timer, - common::Unretained(this), advertiser_id), - privateAddressIntervalRange.min); - - // Update the expected range here. - auto now = std::chrono::system_clock::now(); - advertising_sets_[advertiser_id].address_rotation_interval_min.emplace( - now + privateAddressIntervalRange.min); - advertising_sets_[advertiser_id].address_rotation_interval_max.emplace( - now + privateAddressIntervalRange.max); - } else { - advertising_sets_[advertiser_id].address_rotation_wake_alarm_ = - std::make_unique<os::Alarm>(module_handler_); - advertising_sets_[advertiser_id].address_rotation_wake_alarm_->Schedule( - common::BindOnce(&impl::set_advertising_set_random_address_on_timer, - common::Unretained(this), advertiser_id), - le_address_manager_->GetNextPrivateAddressIntervalMs()); - } + advertising_sets_[advertiser_id].address_rotation_wake_alarm_ = + std::make_unique<os::Alarm>(module_handler_, true); + advertising_sets_[advertiser_id].address_rotation_non_wake_alarm_ = + std::make_unique<os::Alarm>(module_handler_, false); + + std::string client_name = "advertising_set_" + std::to_string(advertiser_id); + auto privateAddressIntervalRange = + le_address_manager_->GetNextPrivateAddressIntervalRange(client_name); + + advertising_sets_[advertiser_id].address_rotation_wake_alarm_->Schedule( + common::BindOnce([]() { log::info("deadline wakeup in handle_set_terminated"); }), + privateAddressIntervalRange.max); + advertising_sets_[advertiser_id].address_rotation_non_wake_alarm_->Schedule( + common::BindOnce(&impl::set_advertising_set_random_address_on_timer, + common::Unretained(this), advertiser_id), + privateAddressIntervalRange.min); + + // Update the expected range here. + auto now = std::chrono::system_clock::now(); + advertising_sets_[advertiser_id].address_rotation_interval_min.emplace( + now + privateAddressIntervalRange.min); + advertising_sets_[advertiser_id].address_rotation_interval_max.emplace( + now + privateAddressIntervalRange.max); } enable_advertiser(advertiser_id, true, 0, 0); } @@ -673,40 +663,31 @@ struct LeAdvertisingManager::impl : public bluetooth::hci::LeAddressManagerCallb !leaudio_requested_nrpa && (!controller_->IsRpaGenerationSupported())) { // start timer for random address log::info("Scheduling address rotation for advertiser_id={}", id); - if (com::android::bluetooth::flags::non_wake_alarm_for_rpa_rotation()) { - advertising_sets_[id].address_rotation_wake_alarm_ = - std::make_unique<os::Alarm>(module_handler_, true); - advertising_sets_[id].address_rotation_non_wake_alarm_ = - std::make_unique<os::Alarm>(module_handler_, false); - - std::string client_name = "advertising_set_" + std::to_string(id); - auto privateAddressIntervalRange = - le_address_manager_->GetNextPrivateAddressIntervalRange(client_name); - - advertising_sets_[id].address_rotation_wake_alarm_->Schedule( - common::BindOnce([]() { - log::info("deadline wakeup in create_extended_advertiser_with_id"); - }), - privateAddressIntervalRange.max); - advertising_sets_[id].address_rotation_non_wake_alarm_->Schedule( - common::BindOnce(&impl::set_advertising_set_random_address_on_timer, - common::Unretained(this), id), - privateAddressIntervalRange.min); - - // Update the expected range here. - auto now = std::chrono::system_clock::now(); - advertising_sets_[id].address_rotation_interval_min.emplace( - now + privateAddressIntervalRange.min); - advertising_sets_[id].address_rotation_interval_max.emplace( - now + privateAddressIntervalRange.max); - } else { - advertising_sets_[id].address_rotation_wake_alarm_ = - std::make_unique<os::Alarm>(module_handler_); - advertising_sets_[id].address_rotation_wake_alarm_->Schedule( - common::BindOnce(&impl::set_advertising_set_random_address_on_timer, - common::Unretained(this), id), - le_address_manager_->GetNextPrivateAddressIntervalMs()); - } + advertising_sets_[id].address_rotation_wake_alarm_ = + std::make_unique<os::Alarm>(module_handler_, true); + advertising_sets_[id].address_rotation_non_wake_alarm_ = + std::make_unique<os::Alarm>(module_handler_, false); + + std::string client_name = "advertising_set_" + std::to_string(id); + auto privateAddressIntervalRange = + le_address_manager_->GetNextPrivateAddressIntervalRange(client_name); + + advertising_sets_[id].address_rotation_wake_alarm_->Schedule( + common::BindOnce([]() { + log::info("deadline wakeup in create_extended_advertiser_with_id"); + }), + privateAddressIntervalRange.max); + advertising_sets_[id].address_rotation_non_wake_alarm_->Schedule( + common::BindOnce(&impl::set_advertising_set_random_address_on_timer, + common::Unretained(this), id), + privateAddressIntervalRange.min); + + // Update the expected range here. + auto now = std::chrono::system_clock::now(); + advertising_sets_[id].address_rotation_interval_min.emplace( + now + privateAddressIntervalRange.min); + advertising_sets_[id].address_rotation_interval_max.emplace( + now + privateAddressIntervalRange.max); } } if (config.advertising_type == AdvertisingType::ADV_IND || @@ -859,39 +840,31 @@ struct LeAdvertisingManager::impl : public bluetooth::hci::LeAddressManagerCallb } log::info("Scheduling address rotation for advertiser_id={}", advertiser_id); - if (com::android::bluetooth::flags::non_wake_alarm_for_rpa_rotation()) { - std::string client_name = "advertising_set_" + std::to_string(advertiser_id); - auto privateAddressIntervalRange = - le_address_manager_->GetNextPrivateAddressIntervalRange(client_name); - advertising_sets_[advertiser_id].address_rotation_wake_alarm_->Schedule( - common::BindOnce([]() { - log::info("deadline wakeup in set_advertising_set_random_address_on_timer"); - }), - privateAddressIntervalRange.max); - advertising_sets_[advertiser_id].address_rotation_non_wake_alarm_->Schedule( - common::BindOnce(&impl::set_advertising_set_random_address_on_timer, - common::Unretained(this), advertiser_id), - privateAddressIntervalRange.min); - - auto now = std::chrono::system_clock::now(); - if (advertising_sets_[advertiser_id].address_rotation_interval_min.has_value()) { - le_address_manager_->CheckAddressRotationHappenedInExpectedTimeInterval( - *(advertising_sets_[advertiser_id].address_rotation_interval_min), - *(advertising_sets_[advertiser_id].address_rotation_interval_max), now, - client_name); - } - - // Update the expected range here. - advertising_sets_[advertiser_id].address_rotation_interval_min.emplace( - now + privateAddressIntervalRange.min); - advertising_sets_[advertiser_id].address_rotation_interval_max.emplace( - now + privateAddressIntervalRange.max); - } else { - advertising_sets_[advertiser_id].address_rotation_wake_alarm_->Schedule( - common::BindOnce(&impl::set_advertising_set_random_address_on_timer, - common::Unretained(this), advertiser_id), - le_address_manager_->GetNextPrivateAddressIntervalMs()); + std::string client_name = "advertising_set_" + std::to_string(advertiser_id); + auto privateAddressIntervalRange = + le_address_manager_->GetNextPrivateAddressIntervalRange(client_name); + advertising_sets_[advertiser_id].address_rotation_wake_alarm_->Schedule( + common::BindOnce([]() { + log::info("deadline wakeup in set_advertising_set_random_address_on_timer"); + }), + privateAddressIntervalRange.max); + advertising_sets_[advertiser_id].address_rotation_non_wake_alarm_->Schedule( + common::BindOnce(&impl::set_advertising_set_random_address_on_timer, + common::Unretained(this), advertiser_id), + privateAddressIntervalRange.min); + + auto now = std::chrono::system_clock::now(); + if (advertising_sets_[advertiser_id].address_rotation_interval_min.has_value()) { + le_address_manager_->CheckAddressRotationHappenedInExpectedTimeInterval( + *(advertising_sets_[advertiser_id].address_rotation_interval_min), + *(advertising_sets_[advertiser_id].address_rotation_interval_max), now, client_name); } + + // Update the expected range here. + advertising_sets_[advertiser_id].address_rotation_interval_min.emplace( + now + privateAddressIntervalRange.min); + advertising_sets_[advertiser_id].address_rotation_interval_max.emplace( + now + privateAddressIntervalRange.max); } void register_advertiser( diff --git a/system/gd/module.cc b/system/gd/module.cc index 8904f0c559..76b386bbfe 100644 --- a/system/gd/module.cc +++ b/system/gd/module.cc @@ -128,10 +128,18 @@ void ModuleRegistry::StopAll() { auto module = Get(*it); last_instance_ = "stopping " + module->ToString(); - // Clear the handler before stopping the module to allow it to shut down gracefully. - log::info("Stopping Handler of Module {}", module->ToString()); - module->handler_->Clear(); - module->handler_->WaitUntilStopped(kModuleStopTimeout); + /* + * b/393449774 since we have now shifted to a single handler for all modules, we don't need + * to clear the handler here, it will be done in the respective teardown. + * Since we have a single handler, we need to make sure that the handler instance is deleted + * only once, otherwise we will see a crash as a handler can only be cleared once. + */ + if (!com::android::bluetooth::flags::same_handler_for_all_modules()) { + // Clear the handler before stopping the module to allow it to shut down gracefully. + log::info("Stopping Handler of Module {}", module->ToString()); + module->handler_->Clear(); + module->handler_->WaitUntilStopped(kModuleStopTimeout); + } log::info("Stopping Module {}", module->ToString()); module->Stop(); } @@ -144,7 +152,9 @@ void ModuleRegistry::StopAll() { auto instance = started_modules_.find(*it); log::assert_that(instance != started_modules_.end(), "assert failed: instance != started_modules_.end()"); - delete instance->second->handler_; + if (!com::android::bluetooth::flags::same_handler_for_all_modules()) { + delete instance->second->handler_; + } delete instance->second; started_modules_.erase(instance); } @@ -165,4 +175,15 @@ os::Handler* ModuleRegistry::GetModuleHandler(const ModuleFactory* module) const return nullptr; } +// Override the StopAll method to use the test thread and handler. +// This function will take care of releasing the handler instances. +void TestModuleRegistry::StopAll() { + os::Handler* handler = GetTestHandler(); + handler->Clear(); + if (com::android::bluetooth::flags::same_handler_for_all_modules()) { + handler->WaitUntilStopped(kHandlerStopTimeout); + } + ModuleRegistry::StopAll(); // call the base class StopAll + delete handler; +} } // namespace bluetooth diff --git a/system/gd/module.h b/system/gd/module.h index 816d455482..57e4db7d16 100644 --- a/system/gd/module.h +++ b/system/gd/module.h @@ -32,6 +32,9 @@ #include "os/thread.h" namespace bluetooth { +// Timeout for waiting for a handler to stop, used in Handler::WaitUntilStopped() +constexpr std::chrono::milliseconds kHandlerStopTimeout = std::chrono::milliseconds(2000); + namespace shim { class Stack; } // namespace shim @@ -191,6 +194,9 @@ public: os::Thread& GetTestThread() { return test_thread; } os::Handler* GetTestHandler() { return test_handler_; } + // Override the StopAll method to use the test thread and handler. + void StopAll(); + bool SynchronizeModuleHandler(const ModuleFactory* module, std::chrono::milliseconds timeout) const { return SynchronizeHandler(GetTestModuleHandler(module), timeout); diff --git a/system/gd/module_unittest.cc b/system/gd/module_unittest.cc index d1bf59ac73..8fb140c229 100644 --- a/system/gd/module_unittest.cc +++ b/system/gd/module_unittest.cc @@ -22,6 +22,7 @@ #include <sstream> #include <string> +#include "com_android_bluetooth_flags.h" #include "gtest/gtest.h" #include "os/handler.h" #include "os/thread.h" @@ -41,9 +42,12 @@ protected: void TearDown() override { handler_->Clear(); + if (com::android::bluetooth::flags::same_handler_for_all_modules()) { + handler_->WaitUntilStopped(kHandlerStopTimeout); + } delete registry_; - delete thread_; delete handler_; + delete thread_; } ModuleRegistry* registry_; diff --git a/system/gd/os/linux_generic/alarm.cc b/system/gd/os/linux_generic/alarm.cc index 8846ede579..a378ca93cd 100644 --- a/system/gd/os/linux_generic/alarm.cc +++ b/system/gd/os/linux_generic/alarm.cc @@ -17,7 +17,6 @@ #include "os/alarm.h" #include <bluetooth/log.h> -#include <com_android_bluetooth_flags.h> #include <sys/timerfd.h> #include <unistd.h> @@ -41,8 +40,7 @@ using common::OnceClosure; Alarm::Alarm(Handler* handler) : Alarm(handler, true) {} Alarm::Alarm(Handler* handler, bool isWakeAlarm) : handler_(handler) { - int timerfd_flag = - com::android::bluetooth::flags::non_wake_alarm_for_rpa_rotation() ? TFD_NONBLOCK : 0; + int timerfd_flag = TFD_NONBLOCK; fd_ = TIMERFD_CREATE(isWakeAlarm ? ALARM_CLOCK : CLOCK_BOOTTIME, timerfd_flag); @@ -85,7 +83,7 @@ void Alarm::on_fire() { auto bytes_read = read(fd_, ×_invoked, sizeof(uint64_t)); lock.unlock(); - if (com::android::bluetooth::flags::non_wake_alarm_for_rpa_rotation() && bytes_read == -1) { + if (bytes_read == -1) { log::debug("No data to read."); if (errno == EAGAIN || errno == EWOULDBLOCK) { log::debug("Alarm is already canceled or rescheduled."); diff --git a/system/main/shim/stack.cc b/system/main/shim/stack.cc index 14447d75b3..4560a6180f 100644 --- a/system/main/shim/stack.cc +++ b/system/main/shim/stack.cc @@ -170,6 +170,9 @@ void Stack::Stop() { is_running_ = false; stack_handler_->Clear(); + if(com::android::bluetooth::flags::same_handler_for_all_modules()) { + stack_handler_->WaitUntilStopped(bluetooth::kHandlerStopTimeout); + } WakelockManager::Get().Acquire(); diff --git a/system/stack/btm/btm_sec.cc b/system/stack/btm/btm_sec.cc index 3eda3ca519..439fa7bc0f 100644 --- a/system/stack/btm/btm_sec.cc +++ b/system/stack/btm/btm_sec.cc @@ -2440,8 +2440,7 @@ void btm_io_capabilities_req(RawAddress p) { /* If device is bonded, and encrypted it's upgrading security and it's ok. * If it's bonded and not encrypted, it's remote missing keys scenario */ - if (!p_dev_rec->sec_rec.is_device_encrypted() && - com::android::bluetooth::flags::key_missing_classic_device()) { + if (!p_dev_rec->sec_rec.is_device_encrypted()) { log::warn("Incoming bond request, but {} is already bonded (notifying user)", p); bta_dm_remote_key_missing(p); btm_sec_disconnect(p_dev_rec->hci_handle, HCI_ERR_AUTH_FAILURE, @@ -2632,8 +2631,7 @@ void btm_io_capabilities_rsp(const tBTM_SP_IO_RSP evt_data) { /* If device is bonded, and encrypted it's upgrading security and it's ok. * If it's bonded and not encrypted, it's remote missing keys scenario */ - if (btm_sec_is_a_bonded_dev(evt_data.bd_addr) && !p_dev_rec->sec_rec.is_device_encrypted() && - com::android::bluetooth::flags::key_missing_classic_device()) { + if (btm_sec_is_a_bonded_dev(evt_data.bd_addr) && !p_dev_rec->sec_rec.is_device_encrypted()) { log::warn("Incoming bond request, but {} is already bonded (notifying user)", evt_data.bd_addr); bta_dm_remote_key_missing(evt_data.bd_addr); btm_sec_disconnect(p_dev_rec->hci_handle, HCI_ERR_AUTH_FAILURE, @@ -3031,8 +3029,7 @@ void btm_sec_auth_complete(uint16_t handle, tHCI_STATUS status) { p_dev_rec->sec_rec.classic_link, p_dev_rec->bd_addr, reinterpret_cast<char const*>(p_dev_rec->sec_bd_name)); - if (status == HCI_ERR_KEY_MISSING && - com::android::bluetooth::flags::key_missing_classic_device()) { + if (status == HCI_ERR_KEY_MISSING) { log::warn("auth_complete KEY_MISSING {} is already bonded (notifying user)", p_dev_rec->bd_addr); bta_dm_remote_key_missing(p_dev_rec->bd_addr); @@ -3321,10 +3318,8 @@ void btm_sec_encrypt_change(uint16_t handle, tHCI_STATUS status, uint8_t encr_en if (status == HCI_ERR_KEY_MISSING) { log::info("Remote key missing - will report"); bta_dm_remote_key_missing(p_dev_rec->ble.pseudo_addr); - if (com::android::bluetooth::flags::sec_disconnect_on_le_key_missing()) { - btm_sec_send_hci_disconnect(p_dev_rec, HCI_ERR_HOST_REJECT_SECURITY, - p_dev_rec->ble_hci_handle, "encryption_change:key_missing"); - } + btm_sec_send_hci_disconnect(p_dev_rec, HCI_ERR_HOST_REJECT_SECURITY, + p_dev_rec->ble_hci_handle, "encryption_change:key_missing"); return; } diff --git a/system/stack/l2cap/l2c_ble.cc b/system/stack/l2cap/l2c_ble.cc index 49dca969dc..d964271002 100644 --- a/system/stack/l2cap/l2c_ble.cc +++ b/system/stack/l2cap/l2c_ble.cc @@ -1443,7 +1443,7 @@ tL2CAP_LE_RESULT_CODE l2ble_sec_access_req(const RawAddress& bd_addr, uint16_t p * constraints. For example, when there is at least one Hearing Aid device * bonded, the minimum interval is raised. On return, min_interval and * max_interval are updated. */ -void L2CA_AdjustConnectionIntervals(uint16_t* min_interval, uint16_t* max_interval, +void L2CA_AdjustConnectionIntervals(uint16_t* /* min_interval */, uint16_t* max_interval, uint16_t floor_interval) { // Allow for customization by systemprops for mainline uint16_t phone_min_interval = floor_interval; @@ -1462,13 +1462,6 @@ void L2CA_AdjustConnectionIntervals(uint16_t* min_interval, uint16_t* max_interv log::verbose("Have Hearing Aids. Min. interval is set to {}", phone_min_interval); } - if (!com::android::bluetooth::flags::l2cap_le_do_not_adjust_min_interval() && - *min_interval < phone_min_interval) { - log::verbose("requested min_interval={} too small. Set to {}", *min_interval, - phone_min_interval); - *min_interval = phone_min_interval; - } - // While this could result in connection parameters that fall // outside fo the range requested, this will allow the connection // to remain established. diff --git a/system/stack/smp/smp_l2c.cc b/system/stack/smp/smp_l2c.cc index 219cb723f7..69591b10e5 100644 --- a/system/stack/smp/smp_l2c.cc +++ b/system/stack/smp/smp_l2c.cc @@ -263,13 +263,6 @@ static void smp_br_connect_callback(uint16_t /* channel */, const RawAddress& bd log::info("BDA:{} pairing_bda:{}, connected:{}", bd_addr, p_cb->pairing_bda, connected); if (bd_addr != p_cb->pairing_bda) { - if (!com::android::bluetooth::flags::smp_state_machine_stuck_after_disconnection_fix()) { - log::info( - "If your pairing failed, get a build with " - "smp_state_machine_stuck_after_disconnection_fix and try again :)"); - return; - } - tBTM_SEC_DEV_REC* p_dev_rec = btm_find_dev(bd_addr); /* When pairing was initiated to RPA, and connection was on LE transport first using RPA, then * we must check record pseudo address, it might be same device */ |