| /* | |
| * 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 android.Manifest.permission.BLUETOOTH_CONNECT; | |
| import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; | |
| import static android.bluetooth.BluetoothUtils.executeFromBinder; | |
| import static java.util.Objects.requireNonNull; | |
| import android.annotation.CallbackExecutor; | |
| import android.annotation.NonNull; | |
| import android.annotation.Nullable; | |
| import android.annotation.RequiresPermission; | |
| import android.annotation.SuppressLint; | |
| 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) -> | |
| executeFromBinder(executor, () -> consumer.accept(callback))); | |
| } | |
| } | |
| /** Register the callback and save the wrapper to the service if needed */ | |
| @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) | |
| @SuppressLint("AndroidFrameworkRequiresPermission") // Consumer wrongly report permission | |
| 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 */ | |
| @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) | |
| @SuppressLint("AndroidFrameworkRequiresPermission") // Consumer wrongly report permission | |
| 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); | |
| } | |
| } | |
| } | |
| } |