Loading framework/java/android/bluetooth/BluetoothHapClient.java +61 −112 Original line number Diff line number Diff line Loading @@ -39,14 +39,11 @@ import android.util.CloseGuard; import android.util.Log; import com.android.bluetooth.flags.Flags; import com.android.internal.annotations.GuardedBy; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import java.util.function.Consumer; Loading @@ -62,9 +59,6 @@ import java.util.function.Consumer; public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable { private static final String TAG = BluetoothHapClient.class.getSimpleName(); @GuardedBy("mCallbackExecutorMap") private final Map<Callback, Executor> mCallbackExecutorMap = new HashMap<>(); private CloseGuard mCloseGuard; /** Loading Loading @@ -237,53 +231,46 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable int hapGroupId, @GroupPresetNameChangeFailureReason int reason); } private final IBluetoothHapClientCallback mCallback = new IBluetoothHapClientCallback.Stub() { private void forEach(Consumer<BluetoothHapClient.Callback> consumer) { synchronized (mCallbackExecutorMap) { mCallbackExecutorMap.forEach( (callback, executor) -> executor.execute(() -> consumer.accept(callback))); } } private final CallbackWrapper<Callback, IBluetoothHapClient> mCallbackWrapper; private final IBluetoothHapClientCallback mCallback = new HapClientNotifyCallback(); private class HapClientNotifyCallback extends IBluetoothHapClientCallback.Stub { @Override public void onPresetSelected(BluetoothDevice device, int presetIndex, int reason) { Attributable.setAttributionSource(device, mAttributionSource); forEach((cb) -> cb.onPresetSelected(device, presetIndex, reason)); mCallbackWrapper.forEach((cb) -> cb.onPresetSelected(device, presetIndex, reason)); } @Override public void onPresetSelectionFailed(BluetoothDevice device, int status) { Attributable.setAttributionSource(device, mAttributionSource); forEach((cb) -> cb.onPresetSelectionFailed(device, status)); mCallbackWrapper.forEach((cb) -> cb.onPresetSelectionFailed(device, status)); } @Override public void onPresetSelectionForGroupFailed(int hapGroupId, int status) { forEach((cb) -> cb.onPresetSelectionForGroupFailed(hapGroupId, status)); public void onPresetSelectionForGroupFailed(int groupId, int status) { mCallbackWrapper.forEach((cb) -> cb.onPresetSelectionForGroupFailed(groupId, status)); } @Override public void onPresetInfoChanged( BluetoothDevice device, List<BluetoothHapPresetInfo> presetInfoList, int status) { BluetoothDevice device, List<BluetoothHapPresetInfo> presets, int status) { Attributable.setAttributionSource(device, mAttributionSource); forEach((cb) -> cb.onPresetInfoChanged(device, presetInfoList, status)); mCallbackWrapper.forEach((cb) -> cb.onPresetInfoChanged(device, presets, status)); } @Override public void onSetPresetNameFailed(BluetoothDevice device, int status) { Attributable.setAttributionSource(device, mAttributionSource); forEach((cb) -> cb.onSetPresetNameFailed(device, status)); mCallbackWrapper.forEach((cb) -> cb.onSetPresetNameFailed(device, status)); } @Override public void onSetPresetNameForGroupFailed(int hapGroupId, int status) { forEach((cb) -> cb.onSetPresetNameForGroupFailed(hapGroupId, status)); mCallbackWrapper.forEach((cb) -> cb.onSetPresetNameForGroupFailed(hapGroupId, status)); } } }; /** * Intent used to broadcast the change in connection state of the Hearing Access Profile Client Loading Loading @@ -462,6 +449,24 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable mAttributionSource = mAdapter.getAttributionSource(); mService = null; Consumer<IBluetoothHapClient> registerConsumer = (IBluetoothHapClient service) -> { try { service.registerCallback(mCallback, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } }; Consumer<IBluetoothHapClient> unregisterConsumer = (IBluetoothHapClient service) -> { try { service.unregisterCallback(mCallback, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } }; mCallbackWrapper = new CallbackWrapper(registerConsumer, unregisterConsumer); mCloseGuard = new CloseGuard(); mCloseGuard.open("close"); } Loading @@ -486,18 +491,7 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable @Override public void onServiceConnected(IBinder service) { mService = IBluetoothHapClient.Stub.asInterface(service); // re-register the service-to-app callback synchronized (mCallbackExecutorMap) { if (mCallbackExecutorMap.isEmpty()) { return; } try { mService.registerCallback(mCallback, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } } mCallbackWrapper.registerToNewService(mService); } /** @hide */ Loading Loading @@ -535,34 +529,7 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public void registerCallback( @NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) { requireNonNull(executor); requireNonNull(callback); synchronized (mCallbackExecutorMap) { // If the callback map is empty, we register the service-to-app callback if (mCallbackExecutorMap.isEmpty()) { if (!mAdapter.isEnabled()) { /* If Bluetooth is off, just store callback and it will be registered * when Bluetooth is on */ mCallbackExecutorMap.put(callback, executor); return; } try { final IBluetoothHapClient service = getService(); if (service != null) { service.registerCallback(mCallback, mAttributionSource); } } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } // Adds the passed in callback to our map of callbacks to executors if (mCallbackExecutorMap.containsKey(callback)) { throw new IllegalArgumentException("This callback has already been registered"); } mCallbackExecutorMap.put(callback, executor); } mCallbackWrapper.registerCallback(mService, callback, executor); } /** Loading @@ -582,25 +549,7 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public void unregisterCallback(@NonNull Callback callback) { requireNonNull(callback); synchronized (mCallbackExecutorMap) { if (mCallbackExecutorMap.remove(callback) == null) { throw new IllegalArgumentException("This callback has not been registered"); } // If the callback map is empty, we unregister the service-to-app callback if (mCallbackExecutorMap.isEmpty()) { try { final IBluetoothHapClient service = getService(); if (service != null) { service.unregisterCallback(mCallback, mAttributionSource); } } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } } mCallbackWrapper.unregisterCallback(mService, callback); } /** Loading framework/java/android/bluetooth/CallbackWrapper.java 0 → 100644 +126 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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 android.bluetooth; import static java.util.Objects.requireNonNull; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import java.util.function.Consumer; /** * This class provide a common abstraction to deal with callback registration and broadcast from the * bluetooth to external app using their executor. It is designed to be thread safe. It register the * client the first time a callback is registered, and unregister when the last callback is * unregistered. It handles when the service to connect is not yet available and provide a method to * register the callback to the newly available service. @see #registerToNewService * * @param <S> the type of binder used to register to the bluetooth app (e.g: IBluetoothHapClient) * @param <T> the type of the callback (e.g: BluetoothHapClient.Callback) * @hide */ public class CallbackWrapper<T, S> { @GuardedBy("mCallbackExecutorMap") private final Map<T, Executor> mCallbackExecutorMap; private final Consumer<S> mRegisterConsumer; private final Consumer<S> mUnregisterConsumer; /** * @param registerConsumer is called the first time a callback is being registered or manually * via @see #registerToNewService * @param unregisterConsumer is called when the last callback is removed */ CallbackWrapper(Consumer<S> registerConsumer, Consumer<S> unregisterConsumer) { this(registerConsumer, unregisterConsumer, new HashMap()); } /** * @param map internal map injected for test purpose * @see #CallbackWrapper(Consumer, Consumer) */ @VisibleForTesting public CallbackWrapper( Consumer<S> registerConsumer, Consumer<S> unregisterConsumer, Map<T, Executor> map) { mRegisterConsumer = requireNonNull(registerConsumer); mUnregisterConsumer = requireNonNull(unregisterConsumer); mCallbackExecutorMap = requireNonNull(map); } /** Dispatch the callback from the Bluetooth service to all the currently registered callback */ public void forEach(Consumer<T> consumer) { synchronized (mCallbackExecutorMap) { mCallbackExecutorMap.forEach( (callback, executor) -> executor.execute(() -> consumer.accept(callback))); } } /** Register the callback and save the wrapper to the service if needed */ public void registerCallback( @Nullable S service, @NonNull T callback, @NonNull @CallbackExecutor Executor executor) { requireNonNull(callback); requireNonNull(executor); synchronized (mCallbackExecutorMap) { if (mCallbackExecutorMap.containsKey(callback)) { throw new IllegalArgumentException("Callback already registered"); } if (service != null && mCallbackExecutorMap.isEmpty()) { mRegisterConsumer.accept(service); } mCallbackExecutorMap.put(callback, executor); } } /** Register the callback and remove the wrapper to the service if needed */ public void unregisterCallback(@Nullable S service, @NonNull T callback) { requireNonNull(callback); synchronized (mCallbackExecutorMap) { if (!mCallbackExecutorMap.containsKey(callback)) { throw new IllegalArgumentException("Callback already unregistered"); } mCallbackExecutorMap.remove(callback); if (service != null && mCallbackExecutorMap.isEmpty()) { mUnregisterConsumer.accept(service); } } } /** A new Service is available from Bluetooth and callback need to be registered */ public void registerToNewService(@NonNull S service) { requireNonNull(service); synchronized (mCallbackExecutorMap) { if (!mCallbackExecutorMap.isEmpty()) { mRegisterConsumer.accept(service); } } } } framework/tests/unit/src/android/bluetooth/CallbackWrapperTest.java 0 → 100644 +229 −0 Original line number Diff line number Diff line /* * Copyright 2024 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 android.bluetooth; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.os.Handler; import android.os.HandlerExecutor; import android.os.test.TestLooper; import androidx.test.filters.SmallTest; import com.google.common.truth.Expect; import org.junit.After; 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.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import java.util.function.Consumer; /** Test cases for {@link CallbackWrapper}. */ @SmallTest @RunWith(JUnit4.class) public class CallbackWrapperTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @Rule public Expect expect = Expect.create(); private final Consumer<int[]> mRegisterConsumer = (int[] counter) -> counter[0]++; private final Consumer<int[]> mUnregisterConsumer = (int[] counter) -> counter[0]--; private final int[] mUnusedCounter = {0}; private interface Callback { void onCallbackCalled(); } @Mock private Callback mCallback; @Mock private Callback mCallback2; private TestLooper mLooper; private Executor mExecutor; private Map<Callback, Executor> mCallbackExecutorMap; private CallbackWrapper<Callback, int[]> mCallbackWrapper; @Before public void setUp() { mLooper = new TestLooper(); mExecutor = new HandlerExecutor(new Handler(mLooper.getLooper())); mCallbackExecutorMap = new HashMap(); mCallbackWrapper = new CallbackWrapper(mRegisterConsumer, mUnregisterConsumer, mCallbackExecutorMap); } @After public void tearDown() { assertThat(mLooper.nextMessage()).isNull(); } @Test public void registerCallback_enforceValidParams() { assertThrows( NullPointerException.class, () -> mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, null)); assertThrows( NullPointerException.class, () -> mCallbackWrapper.registerCallback(mUnusedCounter, null, mExecutor)); // Service can be null, the following should not crash mCallbackWrapper.registerCallback(null, mCallback, mExecutor); assertThat(mCallbackExecutorMap).containsExactly(mCallback, mExecutor); } @Test public void unregisterCallback_enforceValidParams() { assertThrows( NullPointerException.class, () -> mCallbackWrapper.unregisterCallback(mUnusedCounter, null)); } @Test public void registerToNewService_enforceValidParams() { assertThrows(NullPointerException.class, () -> mCallbackWrapper.registerToNewService(null)); } @Test public void registerCallback_whenEmpty_callConsumer() { int[] counter = {0}; mCallbackWrapper.registerCallback(counter, mCallback, mExecutor); assertThat(counter[0]).isEqualTo(1); assertThat(mCallbackExecutorMap).containsExactly(mCallback, mExecutor); } @Test public void unregisterCallback_whenRegistered_callConsumer() { mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, mExecutor); int[] counter = {0}; mCallbackWrapper.unregisterCallback(counter, mCallback); assertThat(counter[0]).isEqualTo(-1); assertThat(mCallbackExecutorMap).isEmpty(); } @Test public void unregisterCallbackWithNoService_whenRegistered_stillRemovedFromMap() { mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, mExecutor); mCallbackWrapper.unregisterCallback(null, mCallback); assertThat(mCallbackExecutorMap).isEmpty(); } @Test public void registerCallback_whenWrapperAlreadyRegisteredToService_doNothing() { mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, mExecutor); int[] counter = {0}; mCallbackWrapper.registerCallback(counter, mCallback2, mExecutor); assertThat(counter[0]).isEqualTo(0); assertThat(mCallbackExecutorMap) .containsExactly(mCallback, mExecutor, mCallback2, mExecutor); } @Test public void unregisterCallback_whenMultiplesCallbackAreRegister_doNothing() { mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, mExecutor); mCallbackWrapper.registerCallback(mUnusedCounter, mCallback2, mExecutor); int[] counter = {0}; mCallbackWrapper.unregisterCallback(counter, mCallback2); assertThat(counter[0]).isEqualTo(0); assertThat(mCallbackExecutorMap).containsExactly(mCallback, mExecutor); } @Test public void registerCallback_whenCallbackAlreadyRegistered_throwException() { mCallbackWrapper.registerCallback(null, mCallback, mExecutor); assertThrows( IllegalArgumentException.class, () -> mCallbackWrapper.registerCallback(null, mCallback, mExecutor)); assertThat(mCallbackExecutorMap).containsExactly(mCallback, mExecutor); } @Test public void unregisterCallback_whenCallbackNotRegistered_throwException() { assertThrows( IllegalArgumentException.class, () -> mCallbackWrapper.unregisterCallback(null, mCallback)); assertThat(mCallbackExecutorMap).isEmpty(); } @Test public void registerToNewService_whenNoCallback_doNothing() { int[] counter = {0}; mCallbackWrapper.registerToNewService(counter); assertThat(counter[0]).isEqualTo(0); } @Test public void registerToNewService_whenCallback_callConsumer() { mCallbackWrapper.registerCallback(null, mCallback, mExecutor); int[] counter = {0}; mCallbackWrapper.registerToNewService(counter); assertThat(counter[0]).isEqualTo(1); } @Test public void triggerCallback_whenRegistered_dispatch() { mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, mExecutor); mCallbackWrapper.registerCallback(mUnusedCounter, mCallback2, mExecutor); mCallbackWrapper.forEach((cb) -> cb.onCallbackCalled()); assertThat(mLooper.dispatchAll()).isEqualTo(2); verify(mCallback).onCallbackCalled(); verify(mCallback2).onCallbackCalled(); } @Test public void triggerCallback_whenRegistered_dispatchOnlyOnCurrentlyRegistered() { mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, mExecutor); mCallbackWrapper.registerCallback(mUnusedCounter, mCallback2, mExecutor); mCallbackWrapper.unregisterCallback(mUnusedCounter, mCallback2); mCallbackWrapper.forEach((cb) -> cb.onCallbackCalled()); assertThat(mLooper.dispatchAll()).isEqualTo(1); verify(mCallback).onCallbackCalled(); verify(mCallback2, never()).onCallbackCalled(); } } Loading
framework/java/android/bluetooth/BluetoothHapClient.java +61 −112 Original line number Diff line number Diff line Loading @@ -39,14 +39,11 @@ import android.util.CloseGuard; import android.util.Log; import com.android.bluetooth.flags.Flags; import com.android.internal.annotations.GuardedBy; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import java.util.function.Consumer; Loading @@ -62,9 +59,6 @@ import java.util.function.Consumer; public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable { private static final String TAG = BluetoothHapClient.class.getSimpleName(); @GuardedBy("mCallbackExecutorMap") private final Map<Callback, Executor> mCallbackExecutorMap = new HashMap<>(); private CloseGuard mCloseGuard; /** Loading Loading @@ -237,53 +231,46 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable int hapGroupId, @GroupPresetNameChangeFailureReason int reason); } private final IBluetoothHapClientCallback mCallback = new IBluetoothHapClientCallback.Stub() { private void forEach(Consumer<BluetoothHapClient.Callback> consumer) { synchronized (mCallbackExecutorMap) { mCallbackExecutorMap.forEach( (callback, executor) -> executor.execute(() -> consumer.accept(callback))); } } private final CallbackWrapper<Callback, IBluetoothHapClient> mCallbackWrapper; private final IBluetoothHapClientCallback mCallback = new HapClientNotifyCallback(); private class HapClientNotifyCallback extends IBluetoothHapClientCallback.Stub { @Override public void onPresetSelected(BluetoothDevice device, int presetIndex, int reason) { Attributable.setAttributionSource(device, mAttributionSource); forEach((cb) -> cb.onPresetSelected(device, presetIndex, reason)); mCallbackWrapper.forEach((cb) -> cb.onPresetSelected(device, presetIndex, reason)); } @Override public void onPresetSelectionFailed(BluetoothDevice device, int status) { Attributable.setAttributionSource(device, mAttributionSource); forEach((cb) -> cb.onPresetSelectionFailed(device, status)); mCallbackWrapper.forEach((cb) -> cb.onPresetSelectionFailed(device, status)); } @Override public void onPresetSelectionForGroupFailed(int hapGroupId, int status) { forEach((cb) -> cb.onPresetSelectionForGroupFailed(hapGroupId, status)); public void onPresetSelectionForGroupFailed(int groupId, int status) { mCallbackWrapper.forEach((cb) -> cb.onPresetSelectionForGroupFailed(groupId, status)); } @Override public void onPresetInfoChanged( BluetoothDevice device, List<BluetoothHapPresetInfo> presetInfoList, int status) { BluetoothDevice device, List<BluetoothHapPresetInfo> presets, int status) { Attributable.setAttributionSource(device, mAttributionSource); forEach((cb) -> cb.onPresetInfoChanged(device, presetInfoList, status)); mCallbackWrapper.forEach((cb) -> cb.onPresetInfoChanged(device, presets, status)); } @Override public void onSetPresetNameFailed(BluetoothDevice device, int status) { Attributable.setAttributionSource(device, mAttributionSource); forEach((cb) -> cb.onSetPresetNameFailed(device, status)); mCallbackWrapper.forEach((cb) -> cb.onSetPresetNameFailed(device, status)); } @Override public void onSetPresetNameForGroupFailed(int hapGroupId, int status) { forEach((cb) -> cb.onSetPresetNameForGroupFailed(hapGroupId, status)); mCallbackWrapper.forEach((cb) -> cb.onSetPresetNameForGroupFailed(hapGroupId, status)); } } }; /** * Intent used to broadcast the change in connection state of the Hearing Access Profile Client Loading Loading @@ -462,6 +449,24 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable mAttributionSource = mAdapter.getAttributionSource(); mService = null; Consumer<IBluetoothHapClient> registerConsumer = (IBluetoothHapClient service) -> { try { service.registerCallback(mCallback, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } }; Consumer<IBluetoothHapClient> unregisterConsumer = (IBluetoothHapClient service) -> { try { service.unregisterCallback(mCallback, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } }; mCallbackWrapper = new CallbackWrapper(registerConsumer, unregisterConsumer); mCloseGuard = new CloseGuard(); mCloseGuard.open("close"); } Loading @@ -486,18 +491,7 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable @Override public void onServiceConnected(IBinder service) { mService = IBluetoothHapClient.Stub.asInterface(service); // re-register the service-to-app callback synchronized (mCallbackExecutorMap) { if (mCallbackExecutorMap.isEmpty()) { return; } try { mService.registerCallback(mCallback, mAttributionSource); } catch (RemoteException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } } mCallbackWrapper.registerToNewService(mService); } /** @hide */ Loading Loading @@ -535,34 +529,7 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public void registerCallback( @NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) { requireNonNull(executor); requireNonNull(callback); synchronized (mCallbackExecutorMap) { // If the callback map is empty, we register the service-to-app callback if (mCallbackExecutorMap.isEmpty()) { if (!mAdapter.isEnabled()) { /* If Bluetooth is off, just store callback and it will be registered * when Bluetooth is on */ mCallbackExecutorMap.put(callback, executor); return; } try { final IBluetoothHapClient service = getService(); if (service != null) { service.registerCallback(mCallback, mAttributionSource); } } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } // Adds the passed in callback to our map of callbacks to executors if (mCallbackExecutorMap.containsKey(callback)) { throw new IllegalArgumentException("This callback has already been registered"); } mCallbackExecutorMap.put(callback, executor); } mCallbackWrapper.registerCallback(mService, callback, executor); } /** Loading @@ -582,25 +549,7 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable @RequiresBluetoothConnectPermission @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) public void unregisterCallback(@NonNull Callback callback) { requireNonNull(callback); synchronized (mCallbackExecutorMap) { if (mCallbackExecutorMap.remove(callback) == null) { throw new IllegalArgumentException("This callback has not been registered"); } // If the callback map is empty, we unregister the service-to-app callback if (mCallbackExecutorMap.isEmpty()) { try { final IBluetoothHapClient service = getService(); if (service != null) { service.unregisterCallback(mCallback, mAttributionSource); } } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } } mCallbackWrapper.unregisterCallback(mService, callback); } /** Loading
framework/java/android/bluetooth/CallbackWrapper.java 0 → 100644 +126 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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 android.bluetooth; import static java.util.Objects.requireNonNull; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import java.util.function.Consumer; /** * This class provide a common abstraction to deal with callback registration and broadcast from the * bluetooth to external app using their executor. It is designed to be thread safe. It register the * client the first time a callback is registered, and unregister when the last callback is * unregistered. It handles when the service to connect is not yet available and provide a method to * register the callback to the newly available service. @see #registerToNewService * * @param <S> the type of binder used to register to the bluetooth app (e.g: IBluetoothHapClient) * @param <T> the type of the callback (e.g: BluetoothHapClient.Callback) * @hide */ public class CallbackWrapper<T, S> { @GuardedBy("mCallbackExecutorMap") private final Map<T, Executor> mCallbackExecutorMap; private final Consumer<S> mRegisterConsumer; private final Consumer<S> mUnregisterConsumer; /** * @param registerConsumer is called the first time a callback is being registered or manually * via @see #registerToNewService * @param unregisterConsumer is called when the last callback is removed */ CallbackWrapper(Consumer<S> registerConsumer, Consumer<S> unregisterConsumer) { this(registerConsumer, unregisterConsumer, new HashMap()); } /** * @param map internal map injected for test purpose * @see #CallbackWrapper(Consumer, Consumer) */ @VisibleForTesting public CallbackWrapper( Consumer<S> registerConsumer, Consumer<S> unregisterConsumer, Map<T, Executor> map) { mRegisterConsumer = requireNonNull(registerConsumer); mUnregisterConsumer = requireNonNull(unregisterConsumer); mCallbackExecutorMap = requireNonNull(map); } /** Dispatch the callback from the Bluetooth service to all the currently registered callback */ public void forEach(Consumer<T> consumer) { synchronized (mCallbackExecutorMap) { mCallbackExecutorMap.forEach( (callback, executor) -> executor.execute(() -> consumer.accept(callback))); } } /** Register the callback and save the wrapper to the service if needed */ public void registerCallback( @Nullable S service, @NonNull T callback, @NonNull @CallbackExecutor Executor executor) { requireNonNull(callback); requireNonNull(executor); synchronized (mCallbackExecutorMap) { if (mCallbackExecutorMap.containsKey(callback)) { throw new IllegalArgumentException("Callback already registered"); } if (service != null && mCallbackExecutorMap.isEmpty()) { mRegisterConsumer.accept(service); } mCallbackExecutorMap.put(callback, executor); } } /** Register the callback and remove the wrapper to the service if needed */ public void unregisterCallback(@Nullable S service, @NonNull T callback) { requireNonNull(callback); synchronized (mCallbackExecutorMap) { if (!mCallbackExecutorMap.containsKey(callback)) { throw new IllegalArgumentException("Callback already unregistered"); } mCallbackExecutorMap.remove(callback); if (service != null && mCallbackExecutorMap.isEmpty()) { mUnregisterConsumer.accept(service); } } } /** A new Service is available from Bluetooth and callback need to be registered */ public void registerToNewService(@NonNull S service) { requireNonNull(service); synchronized (mCallbackExecutorMap) { if (!mCallbackExecutorMap.isEmpty()) { mRegisterConsumer.accept(service); } } } }
framework/tests/unit/src/android/bluetooth/CallbackWrapperTest.java 0 → 100644 +229 −0 Original line number Diff line number Diff line /* * Copyright 2024 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 android.bluetooth; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.os.Handler; import android.os.HandlerExecutor; import android.os.test.TestLooper; import androidx.test.filters.SmallTest; import com.google.common.truth.Expect; import org.junit.After; 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.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import java.util.function.Consumer; /** Test cases for {@link CallbackWrapper}. */ @SmallTest @RunWith(JUnit4.class) public class CallbackWrapperTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @Rule public Expect expect = Expect.create(); private final Consumer<int[]> mRegisterConsumer = (int[] counter) -> counter[0]++; private final Consumer<int[]> mUnregisterConsumer = (int[] counter) -> counter[0]--; private final int[] mUnusedCounter = {0}; private interface Callback { void onCallbackCalled(); } @Mock private Callback mCallback; @Mock private Callback mCallback2; private TestLooper mLooper; private Executor mExecutor; private Map<Callback, Executor> mCallbackExecutorMap; private CallbackWrapper<Callback, int[]> mCallbackWrapper; @Before public void setUp() { mLooper = new TestLooper(); mExecutor = new HandlerExecutor(new Handler(mLooper.getLooper())); mCallbackExecutorMap = new HashMap(); mCallbackWrapper = new CallbackWrapper(mRegisterConsumer, mUnregisterConsumer, mCallbackExecutorMap); } @After public void tearDown() { assertThat(mLooper.nextMessage()).isNull(); } @Test public void registerCallback_enforceValidParams() { assertThrows( NullPointerException.class, () -> mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, null)); assertThrows( NullPointerException.class, () -> mCallbackWrapper.registerCallback(mUnusedCounter, null, mExecutor)); // Service can be null, the following should not crash mCallbackWrapper.registerCallback(null, mCallback, mExecutor); assertThat(mCallbackExecutorMap).containsExactly(mCallback, mExecutor); } @Test public void unregisterCallback_enforceValidParams() { assertThrows( NullPointerException.class, () -> mCallbackWrapper.unregisterCallback(mUnusedCounter, null)); } @Test public void registerToNewService_enforceValidParams() { assertThrows(NullPointerException.class, () -> mCallbackWrapper.registerToNewService(null)); } @Test public void registerCallback_whenEmpty_callConsumer() { int[] counter = {0}; mCallbackWrapper.registerCallback(counter, mCallback, mExecutor); assertThat(counter[0]).isEqualTo(1); assertThat(mCallbackExecutorMap).containsExactly(mCallback, mExecutor); } @Test public void unregisterCallback_whenRegistered_callConsumer() { mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, mExecutor); int[] counter = {0}; mCallbackWrapper.unregisterCallback(counter, mCallback); assertThat(counter[0]).isEqualTo(-1); assertThat(mCallbackExecutorMap).isEmpty(); } @Test public void unregisterCallbackWithNoService_whenRegistered_stillRemovedFromMap() { mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, mExecutor); mCallbackWrapper.unregisterCallback(null, mCallback); assertThat(mCallbackExecutorMap).isEmpty(); } @Test public void registerCallback_whenWrapperAlreadyRegisteredToService_doNothing() { mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, mExecutor); int[] counter = {0}; mCallbackWrapper.registerCallback(counter, mCallback2, mExecutor); assertThat(counter[0]).isEqualTo(0); assertThat(mCallbackExecutorMap) .containsExactly(mCallback, mExecutor, mCallback2, mExecutor); } @Test public void unregisterCallback_whenMultiplesCallbackAreRegister_doNothing() { mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, mExecutor); mCallbackWrapper.registerCallback(mUnusedCounter, mCallback2, mExecutor); int[] counter = {0}; mCallbackWrapper.unregisterCallback(counter, mCallback2); assertThat(counter[0]).isEqualTo(0); assertThat(mCallbackExecutorMap).containsExactly(mCallback, mExecutor); } @Test public void registerCallback_whenCallbackAlreadyRegistered_throwException() { mCallbackWrapper.registerCallback(null, mCallback, mExecutor); assertThrows( IllegalArgumentException.class, () -> mCallbackWrapper.registerCallback(null, mCallback, mExecutor)); assertThat(mCallbackExecutorMap).containsExactly(mCallback, mExecutor); } @Test public void unregisterCallback_whenCallbackNotRegistered_throwException() { assertThrows( IllegalArgumentException.class, () -> mCallbackWrapper.unregisterCallback(null, mCallback)); assertThat(mCallbackExecutorMap).isEmpty(); } @Test public void registerToNewService_whenNoCallback_doNothing() { int[] counter = {0}; mCallbackWrapper.registerToNewService(counter); assertThat(counter[0]).isEqualTo(0); } @Test public void registerToNewService_whenCallback_callConsumer() { mCallbackWrapper.registerCallback(null, mCallback, mExecutor); int[] counter = {0}; mCallbackWrapper.registerToNewService(counter); assertThat(counter[0]).isEqualTo(1); } @Test public void triggerCallback_whenRegistered_dispatch() { mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, mExecutor); mCallbackWrapper.registerCallback(mUnusedCounter, mCallback2, mExecutor); mCallbackWrapper.forEach((cb) -> cb.onCallbackCalled()); assertThat(mLooper.dispatchAll()).isEqualTo(2); verify(mCallback).onCallbackCalled(); verify(mCallback2).onCallbackCalled(); } @Test public void triggerCallback_whenRegistered_dispatchOnlyOnCurrentlyRegistered() { mCallbackWrapper.registerCallback(mUnusedCounter, mCallback, mExecutor); mCallbackWrapper.registerCallback(mUnusedCounter, mCallback2, mExecutor); mCallbackWrapper.unregisterCallback(mUnusedCounter, mCallback2); mCallbackWrapper.forEach((cb) -> cb.onCallbackCalled()); assertThat(mLooper.dispatchAll()).isEqualTo(1); verify(mCallback).onCallbackCalled(); verify(mCallback2, never()).onCallbackCalled(); } }