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

Commit f2f794be authored by Treehugger Robot's avatar Treehugger Robot Committed by Gerrit Code Review
Browse files

Merge "Framework Introduce CallbackWrapper" into main

parents 78af57b4 c150a69f
Loading
Loading
Loading
Loading
+61 −112
Original line number Diff line number Diff line
@@ -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;

@@ -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;

    /**
@@ -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
@@ -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");
    }
@@ -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 */
@@ -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);
    }

    /**
@@ -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);
    }

    /**
+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);
            }
        }
    }
}
+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();
    }
}