Implement Context Hub HAL v4 for Cuttlefish

Bug: 361608756
Change-Id: I56466cb7e83bb6a3ee08a37b8a3461c28cb2e840
Flag: TEST_ONLY
Test: atest VtsAidlHalContextHubTargetTest
diff --git a/contexthub/aidl/default/Android.bp b/contexthub/aidl/default/Android.bp
index da173f9..c0b147c 100644
--- a/contexthub/aidl/default/Android.bp
+++ b/contexthub/aidl/default/Android.bp
@@ -30,6 +30,7 @@
     shared_libs: [
         "libbase",
         "libbinder_ndk",
+        "liblog",
         "android.hardware.contexthub-V4-ndk",
     ],
     export_include_dirs: ["include"],
@@ -51,6 +52,7 @@
     shared_libs: [
         "libbase",
         "libbinder_ndk",
+        "liblog",
         "android.hardware.contexthub-V4-ndk",
     ],
     static_libs: [
diff --git a/contexthub/aidl/default/ContextHub.cpp b/contexthub/aidl/default/ContextHub.cpp
index a915191..4ae9c09 100644
--- a/contexthub/aidl/default/ContextHub.cpp
+++ b/contexthub/aidl/default/ContextHub.cpp
@@ -16,10 +16,54 @@
 
 #include "contexthub-impl/ContextHub.h"
 
-namespace aidl::android::hardware::contexthub {
+#ifndef LOG_TAG
+#define LOG_TAG "CHRE"
+#endif
+
+#include <inttypes.h>
+#include <log/log.h>
 
 using ::ndk::ScopedAStatus;
 
+namespace aidl::android::hardware::contexthub {
+
+namespace {
+
+constexpr uint64_t kMockVendorHubId = 0x1234567812345678;
+constexpr uint64_t kMockVendorHub2Id = 0x0EADBEEFDEADBEEF;
+
+// Mock endpoints for the default implementation.
+// These endpoints just echo back any messages sent to them.
+constexpr size_t kMockEndpointCount = 4;
+const EndpointInfo kMockEndpointInfos[kMockEndpointCount] = {
+        {
+                .id = {.hubId = kMockVendorHubId, .id = UINT64_C(0x1)},
+                .type = EndpointInfo::EndpointType::GENERIC,
+                .name = "Mock Endpoint 1",
+                .version = 1,
+        },
+        {
+                .id = {.hubId = kMockVendorHubId, .id = UINT64_C(0x2)},
+                .type = EndpointInfo::EndpointType::GENERIC,
+                .name = "Mock Endpoint 2",
+                .version = 2,
+        },
+        {
+                .id = {.hubId = kMockVendorHub2Id, .id = UINT64_C(0x1)},
+                .type = EndpointInfo::EndpointType::GENERIC,
+                .name = "Mock Endpoint 3",
+                .version = 1,
+        },
+        {
+                .id = {.hubId = kMockVendorHub2Id, .id = UINT64_C(0x2)},
+                .type = EndpointInfo::EndpointType::GENERIC,
+                .name = "Mock Endpoint 4",
+                .version = 2,
+        },
+};
+
+}  // anonymous namespace
+
 ScopedAStatus ContextHub::getContextHubs(std::vector<ContextHubInfo>* out_contextHubInfos) {
     ContextHubInfo hub = {};
     hub.name = "Mock Context Hub";
@@ -112,7 +156,13 @@
     }
 }
 
-ScopedAStatus ContextHub::setTestMode(bool /* enable */) {
+ScopedAStatus ContextHub::setTestMode(bool enable) {
+    if (enable) {
+        std::unique_lock<std::mutex> lock(mEndpointMutex);
+        mEndpoints.clear();
+        mEndpointSessions.clear();
+        mEndpointCallback = nullptr;
+    }
     return ScopedAStatus::ok();
 }
 
@@ -137,6 +187,10 @@
 }
 
 ScopedAStatus ContextHub::getHubs(std::vector<HubInfo>* _aidl_return) {
+    if (_aidl_return == nullptr) {
+        return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+    }
+
     ContextHubInfo hub = {};
     hub.name = "Mock Context Hub";
     hub.vendor = "AOSP";
@@ -158,61 +212,217 @@
     vendorHub.version = 42;
 
     HubInfo hubInfo2 = {};
-    hubInfo2.hubId = UINT64_C(0x1234567812345678);
+    hubInfo2.hubId = kMockVendorHubId;
     hubInfo2.hubDetails =
             HubInfo::HubDetails::make<HubInfo::HubDetails::Tag::vendorHubInfo>(vendorHub);
 
+    VendorHubInfo vendorHub2 = {};
+    vendorHub2.name = "Mock Vendor Hub 2";
+    vendorHub2.version = 24;
+
+    HubInfo hubInfo3 = {};
+    hubInfo3.hubId = kMockVendorHub2Id;
+    hubInfo3.hubDetails =
+            HubInfo::HubDetails::make<HubInfo::HubDetails::Tag::vendorHubInfo>(vendorHub2);
+
     _aidl_return->push_back(hubInfo1);
     _aidl_return->push_back(hubInfo2);
+    _aidl_return->push_back(hubInfo3);
 
     return ScopedAStatus::ok();
 };
 
-ScopedAStatus ContextHub::getEndpoints(std::vector<EndpointInfo>* /* _aidl_return */) {
-    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+ScopedAStatus ContextHub::getEndpoints(std::vector<EndpointInfo>* _aidl_return) {
+    if (_aidl_return == nullptr) {
+        return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+    }
+
+    Service echoService;
+    echoService.format = Service::RpcFormat::CUSTOM;
+    echoService.serviceDescriptor = "ECHO";
+    echoService.majorVersion = 1;
+    echoService.minorVersion = 0;
+
+    for (const EndpointInfo& endpoint : kMockEndpointInfos) {
+        EndpointInfo endpointWithService(endpoint);
+        endpointWithService.services.push_back(echoService);
+        _aidl_return->push_back(std::move(endpointWithService));
+    }
+
+    return ScopedAStatus::ok();
 };
 
-ScopedAStatus ContextHub::registerEndpoint(const EndpointInfo& /* in_endpoint */) {
-    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+ScopedAStatus ContextHub::registerEndpoint(const EndpointInfo& in_endpoint) {
+    std::unique_lock<std::mutex> lock(mEndpointMutex);
+
+    for (const EndpointInfo& endpoint : mEndpoints) {
+        if ((endpoint.id.id == in_endpoint.id.id && endpoint.id.hubId == in_endpoint.id.hubId) ||
+            endpoint.name == in_endpoint.name) {
+            return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+        }
+    }
+    mEndpoints.push_back(in_endpoint);
+    return ScopedAStatus::ok();
 };
 
-ScopedAStatus ContextHub::unregisterEndpoint(const EndpointInfo& /* in_endpoint */) {
-    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+ScopedAStatus ContextHub::unregisterEndpoint(const EndpointInfo& in_endpoint) {
+    std::unique_lock<std::mutex> lock(mEndpointMutex);
+
+    for (auto it = mEndpoints.begin(); it != mEndpoints.end(); ++it) {
+        if (it->id.id == in_endpoint.id.id && it->id.hubId == in_endpoint.id.hubId) {
+            mEndpoints.erase(it);
+            return ScopedAStatus::ok();
+        }
+    }
+    return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
 };
 
 ScopedAStatus ContextHub::registerEndpointCallback(
-        const std::shared_ptr<IEndpointCallback>& /* in_callback */) {
-    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+        const std::shared_ptr<IEndpointCallback>& in_callback) {
+    std::unique_lock<std::mutex> lock(mEndpointMutex);
+
+    mEndpointCallback = in_callback;
+    return ScopedAStatus::ok();
 };
 
-ScopedAStatus ContextHub::requestSessionIdRange(int32_t /* in_size */,
-                                                std::vector<int32_t>* /* _aidl_return */) {
-    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+ScopedAStatus ContextHub::requestSessionIdRange(int32_t in_size,
+                                                std::vector<int32_t>* _aidl_return) {
+    constexpr int32_t kMaxSize = 1024;
+    if (in_size > kMaxSize || _aidl_return == nullptr) {
+        return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+    }
+
+    {
+        std::lock_guard<std::mutex> lock(mEndpointMutex);
+        mMaxValidSessionId = in_size;
+    }
+
+    _aidl_return->push_back(0);
+    _aidl_return->push_back(in_size);
+    return ScopedAStatus::ok();
 };
 
 ScopedAStatus ContextHub::openEndpointSession(
-        int32_t /* in_sessionId */, const EndpointId& /* in_destination */,
-        const EndpointId& /* in_initiator */,
-        const std::optional<std::string>& /* in_serviceDescriptor */) {
-    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+        int32_t in_sessionId, const EndpointId& in_destination, const EndpointId& in_initiator,
+        const std::optional<std::string>& in_serviceDescriptor) {
+    // We are not calling onCloseEndpointSession on failure because the remote endpoints (our
+    // mock endpoints) always accept the session.
+
+    std::shared_ptr<IEndpointCallback> callback = nullptr;
+    {
+        std::unique_lock<std::mutex> lock(mEndpointMutex);
+        if (in_sessionId > mMaxValidSessionId) {
+            ALOGE("openEndpointSession: session ID %" PRId32 " is invalid", in_sessionId);
+            return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+        }
+
+        for (const EndpointSession& session : mEndpointSessions) {
+            bool sessionAlreadyExists =
+                    (session.initiator == in_destination && session.peer == in_initiator) ||
+                    (session.peer == in_destination && session.initiator == in_initiator);
+            if (sessionAlreadyExists) {
+                ALOGD("openEndpointSession: session ID %" PRId32 " already exists", in_sessionId);
+                return (session.sessionId == in_sessionId &&
+                        session.serviceDescriptor == in_serviceDescriptor)
+                               ? ScopedAStatus::ok()
+                               : ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+            } else if (session.sessionId == in_sessionId) {
+                ALOGE("openEndpointSession: session ID %" PRId32 " is invalid: endpoint mismatch",
+                      in_sessionId);
+                return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+            }
+        }
+
+        // Verify the initiator and destination are valid endpoints
+        bool initiatorIsValid = findEndpoint(in_initiator, mEndpoints.begin(), mEndpoints.end());
+        if (!initiatorIsValid) {
+            ALOGE("openEndpointSession: initiator %" PRIu64 ":%" PRIu64 " is invalid",
+                  in_initiator.id, in_initiator.hubId);
+            return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+        }
+        bool destinationIsValid = findEndpoint(in_destination, &kMockEndpointInfos[0],
+                                               &kMockEndpointInfos[kMockEndpointCount]);
+        if (!destinationIsValid) {
+            ALOGE("openEndpointSession: destination %" PRIu64 ":%" PRIu64 " is invalid",
+                  in_destination.id, in_destination.hubId);
+            return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+        }
+
+        mEndpointSessions.push_back({
+                .sessionId = in_sessionId,
+                .initiator = in_initiator,
+                .peer = in_destination,
+                .serviceDescriptor = in_serviceDescriptor,
+        });
+
+        if (mEndpointCallback != nullptr) {
+            callback = mEndpointCallback;
+        }
+    }
+
+    if (callback != nullptr) {
+        callback->onEndpointSessionOpenComplete(in_sessionId);
+    }
+    return ScopedAStatus::ok();
 };
 
-ScopedAStatus ContextHub::sendMessageToEndpoint(int32_t /* in_sessionId */,
-                                                const Message& /* in_msg */) {
-    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+ScopedAStatus ContextHub::sendMessageToEndpoint(int32_t in_sessionId, const Message& in_msg) {
+    bool foundSession = false;
+    std::shared_ptr<IEndpointCallback> callback = nullptr;
+    {
+        std::unique_lock<std::mutex> lock(mEndpointMutex);
+
+        for (const EndpointSession& session : mEndpointSessions) {
+            if (session.sessionId == in_sessionId) {
+                foundSession = true;
+                break;
+            }
+        }
+
+        if (mEndpointCallback != nullptr) {
+            callback = mEndpointCallback;
+        }
+    }
+
+    if (!foundSession) {
+        ALOGE("sendMessageToEndpoint: session ID %" PRId32 " is invalid", in_sessionId);
+        return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+    }
+
+    if (callback != nullptr) {
+        if (in_msg.flags & Message::FLAG_REQUIRES_DELIVERY_STATUS) {
+            MessageDeliveryStatus msgStatus = {};
+            msgStatus.messageSequenceNumber = in_msg.sequenceNumber;
+            msgStatus.errorCode = ErrorCode::OK;
+            callback->onMessageDeliveryStatusReceived(in_sessionId, msgStatus);
+        }
+
+        // Echo the message back
+        callback->onMessageReceived(in_sessionId, in_msg);
+    }
+    return ScopedAStatus::ok();
 };
 
 ScopedAStatus ContextHub::sendMessageDeliveryStatusToEndpoint(
         int32_t /* in_sessionId */, const MessageDeliveryStatus& /* in_msgStatus */) {
-    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+    return ScopedAStatus::ok();
 };
 
-ScopedAStatus ContextHub::closeEndpointSession(int32_t /* in_sessionId */, Reason /* in_reason */) {
-    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+ScopedAStatus ContextHub::closeEndpointSession(int32_t in_sessionId, Reason /* in_reason */) {
+    std::unique_lock<std::mutex> lock(mEndpointMutex);
+
+    for (auto it = mEndpointSessions.begin(); it != mEndpointSessions.end(); ++it) {
+        if (it->sessionId == in_sessionId) {
+            mEndpointSessions.erase(it);
+            return ScopedAStatus::ok();
+        }
+    }
+    ALOGE("closeEndpointSession: session ID %" PRId32 " is invalid", in_sessionId);
+    return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
 };
 
 ScopedAStatus ContextHub::endpointSessionOpenComplete(int32_t /* in_sessionId */) {
-    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+    return ScopedAStatus::ok();
 };
 
 }  // namespace aidl::android::hardware::contexthub
diff --git a/contexthub/aidl/default/include/contexthub-impl/ContextHub.h b/contexthub/aidl/default/include/contexthub-impl/ContextHub.h
index 5680a77..4968878 100644
--- a/contexthub/aidl/default/include/contexthub-impl/ContextHub.h
+++ b/contexthub/aidl/default/include/contexthub-impl/ContextHub.h
@@ -18,6 +18,7 @@
 
 #include <aidl/android/hardware/contexthub/BnContextHub.h>
 
+#include <mutex>
 #include <unordered_set>
 #include <vector>
 
@@ -72,10 +73,37 @@
     ::ndk::ScopedAStatus endpointSessionOpenComplete(int32_t in_sessionId) override;
 
   private:
+    struct EndpointSession {
+        int32_t sessionId;
+        EndpointId initiator;
+        EndpointId peer;
+        std::optional<std::string> serviceDescriptor;
+    };
+
     static constexpr uint32_t kMockHubId = 0;
+
+    //! Finds an endpoint in the range defined by the endpoints
+    //! @return whether the endpoint was found
+    template <typename Iter>
+    bool findEndpoint(const EndpointId& target, const Iter& begin, const Iter& end) {
+        for (auto iter = begin; iter != end; ++iter) {
+            if (iter->id.id == target.id && iter->id.hubId == target.hubId) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     std::shared_ptr<IContextHubCallback> mCallback;
 
     std::unordered_set<char16_t> mConnectedHostEndpoints;
+
+    //! Endpoint storage and information
+    std::mutex mEndpointMutex;
+    std::vector<EndpointInfo> mEndpoints;
+    std::vector<EndpointSession> mEndpointSessions;
+    std::shared_ptr<IEndpointCallback> mEndpointCallback;
+    int32_t mMaxValidSessionId = 0;
 };
 
 }  // namespace contexthub
diff --git a/contexthub/aidl/vts/Android.bp b/contexthub/aidl/vts/Android.bp
index 62a319e..a19b6fd 100644
--- a/contexthub/aidl/vts/Android.bp
+++ b/contexthub/aidl/vts/Android.bp
@@ -33,7 +33,7 @@
         "libbinder",
     ],
     static_libs: [
-        "android.hardware.contexthub-V3-cpp",
+        "android.hardware.contexthub-V4-cpp",
         "VtsHalContexthubUtils",
     ],
     test_suites: [
diff --git a/contexthub/aidl/vts/VtsAidlHalContextHubTargetTest.cpp b/contexthub/aidl/vts/VtsAidlHalContextHubTargetTest.cpp
index fa427a5..95a96cd 100644
--- a/contexthub/aidl/vts/VtsAidlHalContextHubTargetTest.cpp
+++ b/contexthub/aidl/vts/VtsAidlHalContextHubTargetTest.cpp
@@ -20,8 +20,10 @@
 
 #include <android/hardware/contexthub/BnContextHub.h>
 #include <android/hardware/contexthub/BnContextHubCallback.h>
+#include <android/hardware/contexthub/BnEndpointCallback.h>
 #include <android/hardware/contexthub/IContextHub.h>
 #include <android/hardware/contexthub/IContextHubCallback.h>
+#include <android/hardware/contexthub/IEndpointCallback.h>
 #include <binder/IServiceManager.h>
 #include <binder/ProcessState.h>
 #include <log/log.h>
@@ -34,18 +36,25 @@
 using ::android::String16;
 using ::android::binder::Status;
 using ::android::hardware::contexthub::AsyncEventType;
+using ::android::hardware::contexthub::BnEndpointCallback;
 using ::android::hardware::contexthub::ContextHubInfo;
 using ::android::hardware::contexthub::ContextHubMessage;
+using ::android::hardware::contexthub::EndpointId;
+using ::android::hardware::contexthub::EndpointInfo;
 using ::android::hardware::contexthub::ErrorCode;
 using ::android::hardware::contexthub::HostEndpointInfo;
+using ::android::hardware::contexthub::HubInfo;
 using ::android::hardware::contexthub::IContextHub;
 using ::android::hardware::contexthub::IContextHubCallbackDefault;
+using ::android::hardware::contexthub::Message;
 using ::android::hardware::contexthub::MessageDeliveryStatus;
 using ::android::hardware::contexthub::NanoappBinary;
 using ::android::hardware::contexthub::NanoappInfo;
 using ::android::hardware::contexthub::NanoappRpcService;
 using ::android::hardware::contexthub::NanSessionRequest;
 using ::android::hardware::contexthub::NanSessionStateUpdate;
+using ::android::hardware::contexthub::Reason;
+using ::android::hardware::contexthub::Service;
 using ::android::hardware::contexthub::Setting;
 using ::android::hardware::contexthub::vts_utils::kNonExistentAppId;
 using ::android::hardware::contexthub::vts_utils::waitForCallback;
@@ -61,8 +70,14 @@
         contextHub = android::waitForDeclaredService<IContextHub>(
                 String16(std::get<0>(GetParam()).c_str()));
         ASSERT_NE(contextHub, nullptr);
+
+        // Best effort enable test mode - this may not be supported on older HALS, so we
+        // ignore the return value.
+        contextHub->setTestMode(/* enable= */ true);
     }
 
+    virtual void TearDown() override { contextHub->setTestMode(/* enable= */ false); }
+
     uint32_t getHubId() { return std::get<1>(GetParam()); }
 
     void testSettingChanged(Setting setting);
@@ -465,6 +480,290 @@
     }
 }
 
+class TestEndpointCallback : public BnEndpointCallback {
+  public:
+    Status onEndpointStarted(const std::vector<EndpointInfo>& /* endpointInfos */) override {
+        return Status::ok();
+    }
+
+    Status onEndpointStopped(const std::vector<EndpointId>& /* endpointIds */,
+                             Reason /* reason */) override {
+        return Status::ok();
+    }
+
+    Status onMessageReceived(int32_t /* sessionId */, const Message& message) override {
+        mMessages.push_back(message);
+        return Status::ok();
+    }
+
+    Status onMessageDeliveryStatusReceived(int32_t /* sessionId */,
+                                           const MessageDeliveryStatus& /* msgStatus */) override {
+        return Status::ok();
+    }
+
+    Status onEndpointSessionOpenRequest(
+            int32_t /* sessionId */, const EndpointId& /* destination */,
+            const EndpointId& /* initiator */,
+            const std::optional<String16>& /* serviceDescriptor */) override {
+        return Status::ok();
+    }
+
+    Status onCloseEndpointSession(int32_t /* sessionId */, Reason /* reason */) override {
+        return Status::ok();
+    }
+
+    Status onEndpointSessionOpenComplete(int32_t /* sessionId */) override {
+        mWasOnEndpointSessionOpenCompleteCalled = true;
+        return Status::ok();
+    }
+
+    std::vector<Message> getMessages() { return mMessages; }
+
+    bool wasOnEndpointSessionOpenCompleteCalled() {
+        return mWasOnEndpointSessionOpenCompleteCalled;
+    }
+    void resetWasOnEndpointSessionOpenCompleteCalled() {
+        mWasOnEndpointSessionOpenCompleteCalled = false;
+    }
+
+  private:
+    std::vector<Message> mMessages;
+    bool mWasOnEndpointSessionOpenCompleteCalled = false;
+};
+
+TEST_P(ContextHubAidl, RegisterEndpoint) {
+    EndpointInfo endpointInfo;
+    endpointInfo.id.id = 1;
+    endpointInfo.id.hubId = 0xCAFECAFECAFECAFE;
+    endpointInfo.type = EndpointInfo::EndpointType::NATIVE;
+    endpointInfo.name = String16("Test host endpoint 1");
+    endpointInfo.version = 42;
+
+    Status status = contextHub->registerEndpoint(endpointInfo);
+    if (status.exceptionCode() == Status::EX_UNSUPPORTED_OPERATION ||
+        status.transactionError() == android::UNKNOWN_TRANSACTION) {
+        GTEST_SKIP() << "Not supported -> old API; or not implemented";
+    } else {
+        EXPECT_TRUE(status.isOk());
+    }
+}
+
+TEST_P(ContextHubAidl, RegisterEndpointSameNameFailure) {
+    EndpointInfo endpointInfo;
+    endpointInfo.id.id = 2;
+    endpointInfo.id.hubId = 0xCAFECAFECAFECAFE;
+    endpointInfo.type = EndpointInfo::EndpointType::NATIVE;
+    endpointInfo.name = String16("Test host endpoint 2");
+    endpointInfo.version = 42;
+
+    EndpointInfo endpointInfo2;
+    endpointInfo2.id.id = 3;
+    endpointInfo2.id.hubId = 0xCAFECAFECAFECAFE;
+    endpointInfo2.type = EndpointInfo::EndpointType::NATIVE;
+    endpointInfo2.name = String16("Test host endpoint 2");
+    endpointInfo2.version = 42;
+
+    Status status = contextHub->registerEndpoint(endpointInfo);
+    if (status.exceptionCode() == Status::EX_UNSUPPORTED_OPERATION ||
+        status.transactionError() == android::UNKNOWN_TRANSACTION) {
+        GTEST_SKIP() << "Not supported -> old API; or not implemented";
+    } else {
+        EXPECT_TRUE(status.isOk());
+    }
+
+    EXPECT_FALSE(contextHub->registerEndpoint(endpointInfo2).isOk());
+}
+
+TEST_P(ContextHubAidl, RegisterEndpointSameIdFailure) {
+    EndpointInfo endpointInfo;
+    endpointInfo.id.id = 4;
+    endpointInfo.id.hubId = 0xCAFECAFECAFECAFE;
+    endpointInfo.type = EndpointInfo::EndpointType::NATIVE;
+    endpointInfo.name = String16("Test host endpoint 4");
+    endpointInfo.version = 42;
+
+    EndpointInfo endpointInfo2;
+    endpointInfo2.id.id = 4;
+    endpointInfo2.id.hubId = 0xCAFECAFECAFECAFE;
+    endpointInfo2.type = EndpointInfo::EndpointType::NATIVE;
+    endpointInfo2.name = String16("Test host endpoint - same ID test");
+    endpointInfo2.version = 42;
+
+    Status status = contextHub->registerEndpoint(endpointInfo);
+    if (status.exceptionCode() == Status::EX_UNSUPPORTED_OPERATION ||
+        status.transactionError() == android::UNKNOWN_TRANSACTION) {
+        GTEST_SKIP() << "Not supported -> old API; or not implemented";
+    } else {
+        EXPECT_TRUE(status.isOk());
+    }
+
+    EXPECT_FALSE(contextHub->registerEndpoint(endpointInfo2).isOk());
+}
+
+TEST_P(ContextHubAidl, UnregisterEndpoint) {
+    EndpointInfo endpointInfo;
+    endpointInfo.id.id = 6;
+    endpointInfo.id.hubId = 0xCAFECAFECAFECAFE;
+    endpointInfo.type = EndpointInfo::EndpointType::NATIVE;
+    endpointInfo.name = String16("Test host endpoint 6");
+    endpointInfo.version = 42;
+
+    Status status = contextHub->registerEndpoint(endpointInfo);
+    if (status.exceptionCode() == Status::EX_UNSUPPORTED_OPERATION ||
+        status.transactionError() == android::UNKNOWN_TRANSACTION) {
+        GTEST_SKIP() << "Not supported -> old API; or not implemented";
+    } else {
+        EXPECT_TRUE(status.isOk());
+    }
+
+    EXPECT_TRUE(contextHub->unregisterEndpoint(endpointInfo).isOk());
+}
+
+TEST_P(ContextHubAidl, UnregisterEndpointNonexistent) {
+    EndpointInfo endpointInfo;
+    endpointInfo.id.id = 100;
+    endpointInfo.id.hubId = 0xCAFECAFECAFECAFE;
+    endpointInfo.type = EndpointInfo::EndpointType::NATIVE;
+    endpointInfo.name = String16("Test host endpoint 100");
+    endpointInfo.version = 42;
+
+    Status status = contextHub->unregisterEndpoint(endpointInfo);
+    if (status.exceptionCode() == Status::EX_UNSUPPORTED_OPERATION ||
+        status.transactionError() == android::UNKNOWN_TRANSACTION) {
+        GTEST_SKIP() << "Not supported -> old API; or not implemented";
+    } else {
+        EXPECT_FALSE(status.isOk());
+    }
+}
+
+TEST_P(ContextHubAidl, RegisterCallback) {
+    auto cb = sp<TestEndpointCallback>::make();
+    Status status = contextHub->registerEndpointCallback(cb);
+    if (status.exceptionCode() == Status::EX_UNSUPPORTED_OPERATION ||
+        status.transactionError() == android::UNKNOWN_TRANSACTION) {
+        GTEST_SKIP() << "Not supported -> old API; or not implemented";
+    } else {
+        EXPECT_TRUE(status.isOk());
+    }
+}
+
+TEST_P(ContextHubAidl, OpenEndpointSessionInvalidRange) {
+    auto cb = sp<TestEndpointCallback>::make();
+    Status status = contextHub->registerEndpointCallback(cb);
+    if (status.exceptionCode() == Status::EX_UNSUPPORTED_OPERATION ||
+        status.transactionError() == android::UNKNOWN_TRANSACTION) {
+        GTEST_SKIP() << "Not supported -> old API; or not implemented";
+    } else {
+        EXPECT_TRUE(status.isOk());
+    }
+
+    // Register the endpoint
+    EndpointInfo initiatorEndpoint;
+    initiatorEndpoint.id.id = 7;
+    initiatorEndpoint.id.hubId = 0xCAFECAFECAFECAFE;
+    initiatorEndpoint.type = EndpointInfo::EndpointType::NATIVE;
+    initiatorEndpoint.name = String16("Test host endpoint 7");
+    initiatorEndpoint.version = 42;
+    EXPECT_TRUE(contextHub->registerEndpoint(initiatorEndpoint).isOk());
+
+    // Find the destination, if it exists
+    std::vector<EndpointInfo> endpoints;
+    EXPECT_TRUE(contextHub->getEndpoints(&endpoints).isOk());
+    const EndpointInfo* destinationEndpoint = nullptr;
+    for (const EndpointInfo& endpoint : endpoints) {
+        for (const Service& service : endpoint.services) {
+            if (service.serviceDescriptor == String16("ECHO")) {
+                destinationEndpoint = &endpoint;
+                break;
+            }
+        }
+    }
+    if (destinationEndpoint == nullptr) {
+        return;  // no echo service endpoint -> just return
+    }
+
+    // Request the range
+    constexpr int32_t requestedRange = 100;
+    std::vector<int32_t> range;
+    ASSERT_TRUE(contextHub->requestSessionIdRange(requestedRange, &range).isOk());
+    EXPECT_EQ(range.size(), 2);
+    EXPECT_GE(range[1] - range[0] + 1, requestedRange);
+
+    // Open the session
+    cb->resetWasOnEndpointSessionOpenCompleteCalled();
+    int32_t sessionId = range[1] + 10;  // invalid
+    EXPECT_FALSE(contextHub
+                         ->openEndpointSession(sessionId, destinationEndpoint->id,
+                                               initiatorEndpoint.id,
+                                               /* in_serviceDescriptor= */ String16("ECHO"))
+                         .isOk());
+    EXPECT_FALSE(cb->wasOnEndpointSessionOpenCompleteCalled());
+}
+
+TEST_P(ContextHubAidl, OpenEndpointSessionAndSendMessageEchoesBack) {
+    auto cb = sp<TestEndpointCallback>::make();
+    Status status = contextHub->registerEndpointCallback(cb);
+    if (status.exceptionCode() == Status::EX_UNSUPPORTED_OPERATION ||
+        status.transactionError() == android::UNKNOWN_TRANSACTION) {
+        GTEST_SKIP() << "Not supported -> old API; or not implemented";
+    } else {
+        EXPECT_TRUE(status.isOk());
+    }
+
+    // Register the endpoint
+    EndpointInfo initiatorEndpoint;
+    initiatorEndpoint.id.id = 8;
+    initiatorEndpoint.id.hubId = 0xCAFECAFECAFECAFE;
+    initiatorEndpoint.type = EndpointInfo::EndpointType::NATIVE;
+    initiatorEndpoint.name = String16("Test host endpoint 7");
+    initiatorEndpoint.version = 42;
+    EXPECT_TRUE(contextHub->registerEndpoint(initiatorEndpoint).isOk());
+
+    // Find the destination, if it exists
+    std::vector<EndpointInfo> endpoints;
+    EXPECT_TRUE(contextHub->getEndpoints(&endpoints).isOk());
+    const EndpointInfo* destinationEndpoint = nullptr;
+    for (const EndpointInfo& endpoint : endpoints) {
+        for (const Service& service : endpoint.services) {
+            if (service.serviceDescriptor == String16("ECHO")) {
+                destinationEndpoint = &endpoint;
+                break;
+            }
+        }
+    }
+    if (destinationEndpoint == nullptr) {
+        return;  // no echo service endpoint -> just return
+    }
+
+    // Request the range
+    constexpr int32_t requestedRange = 100;
+    std::vector<int32_t> range;
+    ASSERT_TRUE(contextHub->requestSessionIdRange(requestedRange, &range).isOk());
+    EXPECT_EQ(range.size(), 2);
+    EXPECT_GE(range[1] - range[0] + 1, requestedRange);
+
+    // Open the session
+    cb->resetWasOnEndpointSessionOpenCompleteCalled();
+    int32_t sessionId = range[0];
+    ASSERT_TRUE(contextHub
+                        ->openEndpointSession(sessionId, destinationEndpoint->id,
+                                              initiatorEndpoint.id,
+                                              /* in_serviceDescriptor= */ String16("ECHO"))
+                        .isOk());
+    EXPECT_TRUE(cb->wasOnEndpointSessionOpenCompleteCalled());
+
+    // Send the message
+    Message message;
+    message.flags = 0;
+    message.sequenceNumber = 0;
+    message.content.push_back(42);
+    ASSERT_TRUE(contextHub->sendMessageToEndpoint(sessionId, message).isOk());
+
+    // Check for echo
+    EXPECT_FALSE(cb->getMessages().empty());
+    EXPECT_EQ(cb->getMessages().back().content.back(), 42);
+}
+
 std::string PrintGeneratedTest(const testing::TestParamInfo<ContextHubAidl::ParamType>& info) {
     return std::string("CONTEXT_HUB_ID_") + std::to_string(std::get<1>(info.param));
 }