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

Commit c150a69f authored by William Escande's avatar William Escande
Browse files

Framework Introduce CallbackWrapper

there is a bunch of profiles that register callback to the bluetooth
service, this logic is repeated again and again.

By having a common wrapper we can:
* Finnaly have some tests on the registration logic (none before)
* Make sure all the profile are registering callback the same ways
* remove duplicates code

This initial CL only implement it for HAP, other profiles will be done
in follow up

Bug: 311772251
Flag: Exempt no-op refactor
Test: atest FrameworkBluetoothTests
Change-Id: I61f3fdfb82efd26435ce8d9c74509708aca58b0d
parent e2c3c831
Loading
Loading
Loading
Loading
+61 −112
Original line number Original line Diff line number Diff line
@@ -39,14 +39,11 @@ import android.util.CloseGuard;
import android.util.Log;
import android.util.Log;


import com.android.bluetooth.flags.Flags;
import com.android.bluetooth.flags.Flags;
import com.android.internal.annotations.GuardedBy;


import java.lang.annotation.Retention;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Consumer;


@@ -62,9 +59,6 @@ import java.util.function.Consumer;
public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable {
public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable {
    private static final String TAG = BluetoothHapClient.class.getSimpleName();
    private static final String TAG = BluetoothHapClient.class.getSimpleName();


    @GuardedBy("mCallbackExecutorMap")
    private final Map<Callback, Executor> mCallbackExecutorMap = new HashMap<>();

    private CloseGuard mCloseGuard;
    private CloseGuard mCloseGuard;


    /**
    /**
@@ -237,53 +231,46 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable
                int hapGroupId, @GroupPresetNameChangeFailureReason int reason);
                int hapGroupId, @GroupPresetNameChangeFailureReason int reason);
    }
    }


    private final IBluetoothHapClientCallback mCallback =
    private final CallbackWrapper<Callback, IBluetoothHapClient> mCallbackWrapper;
            new IBluetoothHapClientCallback.Stub() {

                private void forEach(Consumer<BluetoothHapClient.Callback> consumer) {
    private final IBluetoothHapClientCallback mCallback = new HapClientNotifyCallback();
                    synchronized (mCallbackExecutorMap) {
                        mCallbackExecutorMap.forEach(
                                (callback, executor) ->
                                        executor.execute(() -> consumer.accept(callback)));
                    }
                }


    private class HapClientNotifyCallback extends IBluetoothHapClientCallback.Stub {
        @Override
        @Override
        public void onPresetSelected(BluetoothDevice device, int presetIndex, int reason) {
        public void onPresetSelected(BluetoothDevice device, int presetIndex, int reason) {
            Attributable.setAttributionSource(device, mAttributionSource);
            Attributable.setAttributionSource(device, mAttributionSource);
                    forEach((cb) -> cb.onPresetSelected(device, presetIndex, reason));
            mCallbackWrapper.forEach((cb) -> cb.onPresetSelected(device, presetIndex, reason));
        }
        }


        @Override
        @Override
        public void onPresetSelectionFailed(BluetoothDevice device, int status) {
        public void onPresetSelectionFailed(BluetoothDevice device, int status) {
            Attributable.setAttributionSource(device, mAttributionSource);
            Attributable.setAttributionSource(device, mAttributionSource);
                    forEach((cb) -> cb.onPresetSelectionFailed(device, status));
            mCallbackWrapper.forEach((cb) -> cb.onPresetSelectionFailed(device, status));
        }
        }


        @Override
        @Override
                public void onPresetSelectionForGroupFailed(int hapGroupId, int status) {
        public void onPresetSelectionForGroupFailed(int groupId, int status) {
                    forEach((cb) -> cb.onPresetSelectionForGroupFailed(hapGroupId, status));
            mCallbackWrapper.forEach((cb) -> cb.onPresetSelectionForGroupFailed(groupId, status));
        }
        }


        @Override
        @Override
        public void onPresetInfoChanged(
        public void onPresetInfoChanged(
                        BluetoothDevice device,
                BluetoothDevice device, List<BluetoothHapPresetInfo> presets, int status) {
                        List<BluetoothHapPresetInfo> presetInfoList,
                        int status) {
            Attributable.setAttributionSource(device, mAttributionSource);
            Attributable.setAttributionSource(device, mAttributionSource);
                    forEach((cb) -> cb.onPresetInfoChanged(device, presetInfoList, status));
            mCallbackWrapper.forEach((cb) -> cb.onPresetInfoChanged(device, presets, status));
        }
        }


        @Override
        @Override
        public void onSetPresetNameFailed(BluetoothDevice device, int status) {
        public void onSetPresetNameFailed(BluetoothDevice device, int status) {
            Attributable.setAttributionSource(device, mAttributionSource);
            Attributable.setAttributionSource(device, mAttributionSource);
                    forEach((cb) -> cb.onSetPresetNameFailed(device, status));
            mCallbackWrapper.forEach((cb) -> cb.onSetPresetNameFailed(device, status));
        }
        }


        @Override
        @Override
        public void onSetPresetNameForGroupFailed(int hapGroupId, int status) {
        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
     * Intent used to broadcast the change in connection state of the Hearing Access Profile Client
@@ -462,6 +449,24 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable
        mAttributionSource = mAdapter.getAttributionSource();
        mAttributionSource = mAdapter.getAttributionSource();
        mService = null;
        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 = new CloseGuard();
        mCloseGuard.open("close");
        mCloseGuard.open("close");
    }
    }
@@ -486,18 +491,7 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable
    @Override
    @Override
    public void onServiceConnected(IBinder service) {
    public void onServiceConnected(IBinder service) {
        mService = IBluetoothHapClient.Stub.asInterface(service);
        mService = IBluetoothHapClient.Stub.asInterface(service);
        // re-register the service-to-app callback
        mCallbackWrapper.registerToNewService(mService);
        synchronized (mCallbackExecutorMap) {
            if (mCallbackExecutorMap.isEmpty()) {
                return;
            }

            try {
                mService.registerCallback(mCallback, mAttributionSource);
            } catch (RemoteException e) {
                Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
            }
        }
    }
    }


    /** @hide */
    /** @hide */
@@ -535,34 +529,7 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public void registerCallback(
    public void registerCallback(
            @NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) {
            @NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) {
        requireNonNull(executor);
        mCallbackWrapper.registerCallback(mService, callback, 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);
        }
    }
    }


    /**
    /**
@@ -582,25 +549,7 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable
    @RequiresBluetoothConnectPermission
    @RequiresBluetoothConnectPermission
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
    public void unregisterCallback(@NonNull Callback callback) {
    public void unregisterCallback(@NonNull Callback callback) {
        requireNonNull(callback);
        mCallbackWrapper.unregisterCallback(mService, 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();
                }
            }
        }
    }
    }


    /**
    /**
+126 −0
Original line number Original line 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);
            }
        }
    }
}
+229 −0
Original line number Original line 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();
    }
}