blob: 89db88621d00d43e5aba08d97b8f810ada2e0a08 [file] [log] [blame]
/*
* 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);
}
}
}
}