summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp1
-rw-r--r--android/app/aidl/Android.bp1
-rw-r--r--android/app/aidl/android/bluetooth/IBluetooth.aidl8
-rw-r--r--android/app/aidl/android/bluetooth/IBluetoothHciVendorSpecificCallback.aidl26
-rw-r--r--android/app/jni/com_android_bluetooth.h2
-rw-r--r--android/app/jni/com_android_bluetooth_BluetoothHciVendorSpecific.cpp219
-rw-r--r--android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp6
-rw-r--r--android/app/src/com/android/bluetooth/btservice/AdapterService.java91
-rw-r--r--android/app/src/com/android/bluetooth/btservice/BluetoothHciVendorSpecificDispatcher.java129
-rw-r--r--android/app/src/com/android/bluetooth/btservice/BluetoothHciVendorSpecificNativeInterface.java63
-rw-r--r--framework/api/system-current.txt9
-rw-r--r--framework/java/android/bluetooth/BluetoothAdapter.java283
-rw-r--r--system/btif/Android.bp1
-rw-r--r--system/btif/BUILD.gn1
-rw-r--r--system/btif/include/btif_hci_vs.h32
-rw-r--r--system/btif/src/btif_hci_vs.cc61
-rw-r--r--system/include/hardware/bt_hci_vs.h48
17 files changed, 981 insertions, 0 deletions
diff --git a/Android.bp b/Android.bp
index 476d018736..fde5137bba 100644
--- a/Android.bp
+++ b/Android.bp
@@ -154,6 +154,7 @@ java_defaults {
"-Xep:OperatorPrecedence:ERROR",
"-Xep:ReferenceEquality:ERROR",
"-Xep:ReturnAtTheEndOfVoidFunction:ERROR",
+ "-Xep:ReturnFromVoid:ERROR",
"-Xep:StaticAssignmentInConstructor:ERROR",
"-Xep:StaticGuardedByInstance:ERROR",
"-Xep:StringCaseLocaleUsage:ERROR",
diff --git a/android/app/aidl/Android.bp b/android/app/aidl/Android.bp
index 37678c2917..03f2d109ce 100644
--- a/android/app/aidl/Android.bp
+++ b/android/app/aidl/Android.bp
@@ -27,6 +27,7 @@ filegroup {
"android/bluetooth/IBluetoothGattServerCallback.aidl",
"android/bluetooth/IBluetoothHapClient.aidl",
"android/bluetooth/IBluetoothHapClientCallback.aidl",
+ "android/bluetooth/IBluetoothHciVendorSpecificCallback.aidl",
"android/bluetooth/IBluetoothHeadset.aidl",
"android/bluetooth/IBluetoothHeadsetClient.aidl",
"android/bluetooth/IBluetoothHearingAid.aidl",
diff --git a/android/app/aidl/android/bluetooth/IBluetooth.aidl b/android/app/aidl/android/bluetooth/IBluetooth.aidl
index 28cb07db6e..83cbe469ae 100644
--- a/android/app/aidl/android/bluetooth/IBluetooth.aidl
+++ b/android/app/aidl/android/bluetooth/IBluetooth.aidl
@@ -19,6 +19,7 @@ package android.bluetooth;
import android.app.PendingIntent;
import android.bluetooth.IBluetoothActivityEnergyInfoListener;
import android.bluetooth.IBluetoothGatt;
+import android.bluetooth.IBluetoothHciVendorSpecificCallback;
import android.bluetooth.IBluetoothPreferredAudioProfilesCallback;
import android.bluetooth.IBluetoothQualityReportReadyCallback;
import android.bluetooth.IBluetoothCallback;
@@ -293,6 +294,13 @@ interface IBluetooth
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})")
int unregisterBluetoothQualityReportReadyCallback(in IBluetoothQualityReportReadyCallback callback, in AttributionSource attributionSource);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+ void registerHciVendorSpecificCallback(in IBluetoothHciVendorSpecificCallback callback, in int[] eventCodes);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+ void unregisterHciVendorSpecificCallback(in IBluetoothHciVendorSpecificCallback callback);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
+ void sendHciVendorSpecificCommand(in int ocf, in byte[] parameters, in IBluetoothHciVendorSpecificCallback callback);
+
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN,android.Manifest.permission.BLUETOOTH_PRIVILEGED})")
int getOffloadedTransportDiscoveryDataScanSupported(in AttributionSource attributionSource);
diff --git a/android/app/aidl/android/bluetooth/IBluetoothHciVendorSpecificCallback.aidl b/android/app/aidl/android/bluetooth/IBluetoothHciVendorSpecificCallback.aidl
new file mode 100644
index 0000000000..2aac21b158
--- /dev/null
+++ b/android/app/aidl/android/bluetooth/IBluetoothHciVendorSpecificCallback.aidl
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+/**
+ * Callback definitions for HCI Vendor Specific Callback.
+ * @hide
+ */
+oneway interface IBluetoothHciVendorSpecificCallback {
+ void onCommandStatus(int ocf, in int status);
+ void onCommandComplete(int ocf, in byte[] returnParameters);
+ void onEvent(int code, in byte[] data);
+}
diff --git a/android/app/jni/com_android_bluetooth.h b/android/app/jni/com_android_bluetooth.h
index c473b50ac6..d83e6a9f74 100644
--- a/android/app/jni/com_android_bluetooth.h
+++ b/android/app/jni/com_android_bluetooth.h
@@ -162,6 +162,8 @@ int register_com_android_bluetooth_csip_set_coordinator(JNIEnv* env);
int register_com_android_bluetooth_btservice_BluetoothQualityReport(JNIEnv* env);
+int register_com_android_bluetooth_btservice_BluetoothHciVendorSpecific(JNIEnv* env);
+
struct JNIJavaMethod {
const char* name;
const char* signature;
diff --git a/android/app/jni/com_android_bluetooth_BluetoothHciVendorSpecific.cpp b/android/app/jni/com_android_bluetooth_BluetoothHciVendorSpecific.cpp
new file mode 100644
index 0000000000..96c15300af
--- /dev/null
+++ b/android/app/jni/com_android_bluetooth_BluetoothHciVendorSpecific.cpp
@@ -0,0 +1,219 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "BluetoothHciVendorSpecificJni"
+
+#include <shared_mutex>
+
+#include "btif/include/btif_hci_vs.h"
+#include "com_android_bluetooth.h"
+#include "hardware/bt_hci_vs.h"
+
+using bluetooth::hci_vs::BluetoothHciVendorSpecificInterface;
+using bluetooth::hci_vs::Cookie;
+
+namespace android {
+
+static std::shared_timed_mutex interface_mutex;
+static std::shared_timed_mutex callbacks_mutex;
+
+static jmethodID method_onCommandStatus;
+static jmethodID method_onCommandComplete;
+static jmethodID method_onEvent;
+static jobject mCallbacksObj = nullptr;
+
+class BluetoothHciVendorSpecificCallbacksImpl
+ : public bluetooth::hci_vs::BluetoothHciVendorSpecificCallbacks {
+public:
+ ~BluetoothHciVendorSpecificCallbacksImpl() = default;
+
+ void onCommandStatus(uint16_t ocf, uint8_t status, Cookie cookie) override {
+ log::info("");
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+
+ CallbackEnv callbackEnv(__func__);
+ if (!callbackEnv.valid() || mCallbacksObj == nullptr) {
+ return;
+ }
+
+ auto j_cookie = toJByteArray(callbackEnv.get(), cookie);
+ if (!j_cookie.get()) {
+ log::error("Error while allocating byte array for cookie");
+ return;
+ }
+
+ callbackEnv->CallVoidMethod(mCallbacksObj, method_onCommandStatus, (jint)ocf, (jint)status,
+ j_cookie.get());
+ }
+
+ void onCommandComplete(uint16_t ocf, std::vector<uint8_t> return_parameters,
+ Cookie cookie) override {
+ log::info("");
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+
+ CallbackEnv callbackEnv(__func__);
+ if (!callbackEnv.valid() || mCallbacksObj == nullptr) {
+ return;
+ }
+
+ auto j_return_parameters = toJByteArray(callbackEnv.get(), return_parameters);
+ auto j_cookie = toJByteArray(callbackEnv.get(), cookie);
+ if (!j_return_parameters.get() || !j_cookie.get()) {
+ log::error("Error while allocating byte array for return parameters or cookie");
+ return;
+ }
+
+ callbackEnv->CallVoidMethod(mCallbacksObj, method_onCommandComplete, (jint)ocf,
+ j_return_parameters.get(), j_cookie.get());
+ }
+
+ void onEvent(uint8_t code, std::vector<uint8_t> data) override {
+ log::info("");
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+
+ CallbackEnv callbackEnv(__func__);
+ if (!callbackEnv.valid() || mCallbacksObj == nullptr) {
+ return;
+ }
+
+ auto j_data = toJByteArray(callbackEnv.get(), data);
+ if (!j_data.get()) {
+ log::error("Error while allocating byte array for event data");
+ return;
+ }
+
+ callbackEnv->CallVoidMethod(mCallbacksObj, method_onEvent, (jint)code, j_data.get());
+ }
+
+private:
+ template <typename T>
+ static ScopedLocalRef<jbyteArray> toJByteArray(JNIEnv* env, const T& src) {
+ ScopedLocalRef<jbyteArray> dst(env, env->NewByteArray(src.size()));
+ if (dst.get()) {
+ env->SetByteArrayRegion(dst.get(), 0, src.size(), reinterpret_cast<const jbyte*>(src.data()));
+ }
+ return dst;
+ }
+};
+
+static BluetoothHciVendorSpecificInterface* sBluetoothHciVendorSpecificInterface = nullptr;
+static BluetoothHciVendorSpecificCallbacksImpl sBluetoothHciVendorSpecificCallbacks;
+
+static void initNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_timed_mutex> interface_lock(interface_mutex);
+ std::unique_lock<std::shared_timed_mutex> callbacks_lock(callbacks_mutex);
+
+ if (sBluetoothHciVendorSpecificInterface != nullptr) {
+ log::info("Cleaning up BluetoothHciVendorSpecific Interface before initializing...");
+ sBluetoothHciVendorSpecificInterface = nullptr;
+ }
+
+ if (mCallbacksObj != nullptr) {
+ log::info("Cleaning up BluetoothHciVendorSpecific callback object");
+ env->DeleteGlobalRef(mCallbacksObj);
+ mCallbacksObj = nullptr;
+ }
+
+ if ((mCallbacksObj = env->NewGlobalRef(object)) == nullptr) {
+ log::error("Failed to allocate Global Ref for BluetoothHciVendorSpecific Callbacks");
+ return;
+ }
+
+ sBluetoothHciVendorSpecificInterface =
+ bluetooth::hci_vs::getBluetoothHciVendorSpecificInterface();
+ if (sBluetoothHciVendorSpecificInterface == nullptr) {
+ log::error("Failed to get BluetoothHciVendorSpecific Interface");
+ return;
+ }
+
+ sBluetoothHciVendorSpecificInterface->init(&sBluetoothHciVendorSpecificCallbacks);
+}
+
+static void cleanupNative(JNIEnv* env, jobject /* object */) {
+ std::unique_lock<std::shared_timed_mutex> interface_lock(interface_mutex);
+ std::unique_lock<std::shared_timed_mutex> callbacks_lock(callbacks_mutex);
+
+ if (sBluetoothHciVendorSpecificInterface != nullptr) {
+ sBluetoothHciVendorSpecificInterface = nullptr;
+ }
+
+ if (mCallbacksObj != nullptr) {
+ env->DeleteGlobalRef(mCallbacksObj);
+ mCallbacksObj = nullptr;
+ }
+}
+
+static void sendCommandNative(JNIEnv* env, jobject /* obj */, jint ocf, jbyteArray parametersArray,
+ jbyteArray uuidArray) {
+ std::unique_lock<std::shared_timed_mutex> interface_lock(interface_mutex);
+ log::verbose("");
+ if (!sBluetoothHciVendorSpecificInterface) {
+ return;
+ }
+
+ jbyte* pParameters = env->GetByteArrayElements(parametersArray, NULL);
+ jbyte* pUuid = env->GetByteArrayElements(uuidArray, NULL);
+ if (pParameters && pUuid && env->GetArrayLength(uuidArray) == 16) {
+ std::vector<uint8_t> parameters(
+ reinterpret_cast<uint8_t*>(pParameters),
+ reinterpret_cast<uint8_t*>(pParameters + env->GetArrayLength(parametersArray)));
+
+ Cookie cookie;
+ std::memcpy(cookie.data(), reinterpret_cast<const uint8_t*>(pUuid), 16);
+
+ if (!sBluetoothHciVendorSpecificInterface) {
+ return;
+ }
+ sBluetoothHciVendorSpecificInterface->sendCommand(ocf, parameters, cookie);
+
+ } else {
+ jniThrowIOException(env, EINVAL);
+ }
+
+ if (pParameters) {
+ env->ReleaseByteArrayElements(parametersArray, pParameters, 0);
+ }
+
+ if (pUuid) {
+ env->ReleaseByteArrayElements(uuidArray, pUuid, 0);
+ }
+}
+
+int register_com_android_bluetooth_btservice_BluetoothHciVendorSpecific(JNIEnv* env) {
+ const JNINativeMethod methods[] = {
+ {"initNative", "()V", reinterpret_cast<void*>(initNative)},
+ {"cleanupNative", "()V", reinterpret_cast<void*>(cleanupNative)},
+ {"sendCommandNative", "(I[B[B)V", reinterpret_cast<void*>(sendCommandNative)},
+ };
+ const int result = REGISTER_NATIVE_METHODS(
+ env, "com/android/bluetooth/btservice/BluetoothHciVendorSpecificNativeInterface",
+ methods);
+ if (result != 0) {
+ return result;
+ }
+
+ const JNIJavaMethod javaMethods[] = {
+ {"onCommandStatus", "(II[B)V", &method_onCommandStatus},
+ {"onCommandComplete", "(I[B[B)V", &method_onCommandComplete},
+ {"onEvent", "(I[B)V", &method_onEvent},
+ };
+ GET_JAVA_METHODS(env, "com/android/bluetooth/btservice/BluetoothHciVendorSpecificNativeInterface",
+ javaMethods);
+
+ return 0;
+}
+
+} // namespace android
diff --git a/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp b/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp
index 8e2f4e201d..da4b61e146 100644
--- a/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp
+++ b/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp
@@ -2509,6 +2509,12 @@ jint JNI_OnLoad(JavaVM* jvm, void* /* reserved */) {
return JNI_ERR;
}
+ status = android::register_com_android_bluetooth_btservice_BluetoothHciVendorSpecific(e);
+ if (status < 0) {
+ log::error("jni bluetooth hci vendor-specific registration failure: {}", status);
+ return JNI_ERR;
+ }
+
return JNI_VERSION_1_6;
}
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
index d1feef50d5..695ad6e138 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
@@ -73,6 +73,7 @@ import android.bluetooth.IBluetooth;
import android.bluetooth.IBluetoothActivityEnergyInfoListener;
import android.bluetooth.IBluetoothCallback;
import android.bluetooth.IBluetoothConnectionCallback;
+import android.bluetooth.IBluetoothHciVendorSpecificCallback;
import android.bluetooth.IBluetoothMetadataListener;
import android.bluetooth.IBluetoothOobDataCallback;
import android.bluetooth.IBluetoothPreferredAudioProfilesCallback;
@@ -188,6 +189,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -273,6 +275,13 @@ public class AdapterService extends Service {
private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();
+ private final BluetoothHciVendorSpecificDispatcher mBluetoothHciVendorSpecificDispatcher =
+ new BluetoothHciVendorSpecificDispatcher();
+ private final BluetoothHciVendorSpecificNativeInterface
+ mBluetoothHciVendorSpecificNativeInterface =
+ new BluetoothHciVendorSpecificNativeInterface(
+ mBluetoothHciVendorSpecificDispatcher);
+
private final Looper mLooper;
private final AdapterServiceHandler mHandler;
@@ -685,6 +694,10 @@ public class AdapterService extends Service {
"BluetoothQualityReportNativeInterface cannot be null when BQR starts");
mBluetoothQualityReportNativeInterface.init();
+ if (Flags.hciVendorSpecificExtension()) {
+ mBluetoothHciVendorSpecificNativeInterface.init();
+ }
+
mSdpManager = new SdpManager(this, mLooper);
mDatabaseManager.start(MetadataDatabase.createDatabase(this));
@@ -4135,6 +4148,84 @@ public class AdapterService extends Service {
}
@Override
+ public void registerHciVendorSpecificCallback(
+ IBluetoothHciVendorSpecificCallback callback, int[] eventCodes) {
+ AdapterService service = getService();
+ if (service == null) {
+ return;
+ }
+ if (!callerIsSystemOrActiveOrManagedUser(
+ service, TAG, "registerHciVendorSpecificCallback")) {
+ throw new SecurityException("not allowed");
+ }
+ service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null);
+ requireNonNull(callback);
+ requireNonNull(eventCodes);
+
+ Set<Integer> eventCodesSet =
+ Arrays.stream(eventCodes).boxed().collect(Collectors.toSet());
+ if (eventCodesSet.stream()
+ .anyMatch((n) -> (n < 0) || (n >= 0x50 && n < 0x60) || (n > 0xff))) {
+ throw new IllegalArgumentException("invalid vendor-specific event code");
+ }
+
+ service.mBluetoothHciVendorSpecificDispatcher.register(callback, eventCodesSet);
+ }
+
+ @Override
+ public void unregisterHciVendorSpecificCallback(
+ IBluetoothHciVendorSpecificCallback callback) {
+ AdapterService service = getService();
+ if (service == null) {
+ return;
+ }
+ if (!callerIsSystemOrActiveOrManagedUser(
+ service, TAG, "unregisterHciVendorSpecificCallback")) {
+ throw new SecurityException("not allowed");
+ }
+ service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null);
+ requireNonNull(callback);
+
+ service.mBluetoothHciVendorSpecificDispatcher.unregister(callback);
+ }
+
+ @Override
+ public void sendHciVendorSpecificCommand(
+ int ocf, byte[] parameters, IBluetoothHciVendorSpecificCallback callback) {
+ AdapterService service = getService();
+ if (service == null) {
+ return;
+ }
+ if (!callerIsSystemOrActiveOrManagedUser(
+ service, TAG, "sendHciVendorSpecificCommand")) {
+ throw new SecurityException("not allowed");
+ }
+ service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null);
+
+ // Open this no-op android command for test purpose
+ int getVendorCapabilitiesOcf = 0x153;
+ if (ocf < 0
+ || (ocf >= 0x150 && ocf < 0x160 && ocf != getVendorCapabilitiesOcf)
+ || (ocf > 0x3ff)) {
+ throw new IllegalArgumentException("invalid vendor-specific event code");
+ }
+ requireNonNull(parameters);
+ if (parameters.length > 255) {
+ throw new IllegalArgumentException("Parameters size is too big");
+ }
+
+ Optional<byte[]> cookie =
+ service.mBluetoothHciVendorSpecificDispatcher.getRegisteredCookie(callback);
+ if (!cookie.isPresent()) {
+ Log.e(TAG, "send command without registered callback");
+ throw new IllegalStateException("callback not registered");
+ }
+
+ service.mBluetoothHciVendorSpecificNativeInterface.sendCommand(
+ ocf, parameters, cookie.get());
+ }
+
+ @Override
public int getOffloadedTransportDiscoveryDataScanSupported(AttributionSource source) {
AdapterService service = getService();
if (service == null
diff --git a/android/app/src/com/android/bluetooth/btservice/BluetoothHciVendorSpecificDispatcher.java b/android/app/src/com/android/bluetooth/btservice/BluetoothHciVendorSpecificDispatcher.java
new file mode 100644
index 0000000000..fe40294d20
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/btservice/BluetoothHciVendorSpecificDispatcher.java
@@ -0,0 +1,129 @@
+/*
+ * 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 com.android.bluetooth.btservice;
+
+import android.bluetooth.IBluetoothHciVendorSpecificCallback;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.bluetooth.Utils;
+import com.android.internal.annotations.GuardedBy;
+
+import java.nio.ByteBuffer;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+class BluetoothHciVendorSpecificDispatcher {
+ private static final String TAG = "BluetoothHciVendorSpecificDispatcher";
+
+ private final class Registration implements IBinder.DeathRecipient {
+ final IBluetoothHciVendorSpecificCallback mCallback;
+ final Set<Integer> mEventCodes;
+ final UUID mUuid;
+
+ Registration(IBluetoothHciVendorSpecificCallback callback, Set<Integer> eventCodes) {
+ mCallback = callback;
+ mEventCodes = eventCodes;
+ mUuid = UUID.randomUUID();
+ }
+
+ public void binderDied() {
+ synchronized (mRegistrations) {
+ mRegistrations.remove(mCallback.asBinder());
+ }
+ }
+ }
+
+ @GuardedBy("mRegistrations")
+ private final ArrayMap<IBinder, Registration> mRegistrations = new ArrayMap<>();
+
+ void unregister(IBluetoothHciVendorSpecificCallback callback) {
+ IBinder binder = callback.asBinder();
+ synchronized (mRegistrations) {
+ Registration registration = mRegistrations.remove(binder);
+ if (registration == null) {
+ throw new IllegalStateException("callback was never registered");
+ }
+
+ binder.unlinkToDeath(registration, 0);
+ }
+ }
+
+ void register(IBluetoothHciVendorSpecificCallback callback, Set<Integer> eventCodes) {
+ IBinder binder = callback.asBinder();
+ synchronized (mRegistrations) {
+ if (mRegistrations.containsKey(binder)) {
+ throw new IllegalStateException("callback already registered");
+ }
+
+ try {
+ Registration registration = new Registration(callback, eventCodes);
+ binder.linkToDeath(registration, 0);
+ mRegistrations.put(binder, registration);
+ } catch (RemoteException e) {
+ Log.e(TAG, "link to death", e);
+ }
+ }
+ }
+
+ Optional<byte[]> getRegisteredCookie(IBluetoothHciVendorSpecificCallback callback) {
+ IBinder binder = callback.asBinder();
+ synchronized (mRegistrations) {
+ if (!mRegistrations.containsKey(binder)) return Optional.empty();
+
+ Registration registration = mRegistrations.get(binder);
+ ByteBuffer cookieBb = ByteBuffer.allocate(16);
+ cookieBb.putLong(registration.mUuid.getMostSignificantBits());
+ cookieBb.putLong(registration.mUuid.getLeastSignificantBits());
+ return Optional.of(cookieBb.array());
+ }
+ }
+
+ void dispatchCommandStatusOrComplete(
+ byte[] cookie,
+ Utils.RemoteExceptionIgnoringConsumer<IBluetoothHciVendorSpecificCallback> action) {
+ ByteBuffer cookieBb = ByteBuffer.wrap(cookie);
+ UUID uuid = new UUID(cookieBb.getLong(), cookieBb.getLong());
+ synchronized (mRegistrations) {
+ try {
+ Registration registration =
+ mRegistrations.values().stream()
+ .filter((r) -> r.mUuid.equals(uuid))
+ .findAny()
+ .get();
+ action.accept(registration.mCallback);
+ } catch (NoSuchElementException e) {
+ Log.e(TAG, "Command status or complete owner not registered");
+ return;
+ }
+ }
+ }
+
+ void broadcastEvent(
+ int eventCode,
+ Utils.RemoteExceptionIgnoringConsumer<IBluetoothHciVendorSpecificCallback> action) {
+ synchronized (mRegistrations) {
+ mRegistrations.values().stream()
+ .filter((r) -> r.mEventCodes.contains(eventCode))
+ .forEach((r) -> action.accept(r.mCallback));
+ }
+ }
+}
diff --git a/android/app/src/com/android/bluetooth/btservice/BluetoothHciVendorSpecificNativeInterface.java b/android/app/src/com/android/bluetooth/btservice/BluetoothHciVendorSpecificNativeInterface.java
new file mode 100644
index 0000000000..7acf26f360
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/btservice/BluetoothHciVendorSpecificNativeInterface.java
@@ -0,0 +1,63 @@
+/*
+ * 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 com.android.bluetooth.btservice;
+
+class BluetoothHciVendorSpecificNativeInterface {
+ private static final String TAG = "BluetoothHciVendorSpecificNativeInterface";
+ private final BluetoothHciVendorSpecificDispatcher mDispatcher;
+
+ public BluetoothHciVendorSpecificNativeInterface(
+ BluetoothHciVendorSpecificDispatcher dispatcher) {
+ mDispatcher = dispatcher;
+ }
+
+ void init() {
+ initNative();
+ }
+
+ void cleanup() {
+ cleanupNative();
+ }
+
+ void sendCommand(int ocf, byte[] parameters, byte[] cookie) {
+ sendCommandNative(ocf, parameters, cookie);
+ }
+
+ // Callbacks from the native stack
+
+ private void onCommandStatus(int ocf, int status, byte[] cookie) {
+ mDispatcher.dispatchCommandStatusOrComplete(
+ cookie, (cb) -> cb.onCommandStatus(ocf, status));
+ }
+
+ private void onCommandComplete(int ocf, byte[] returnParameters, byte[] cookie) {
+ mDispatcher.dispatchCommandStatusOrComplete(
+ cookie, (cb) -> cb.onCommandComplete(ocf, returnParameters));
+ }
+
+ private void onEvent(int code, byte[] data) {
+ mDispatcher.broadcastEvent(code, (cb) -> cb.onEvent(code, data));
+ }
+
+ // Native methods that call into the JNI interface
+
+ private native void initNative();
+
+ private native void cleanupNative();
+
+ private native void sendCommandNative(int ocf, byte[] parameters, byte[] cookie);
+}
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index cd02d1805e..4934454873 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -83,12 +83,14 @@ package android.bluetooth {
method @NonNull public static String nameForState(int);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int notifyActiveDeviceChangeApplied(@NonNull android.bluetooth.BluetoothDevice);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public boolean registerBluetoothConnectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.bluetooth.BluetoothAdapter.BluetoothConnectionCallback);
+ method @FlaggedApi("com.android.bluetooth.flags.hci_vendor_specific_extension") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void registerBluetoothHciVendorSpecificCallback(@NonNull java.util.Set<java.lang.Integer>, @NonNull java.util.concurrent.Executor, @NonNull android.bluetooth.BluetoothAdapter.BluetoothHciVendorSpecificCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int registerBluetoothQualityReportReadyCallback(@NonNull java.util.concurrent.Executor, @NonNull android.bluetooth.BluetoothAdapter.BluetoothQualityReportReadyCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int registerPreferredAudioProfilesChangedCallback(@NonNull java.util.concurrent.Executor, @NonNull android.bluetooth.BluetoothAdapter.PreferredAudioProfilesChangedCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, android.Manifest.permission.MODIFY_PHONE_STATE}) public boolean removeActiveDevice(int);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public boolean removeOnMetadataChangedListener(@NonNull android.bluetooth.BluetoothDevice, @NonNull android.bluetooth.BluetoothAdapter.OnMetadataChangedListener);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void requestControllerActivityEnergyInfo(@NonNull java.util.concurrent.Executor, @NonNull android.bluetooth.BluetoothAdapter.OnBluetoothActivityEnergyInfoCallback);
method @NonNull @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public android.bluetooth.BluetoothSocket retrieveConnectedRfcommSocket(@NonNull java.util.UUID);
+ method @FlaggedApi("com.android.bluetooth.flags.hci_vendor_specific_extension") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void sendBluetoothHciVendorSpecificCommand(@IntRange(from=0, to=1023) int, @NonNull byte[]);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, android.Manifest.permission.MODIFY_PHONE_STATE}) public boolean setActiveDevice(@NonNull android.bluetooth.BluetoothDevice, int);
method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void setAutoOnEnabled(boolean);
method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int setBluetoothHciSnoopLoggingMode(int);
@@ -98,6 +100,7 @@ package android.bluetooth {
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int startRfcommServer(@NonNull String, @NonNull java.util.UUID, @NonNull android.app.PendingIntent);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int stopRfcommServer(@NonNull java.util.UUID);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public boolean unregisterBluetoothConnectionCallback(@NonNull android.bluetooth.BluetoothAdapter.BluetoothConnectionCallback);
+ method @FlaggedApi("com.android.bluetooth.flags.hci_vendor_specific_extension") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void unregisterBluetoothHciVendorSpecificCallback(@NonNull android.bluetooth.BluetoothAdapter.BluetoothHciVendorSpecificCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int unregisterBluetoothQualityReportReadyCallback(@NonNull android.bluetooth.BluetoothAdapter.BluetoothQualityReportReadyCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int unregisterPreferredAudioProfilesChangedCallback(@NonNull android.bluetooth.BluetoothAdapter.PreferredAudioProfilesChangedCallback);
field @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public static final String ACTION_AUTO_ON_STATE_CHANGED = "android.bluetooth.action.AUTO_ON_STATE_CHANGED";
@@ -126,6 +129,12 @@ package android.bluetooth {
method public void onDeviceDisconnected(@NonNull android.bluetooth.BluetoothDevice, int);
}
+ @FlaggedApi("com.android.bluetooth.flags.hci_vendor_specific_extension") public static interface BluetoothAdapter.BluetoothHciVendorSpecificCallback {
+ method public void onCommandComplete(@IntRange(from=0, to=1023) int, @NonNull byte[]);
+ method public void onCommandStatus(@IntRange(from=0, to=1023) int, @IntRange(from=0, to=255) int);
+ method public void onEvent(@IntRange(from=0, to=254) int, @NonNull byte[]);
+ }
+
public static interface BluetoothAdapter.BluetoothQualityReportReadyCallback {
method public void onBluetoothQualityReportReady(@NonNull android.bluetooth.BluetoothDevice, @NonNull android.bluetooth.BluetoothQualityReport, int);
}
diff --git a/framework/java/android/bluetooth/BluetoothAdapter.java b/framework/java/android/bluetooth/BluetoothAdapter.java
index ebbe97566d..2be58683c5 100644
--- a/framework/java/android/bluetooth/BluetoothAdapter.java
+++ b/framework/java/android/bluetooth/BluetoothAdapter.java
@@ -31,7 +31,9 @@ import static java.util.Objects.requireNonNull;
import android.annotation.BroadcastBehavior;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresNoPermission;
@@ -3901,6 +3903,14 @@ public final class BluetoothAdapter {
mAudioProfilesCallbackWrapper.registerToNewService(mService);
mQualityCallbackWrapper.registerToNewService(mService);
mBluetoothConnectionCallbackWrapper.registerToNewService(mService);
+ synchronized (mHciVendorSpecificCallbackRegistration) {
+ try {
+ mHciVendorSpecificCallbackRegistration.registerToService(
+ mService, mHciVendorSpecificCallbackStub);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to register HCI vendor-specific callback", e);
+ }
+ }
} finally {
mServiceLock.readLock().unlock();
}
@@ -5575,4 +5585,277 @@ public final class BluetoothAdapter {
}
return BluetoothStatusCodes.ERROR_UNKNOWN;
}
+
+ /**
+ * Callbacks for receiving response of HCI Vendor-Specific Commands and Vendor-Specific Events
+ * that arise from the controller.
+ *
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_HCI_VENDOR_SPECIFIC_EXTENSION)
+ public interface BluetoothHciVendorSpecificCallback {
+ /**
+ * Invoked when an `HCI_Command_Status`, in response to a Vendor Command is received.
+ *
+ * @param ocf "Opcode command field" of the HCI Opcode, as defined in the Bluetooth Core
+ * Specification Vol 4, Part E, Section 5.4.1.
+ * @param status as defined by the Bluetooth Core Specification.
+ */
+ void onCommandStatus(
+ @IntRange(from = 0x000, to = 0x3ff) int ocf,
+ @IntRange(from = 0x00, to = 0xff) int status);
+
+ /**
+ * Invoked when an `HCI_Command_Complete`, in response to a Vendor Command is received.
+ *
+ * @param ocf "Opcode command field" of the HCI Opcode, as defined in the Bluetooth Core
+ * Specification Vol 4, Part E, Section 5.4.1.
+ * @param returnParameters Data returned by the command (from 0 to 252 Bytes).
+ */
+ void onCommandComplete(
+ @IntRange(from = 0x000, to = 0x3ff) int ocf, @NonNull byte[] returnParameters);
+
+ /**
+ * Invoked when an event is received.
+ *
+ * @param code The vendor-specific event Code. The first octet of the event parameters
+ * of a vendor-specific event (Bluetooth Core Specification Vol 4, Part E, 5.4.3).
+ * @param data from 0 to 254 Bytes.
+ */
+ void onEvent(@IntRange(from = 0x00, to = 0xfe) int code, @NonNull byte[] data);
+ }
+
+ private static final class HciVendorSpecificCallbackRegistration {
+ private BluetoothHciVendorSpecificCallback mCallback;
+ private Executor mExecutor;
+ private Set<Integer> mEventCodeSet;
+
+ void set(
+ BluetoothHciVendorSpecificCallback callback,
+ Set<Integer> eventCodeSet,
+ Executor executor) {
+ mCallback = callback;
+ mEventCodeSet = eventCodeSet;
+ mExecutor = executor;
+ }
+
+ void reset() {
+ mCallback = null;
+ mEventCodeSet = null;
+ mExecutor = null;
+ }
+
+ boolean isSet() {
+ return mCallback != null;
+ }
+
+ boolean isSet(BluetoothHciVendorSpecificCallback callback) {
+ return isSet() && (callback == mCallback);
+ }
+
+ @RequiresPermission(BLUETOOTH_PRIVILEGED)
+ void registerToService(IBluetooth service, IBluetoothHciVendorSpecificCallback stub) {
+ if (service == null || !isSet()) {
+ return;
+ }
+
+ int[] eventCodes = mEventCodeSet.stream().mapToInt(i -> i).toArray();
+ try {
+ service.registerHciVendorSpecificCallback(stub, eventCodes);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+
+ @RequiresPermission(BLUETOOTH_PRIVILEGED)
+ void unregisterFromService(IBluetooth service, IBluetoothHciVendorSpecificCallback stub) {
+ if (service == null) {
+ return;
+ }
+ try {
+ service.unregisterHciVendorSpecificCallback(stub);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+
+ void execute(Consumer<BluetoothHciVendorSpecificCallback> consumer) {
+ BluetoothHciVendorSpecificCallback callback = mCallback;
+ if (callback == null) {
+ return;
+ }
+ executeFromBinder(mExecutor, () -> consumer.accept(callback));
+ }
+ }
+
+ private final HciVendorSpecificCallbackRegistration mHciVendorSpecificCallbackRegistration =
+ new HciVendorSpecificCallbackRegistration();
+
+ private final IBluetoothHciVendorSpecificCallback mHciVendorSpecificCallbackStub =
+ new IBluetoothHciVendorSpecificCallback.Stub() {
+
+ @Override
+ @RequiresNoPermission
+ public void onCommandStatus(int ocf, int status) {
+ synchronized (mHciVendorSpecificCallbackRegistration) {
+ if (Flags.hciVendorSpecificExtension()) {
+ mHciVendorSpecificCallbackRegistration.execute(
+ (cb) -> cb.onCommandStatus(ocf, status));
+ }
+ }
+ }
+
+ @Override
+ @RequiresNoPermission
+ public void onCommandComplete(int ocf, byte[] returnParameters) {
+ synchronized (mHciVendorSpecificCallbackRegistration) {
+ if (Flags.hciVendorSpecificExtension()) {
+ mHciVendorSpecificCallbackRegistration.execute(
+ (cb) -> cb.onCommandComplete(ocf, returnParameters));
+ }
+ }
+ }
+
+ @Override
+ @RequiresNoPermission
+ public void onEvent(int code, byte[] data) {
+ synchronized (mHciVendorSpecificCallbackRegistration) {
+ if (Flags.hciVendorSpecificExtension()) {
+ mHciVendorSpecificCallbackRegistration.execute(
+ (cb) -> cb.onEvent(code, data));
+ }
+ }
+ }
+ };
+
+ /**
+ * Register an {@link BluetoothHciVendorCallback} to listen for HCI vendor responses and events
+ *
+ * @param eventCodeSet Set of vendor-specific event codes to listen for updates. Each
+ * vendor-specific event code must be in the range 0x00 to 0x4f or 0x60 to 0xff.
+ * The inclusive range 0x50-0x5f is reserved by the system.
+ * @param executor an {@link Executor} to execute given callback
+ * @param callback user implementation of the {@link BluetoothHciVendorCallback}
+ * @throws IllegalArgumentException if the callback is already registered, or event codes not in
+ * a valid range
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_HCI_VENDOR_SPECIFIC_EXTENSION)
+ @RequiresPermission(BLUETOOTH_PRIVILEGED)
+ @SuppressLint("AndroidFrameworkRequiresPermission") // Consumer wrongly report permission
+ public void registerBluetoothHciVendorSpecificCallback(
+ @NonNull Set<Integer> eventCodeSet,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull BluetoothHciVendorSpecificCallback callback) {
+ if (DBG) Log.d(TAG, "registerBluetoothHciVendorSpecificCallback()");
+
+ requireNonNull(eventCodeSet);
+ requireNonNull(executor);
+ requireNonNull(callback);
+ if (eventCodeSet.stream()
+ .anyMatch((n) -> (n < 0) || (n >= 0x50 && n < 0x60) || (n > 0xff))) {
+ throw new IllegalArgumentException("Event code not in valid range");
+ }
+
+ mServiceLock.readLock().lock();
+ try {
+ synchronized (mHciVendorSpecificCallbackRegistration) {
+ if (mHciVendorSpecificCallbackRegistration.isSet()) {
+ throw new IllegalArgumentException("Only one registration allowed");
+ }
+ mHciVendorSpecificCallbackRegistration.set(callback, eventCodeSet, executor);
+ try {
+ mHciVendorSpecificCallbackRegistration.registerToService(
+ mService, mHciVendorSpecificCallbackStub);
+ } catch (Exception e) {
+ mHciVendorSpecificCallbackRegistration.reset();
+ throw e;
+ }
+ }
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Unregister the specified {@link BluetoothHciVendorCallback}
+ *
+ * @param callback user implementation of the {@link BluetoothHciVendorCallback}
+ * @throws IllegalArgumentException if the callback has not been registered
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_HCI_VENDOR_SPECIFIC_EXTENSION)
+ @RequiresPermission(BLUETOOTH_PRIVILEGED)
+ @SuppressLint("AndroidFrameworkRequiresPermission") // Consumer wrongly report permission
+ public void unregisterBluetoothHciVendorSpecificCallback(
+ @NonNull BluetoothHciVendorSpecificCallback callback) {
+ if (DBG) Log.d(TAG, "unregisterBluetoothHciVendorSpecificCallback()");
+
+ requireNonNull(callback);
+
+ mServiceLock.readLock().lock();
+ try {
+ synchronized (mHciVendorSpecificCallbackRegistration) {
+ if (!mHciVendorSpecificCallbackRegistration.isSet(callback)) {
+ throw new IllegalArgumentException("Callback not registered");
+ }
+ mHciVendorSpecificCallbackRegistration.unregisterFromService(
+ mService, mHciVendorSpecificCallbackStub);
+ mHciVendorSpecificCallbackRegistration.reset();
+ }
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Send an HCI Vendor-Specific Command
+ *
+ * @param ocf "Opcode command field" of the HCI Opcode, as defined in the Bluetooth Core
+ * Specification Vol 4, Part E, Section 5.4.1. Each vendor-specific ocf must be in the range
+ * 0x000-0x14f or 0x160-0x3ff. The inclusive range 0x150-0x15f is reserved by the system.
+ * @param parameters shall be less or equal to 255 bytes.
+ * @throws IllegalArgumentException if the ocf is not in a valid range
+ * @throws IllegalStateException when a callback has not been registered
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_HCI_VENDOR_SPECIFIC_EXTENSION)
+ @RequiresPermission(BLUETOOTH_PRIVILEGED)
+ public void sendBluetoothHciVendorSpecificCommand(
+ @IntRange(from = 0x000, to = 0x3ff) int ocf, @NonNull byte[] parameters) {
+ if (DBG) Log.d(TAG, "sendBluetoothHciVendorSpecificCommand()");
+
+ // Open this no-op android command for test purpose
+ int getVendorCapabilitiesOcf = 0x153;
+ if (ocf < 0
+ || (ocf >= 0x150 && ocf < 0x160 && ocf != getVendorCapabilitiesOcf)
+ || ocf > 0x3ff) {
+ throw new IllegalArgumentException("Opcode command value not in valid range");
+ }
+
+ requireNonNull(parameters);
+ if (parameters.length > 255) {
+ throw new IllegalArgumentException("Parameters size is too big");
+ }
+
+ mServiceLock.readLock().lock();
+ try {
+ if (!mHciVendorSpecificCallbackRegistration.isSet()) {
+ throw new IllegalStateException("No Callback registered");
+ }
+
+ if (mService != null) {
+ mService.sendHciVendorSpecificCommand(
+ ocf, parameters, mHciVendorSpecificCallbackStub);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
}
diff --git a/system/btif/Android.bp b/system/btif/Android.bp
index e6bdf90de5..fd27f850ba 100644
--- a/system/btif/Android.bp
+++ b/system/btif/Android.bp
@@ -198,6 +198,7 @@ cc_library_static {
"src/btif_gatt_client.cc",
"src/btif_gatt_server.cc",
"src/btif_gatt_util.cc",
+ "src/btif_hci_vs.cc",
"src/btif_iot_config.cc",
"src/btif_keystore.cc",
"src/btif_metrics_logging.cc",
diff --git a/system/btif/BUILD.gn b/system/btif/BUILD.gn
index 68ca336a3d..167ac3bcfb 100644
--- a/system/btif/BUILD.gn
+++ b/system/btif/BUILD.gn
@@ -59,6 +59,7 @@ static_library("btif") {
"src/btif_le_audio_broadcaster.cc",
"src/btif_has_client.cc",
"src/btif_hearing_aid.cc",
+ "src/btif_hci_vs.cc",
"src/btif_hf.cc",
"src/btif_hf_client.cc",
"src/btif_hh.cc",
diff --git a/system/btif/include/btif_hci_vs.h b/system/btif/include/btif_hci_vs.h
new file mode 100644
index 0000000000..0e227ed8e7
--- /dev/null
+++ b/system/btif/include/btif_hci_vs.h
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+#ifndef BTIF_HCI_VS_H_
+#define BTIF_HCI_VS_H_
+
+#include <bluetooth/log.h>
+
+#include "include/hardware/bt_hci_vs.h"
+
+namespace bluetooth {
+namespace hci_vs {
+
+BluetoothHciVendorSpecificInterface* getBluetoothHciVendorSpecificInterface();
+
+} // namespace hci_vs
+} // namespace bluetooth
+
+#endif // BTIF_HCI_VS_H_
diff --git a/system/btif/src/btif_hci_vs.cc b/system/btif/src/btif_hci_vs.cc
new file mode 100644
index 0000000000..5d591eaf5b
--- /dev/null
+++ b/system/btif/src/btif_hci_vs.cc
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+
+#include "btif_hci_vs.h"
+
+#include <bluetooth/log.h>
+
+#include "btif_common.h"
+#include "hci/hci_interface.h"
+#include "main/shim/entry.h"
+#include "packet/raw_builder.h"
+#include "stack/include/main_thread.h"
+
+namespace bluetooth {
+namespace hci_vs {
+
+std::unique_ptr<BluetoothHciVendorSpecificInterface> hciVendorSpecificInterface;
+
+class BluetoothHciVendorSpecificInterfaceImpl
+ : public bluetooth::hci_vs::BluetoothHciVendorSpecificInterface {
+ ~BluetoothHciVendorSpecificInterfaceImpl() override = default;
+
+ void init(BluetoothHciVendorSpecificCallbacks* callbacks) override {
+ log::info("BluetoothHciVendorSpecificInterfaceImpl");
+ this->callbacks = callbacks;
+ }
+
+ void sendCommand(uint16_t ocf, std::vector<uint8_t> parameters, Cookie cookie) override {
+ // TODO: Send HCI Command
+ (void)ocf;
+ (void)parameters;
+ (void)cookie;
+ }
+
+private:
+ BluetoothHciVendorSpecificCallbacks* callbacks = nullptr;
+};
+
+BluetoothHciVendorSpecificInterface* getBluetoothHciVendorSpecificInterface() {
+ if (!hciVendorSpecificInterface) {
+ hciVendorSpecificInterface.reset(new BluetoothHciVendorSpecificInterfaceImpl());
+ }
+
+ return hciVendorSpecificInterface.get();
+}
+
+} // namespace hci_vs
+} // namespace bluetooth
diff --git a/system/include/hardware/bt_hci_vs.h b/system/include/hardware/bt_hci_vs.h
new file mode 100644
index 0000000000..729f5a5334
--- /dev/null
+++ b/system/include/hardware/bt_hci_vs.h
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <vector>
+
+namespace bluetooth {
+namespace hci_vs {
+
+typedef std::array<uint8_t, 16> Cookie;
+
+class BluetoothHciVendorSpecificCallbacks {
+public:
+ virtual ~BluetoothHciVendorSpecificCallbacks() = default;
+
+ virtual void onCommandStatus(uint16_t ocf, uint8_t status, Cookie cookie) = 0;
+ virtual void onCommandComplete(uint16_t ocf, std::vector<uint8_t> return_parameters,
+ Cookie cookie) = 0;
+ virtual void onEvent(uint8_t code, std::vector<uint8_t> data) = 0;
+};
+
+class BluetoothHciVendorSpecificInterface {
+public:
+ virtual ~BluetoothHciVendorSpecificInterface() = default;
+
+ virtual void init(BluetoothHciVendorSpecificCallbacks* callbacks) = 0;
+
+ virtual void sendCommand(uint16_t ocf, std::vector<uint8_t> parameters, Cookie cookie) = 0;
+};
+
+} // namespace hci_vs
+} // namespace bluetooth