Implement Minimal Telephony

Bug: 310710841
Test: atest CtsTelephonyTestCases
Test: atest CtsCarrierApiTestCases
Change-Id: Ia1a5567419871a9c64abb38f2f0e0951cad3fd7b
diff --git a/radio/aidl/minradio/libminradio/Android.bp b/radio/aidl/minradio/libminradio/Android.bp
new file mode 100644
index 0000000..d920a3a
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/Android.bp
@@ -0,0 +1,85 @@
+// 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "hardware_interfaces_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["hardware_interfaces_license"],
+}
+
+cc_defaults {
+    name: "android.hardware.radio-minradio@defaults",
+    relative_install_path: "hw",
+    vendor: true,
+    cflags: [
+        "-Wall",
+        "-Wextra",
+        "-Werror",
+        "-DANDROID_UTILS_REF_BASE_DISABLE_IMPLICIT_CONSTRUCTION",
+        "-g",
+
+        // binder_to_string.h uses deprecated codecvt_utf8_utf16.
+        // We can't fix it in foreesable future.
+        "-D_LIBCPP_DISABLE_DEPRECATION_WARNINGS",
+    ],
+    shared_libs: [
+        "android.hardware.radio.config-V4-ndk",
+        "android.hardware.radio.data-V4-ndk",
+        "android.hardware.radio.modem-V4-ndk",
+        "android.hardware.radio.network-V4-ndk",
+        "android.hardware.radio.sim-V4-ndk",
+        "libbase",
+        "libbinder_ndk",
+        "libutils",
+    ],
+    sanitize: {
+        address: true,
+        all_undefined: true,
+        fuzzer: true,
+        integer_overflow: true,
+    },
+    strip: {
+        none: true,
+    },
+}
+
+cc_library {
+    name: "android.hardware.radio-library.minradio",
+    defaults: ["android.hardware.radio-minradio@defaults"],
+    export_include_dirs: ["include"],
+    srcs: [
+        "RadioSlotBase.cpp",
+        "ResponseTracker.cpp",
+        "SlotContext.cpp",
+        "config/RadioConfig.cpp",
+        "data/RadioData.cpp",
+        "modem/RadioModem.cpp",
+        "modem/RadioModemResponseTracker.cpp",
+        "network/RadioNetwork.cpp",
+        "network/RadioNetworkResponseTracker.cpp",
+        "network/structs.cpp",
+        "response.cpp",
+        "sim/apps/AraM.cpp",
+        "sim/apps/FilesystemApp.cpp",
+        "sim/apps/tlv.cpp",
+        "sim/App.cpp",
+        "sim/AppManager.cpp",
+        "sim/Filesystem.cpp",
+        "sim/IccUtils.cpp",
+        "sim/RadioSim.cpp",
+    ],
+}
diff --git a/radio/aidl/minradio/libminradio/RadioSlotBase.cpp b/radio/aidl/minradio/libminradio/RadioSlotBase.cpp
new file mode 100644
index 0000000..0f4bd68
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/RadioSlotBase.cpp
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+#include <libminradio/RadioSlotBase.h>
+
+namespace android::hardware::radio::minimal {
+
+RadioSlotBase::RadioSlotBase(std::shared_ptr<SlotContext> context) : mContext(context) {}
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/ResponseTracker.cpp b/radio/aidl/minradio/libminradio/ResponseTracker.cpp
new file mode 100644
index 0000000..20b8522
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/ResponseTracker.cpp
@@ -0,0 +1,165 @@
+/*
+ * 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.
+ */
+
+#include <libminradio/ResponseTracker.h>
+
+#include <libminradio/debug.h>
+
+#include <random>
+
+namespace android::hardware::radio::minimal {
+
+using namespace ::android::hardware::radio::minimal::binder_printing;
+using ::aidl::android::hardware::radio::RadioError;
+using ::aidl::android::hardware::radio::RadioResponseInfo;
+using ::ndk::ScopedAStatus;
+
+RadioError ResponseTrackerResultBase::toError(const ScopedAStatus& status) {
+    CHECK(!status.isOk()) << "statusToError called with no error";
+    return RadioError::GENERIC_FAILURE;
+}
+
+ResponseTrackerResultBase::ResponseTrackerResultBase(const char* descriptor)
+    : ResponseTrackerResultBase(descriptor, RadioError::RADIO_NOT_AVAILABLE) {}
+
+ResponseTrackerResultBase::ResponseTrackerResultBase(const char* descriptor, RadioError error)
+    : mDescriptor(descriptor), mError(error) {}
+
+ResponseTrackerResultBase::ResponseTrackerResultBase(const char* descriptor, ScopedAStatus st)
+    : ResponseTrackerResultBase(descriptor, toError(st)) {}
+
+bool ResponseTrackerResultBase::isOk() const {
+    return mError == RadioError::NONE;
+}
+
+bool ResponseTrackerResultBase::expectOk() const {
+    if (isOk()) return true;
+    LOG(ERROR) << "Request for " << mDescriptor << " failed: " << mError;
+    return false;
+}
+
+RadioError ResponseTrackerResultBase::getError() const {
+    return mError;
+}
+
+const char* ResponseTrackerResultBase::getDescriptor() const {
+    return mDescriptor;
+}
+
+ResponseTrackerBase::ScopedSerial::ScopedSerial(int32_t serial, ResponseTrackerBase* tracker)
+    : mSerial(serial), mTracker(tracker) {}
+
+ResponseTrackerBase::ScopedSerial::~ScopedSerial() {
+    if (mIsReleased) return;
+    mTracker->cancelTracking(*this);
+}
+
+ResponseTrackerBase::ScopedSerial::operator int32_t() const {
+    CHECK(!mIsReleased) << "ScopedSerial " << mSerial << " is not valid anymore";
+    return mSerial;
+}
+
+void ResponseTrackerBase::ScopedSerial::release() {
+    mIsReleased = true;
+}
+
+int32_t ResponseTrackerBase::initialSerial() {
+    /* Android framework tends to start request serial numbers from 0, so let's pick something from
+     * the second quarter of int32_t negative range. This way the chance of having a conflict is
+     * closer to zero. */
+    static const int32_t rangeSize = std::abs(std::numeric_limits<int32_t>::min() / 4);
+    static const int32_t rangeStart = std::numeric_limits<int32_t>::min() + rangeSize;
+
+    static std::random_device generator;
+    static std::uniform_int_distribution<int32_t> distribution(rangeStart, rangeStart + rangeSize);
+
+    return distribution(generator);
+}
+
+ResponseTrackerBase::ScopedSerial ResponseTrackerBase::newSerial() {
+    std::unique_lock lck(mSerialsGuard);
+
+    auto serial = mSerial++;
+    if (serial == 0) [[unlikely]] {
+        serial = mSerial++;
+    }
+    if constexpr (debug::kSuperCrazyVerbose) {
+        LOG(VERBOSE) << "Tracking " << serial << " internally";
+    }
+
+    auto inserted = mTrackedSerials.emplace(serial, nullptr).second;
+    CHECK(inserted) << "Detected tracked serials conflict at " << serial;
+
+    return {serial, this};
+}
+
+bool ResponseTrackerBase::isTracked(int32_t serial) const {
+    std::unique_lock lck(mSerialsGuard);
+    return mTrackedSerials.contains(serial);
+}
+
+void ResponseTrackerBase::cancelTracking(ResponseTrackerBase::ScopedSerial& serial) {
+    std::unique_lock lck(mSerialsGuard);
+    auto erased = mTrackedSerials.erase(serial);
+    CHECK(erased == 1) << "Couldn't cancel tracking " << serial;
+    LOG(VERBOSE) << "Cancelled tracking " << serial << " internally";
+    serial.release();
+}
+
+ScopedAStatus ResponseTrackerBase::handle(const RadioResponseInfo& info,
+                                          std::unique_ptr<ResponseTrackerResultBase> result) {
+    std::unique_lock lck(mSerialsGuard);
+    if constexpr (debug::kSuperCrazyVerbose) {
+        LOG(VERBOSE) << "Handling " << info.serial << " internally (not sending to the framework)";
+    }
+
+    auto it = mTrackedSerials.find(info.serial);
+    CHECK(it != mTrackedSerials.end()) << "Request not tracked: " << info;
+    CHECK(it->second == nullptr) << "Request already handled: " << info;
+    it->second = std::move(result);
+
+    return ScopedAStatus::ok();
+}
+
+std::unique_ptr<ResponseTrackerResultBase> ResponseTrackerBase::getResultBase(
+        ResponseTrackerBase::ScopedSerial& serial) {
+    std::unique_lock lck(mSerialsGuard);
+    auto node = mTrackedSerials.extract(serial);
+    CHECK(node.key()) << "Request " << serial << " is not tracked";
+    if (!node.mapped()) {
+        LOG(WARNING) << "Didn't get result for " << serial
+                     << ". It may either mean setResponseFunctions has reset the callbacks or"
+                        " the callback wasn't called synchronously from the scope of "
+                        "request method implementation.";
+        serial.release();
+        return nullptr;
+    }
+    if constexpr (debug::kSuperCrazyVerbose) {
+        LOG(VERBOSE) << "Finished tracking " << serial << " internally";
+    }
+    serial.release();
+    return std::move(node.mapped());
+}
+
+// This symbol silences "Mismatched versions of delegator and implementation" errors from Delegator
+// implementation. In this specific case, Delegators are used to encapsulate incoming callbacks, not
+// outgoing interfaces - so clamping delegator interface version to lower than implementation's
+// version wouldn't make any difference - the local binary wouldn't know what to do with a newer
+// interface anyways. This happens when Radio HAL (which includes callback interfaces) defined on
+// system partition is newer than one used to build local binary (usually on vendor partition).
+extern "C" void assert2_no_op(const char*, int, const char*, const char*) {}
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/SlotContext.cpp b/radio/aidl/minradio/libminradio/SlotContext.cpp
new file mode 100644
index 0000000..cffc178
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/SlotContext.cpp
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+#include <libminradio/SlotContext.h>
+
+namespace android::hardware::radio::minimal {
+
+SlotContext::SlotContext(unsigned slotIndex) : mSlotIndex(slotIndex) {}
+
+unsigned SlotContext::getSlotIndex() const {
+    return mSlotIndex;
+}
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/config/RadioConfig.cpp b/radio/aidl/minradio/libminradio/config/RadioConfig.cpp
new file mode 100644
index 0000000..bf89368
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/config/RadioConfig.cpp
@@ -0,0 +1,119 @@
+/*
+ * 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.
+ */
+
+#include <libminradio/config/RadioConfig.h>
+
+#include <libminradio/debug.h>
+#include <libminradio/response.h>
+
+#define RADIO_MODULE "Config"
+
+namespace android::hardware::radio::minimal {
+
+using namespace ::android::hardware::radio::minimal::binder_printing;
+using ::aidl::android::hardware::radio::RadioError;
+using ::ndk::ScopedAStatus;
+namespace aidl = ::aidl::android::hardware::radio::config;
+constexpr auto ok = &ScopedAStatus::ok;
+
+RadioConfig::RadioConfig() {}
+
+ScopedAStatus RadioConfig::getHalDeviceCapabilities(int32_t serial) {
+    LOG_CALL;
+    /* modemReducedFeatureSet1 disables:
+     *  - android.hardware.radio.network.LinkCapacityEstimate.secondaryDownlinkCapacityKbps
+     *  - android.hardware.radio.network.LinkCapacityEstimate.secondaryUplinkCapacityKbps
+     *  - android.hardware.radio.network.IRadioNetwork.setNrDualConnectivityState
+     *  - android.hardware.radio.network.IRadioNetwork.isNrDualConnectivityEnabled
+     *  - android.hardware.radio.data.IRadioData.setDataThrottling
+     *  - android.hardware.radio.data.IRadioData.getSlicingConfig
+     *  - android.hardware.radio.network.IRadioNetworkIndication.currentPhysicalChannelConfigs
+     */
+    respond()->getHalDeviceCapabilitiesResponse(noError(serial), /*modemReducedFeatureSet1*/ true);
+    return ok();
+}
+
+ScopedAStatus RadioConfig::getNumOfLiveModems(int32_t serial) {
+    LOG_CALL;
+    respond()->getNumOfLiveModemsResponse(noError(serial), 1);
+    return ok();
+}
+
+ScopedAStatus RadioConfig::getPhoneCapability(int32_t serial) {
+    LOG_CALL;
+    aidl::PhoneCapability cap{
+            .maxActiveData = 1,
+            .maxActiveInternetData = 1,
+            .isInternetLingeringSupported = false,
+            .logicalModemIds = {0},
+    };
+    respond()->getPhoneCapabilityResponse(noError(serial), cap);
+    return ok();
+}
+
+ScopedAStatus RadioConfig::setNumOfLiveModems(int32_t serial, int8_t numOfLiveModems) {
+    LOG_CALL << numOfLiveModems;
+    if (numOfLiveModems == 1) {
+        respond()->setNumOfLiveModemsResponse(noError(serial));
+    } else {
+        respond()->setNumOfLiveModemsResponse(errorResponse(serial, RadioError::INVALID_ARGUMENTS));
+    }
+    return ok();
+}
+
+ScopedAStatus RadioConfig::setPreferredDataModem(int32_t serial, int8_t modemId) {
+    LOG_CALL_IGNORED << modemId;
+    respond()->setPreferredDataModemResponse(
+            (modemId == 0) ? noError(serial)
+                           : errorResponse(serial, RadioError::INVALID_ARGUMENTS));
+    return ok();
+}
+
+ScopedAStatus RadioConfig::setResponseFunctions(
+        const std::shared_ptr<aidl::IRadioConfigResponse>& response,
+        const std::shared_ptr<aidl::IRadioConfigIndication>& indication) {
+    LOG_CALL_NOSERIAL << response << ' ' << indication;
+    CHECK(response);
+    CHECK(indication);
+    respond = response;
+    indicate = indication;
+    return ok();
+}
+
+ScopedAStatus RadioConfig::setSimSlotsMapping(  //
+        int32_t serial, const std::vector<aidl::SlotPortMapping>& slotMap) {
+    LOG_CALL_IGNORED << slotMap;
+    respond()->setSimSlotsMappingResponse(noError(serial));
+    return ok();
+}
+
+ScopedAStatus RadioConfig::getSimultaneousCallingSupport(int32_t serial) {
+    LOG_CALL;
+    respond()->getSimultaneousCallingSupportResponse(noError(serial), {});
+    return ok();
+}
+
+ScopedAStatus RadioConfig::getSimTypeInfo(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+ScopedAStatus RadioConfig::setSimType(int32_t serial, const std::vector<aidl::SimType>& simTypes) {
+    LOG_NOT_SUPPORTED << simTypes;
+    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/data/RadioData.cpp b/radio/aidl/minradio/libminradio/data/RadioData.cpp
new file mode 100644
index 0000000..a096c82
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/data/RadioData.cpp
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2023 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 <libminradio/data/RadioData.h>
+
+#include <libminradio/debug.h>
+#include <libminradio/response.h>
+
+#include <ranges>
+
+#define RADIO_MODULE "Data"
+
+namespace android::hardware::radio::minimal {
+
+using namespace ::android::hardware::radio::minimal::binder_printing;
+using ::aidl::android::hardware::radio::RadioIndicationType;
+using ::ndk::ScopedAStatus;
+namespace aidl = ::aidl::android::hardware::radio::data;
+constexpr auto ok = &ScopedAStatus::ok;
+
+int32_t RadioData::setupDataCallCid() {
+    return ++mLastDataCallCid;
+}
+
+void RadioData::setupDataCallBase(aidl::SetupDataCallResult dataCall) {
+    {
+        const std::lock_guard<std::mutex> lock(mDataCallListGuard);
+        mDataCallList[dataCall.cid] = dataCall;
+    }
+    indicate()->dataCallListChanged(RadioIndicationType::UNSOLICITED, getDataCallListBase());
+}
+
+void RadioData::deactivateDataCallBase(int32_t cid) {
+    {
+        const std::lock_guard<std::mutex> lock(mDataCallListGuard);
+        auto it = mDataCallList.find(cid);
+        if (it == mDataCallList.end()) return;
+
+        mDataCallList.erase(it);
+    }
+    indicate()->dataCallListChanged(RadioIndicationType::UNSOLICITED, getDataCallListBase());
+}
+
+std::vector<aidl::SetupDataCallResult> RadioData::getDataCallListBase() const {
+    const std::lock_guard<std::mutex> lock(mDataCallListGuard);
+    auto dataCalls = std::views::values(mDataCallList);
+    return {dataCalls.begin(), dataCalls.end()};
+}
+
+ScopedAStatus RadioData::allocatePduSessionId(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+ScopedAStatus RadioData::cancelHandover(int32_t serial, int32_t callId) {
+    LOG_NOT_SUPPORTED << callId;
+    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+ScopedAStatus RadioData::deactivateDataCall(int32_t serial, int32_t cid,
+                                            aidl::DataRequestReason reason) {
+    LOG_CALL_IGNORED << cid << " " << reason;
+    deactivateDataCallBase(cid);
+    respond()->deactivateDataCallResponse(noError(serial));
+    return ok();
+}
+
+ScopedAStatus RadioData::getDataCallList(int32_t serial) {
+    LOG_CALL;
+    respond()->getDataCallListResponse(noError(serial), getDataCallListBase());
+    return ok();
+}
+
+ScopedAStatus RadioData::getSlicingConfig(int32_t serial) {
+    // Disabled with modemReducedFeatureSet1.
+    LOG_NOT_SUPPORTED;
+    respond()->getSlicingConfigResponse(notSupported(serial), {});
+    return ok();
+}
+
+ScopedAStatus RadioData::releasePduSessionId(int32_t serial, int32_t id) {
+    LOG_NOT_SUPPORTED << id;
+    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+ScopedAStatus RadioData::responseAcknowledgement() {
+    LOG_CALL_NOSERIAL;
+    return ok();
+}
+
+ScopedAStatus RadioData::setDataAllowed(int32_t serial, bool allow) {
+    LOG_NOT_SUPPORTED << allow;
+    respond()->setDataAllowedResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioData::setDataProfile(int32_t serial,
+                                        const std::vector<aidl::DataProfileInfo>& profiles) {
+    LOG_CALL_IGNORED << profiles;
+    respond()->setDataProfileResponse(noError(serial));
+    return ok();
+}
+
+ScopedAStatus RadioData::setDataThrottling(int32_t serial, aidl::DataThrottlingAction dta,
+                                           int64_t completionDurationMs) {
+    // Disabled with modemReducedFeatureSet1.
+    LOG_NOT_SUPPORTED << dta << ' ' << completionDurationMs;
+    respond()->setDataThrottlingResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioData::setInitialAttachApn(int32_t serial,
+                                             const std::optional<aidl::DataProfileInfo>& info) {
+    LOG_CALL_IGNORED << info;
+    respond()->setInitialAttachApnResponse(noError(serial));
+    return ok();
+}
+
+ScopedAStatus RadioData::setResponseFunctions(
+        const std::shared_ptr<aidl::IRadioDataResponse>& response,
+        const std::shared_ptr<aidl::IRadioDataIndication>& indication) {
+    LOG_CALL_NOSERIAL << response << ' ' << indication;
+    CHECK(response);
+    CHECK(indication);
+    respond = response;
+    indicate = indication;
+    return ok();
+}
+
+ScopedAStatus RadioData::startHandover(int32_t serial, int32_t callId) {
+    LOG_NOT_SUPPORTED << callId;
+    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+ScopedAStatus RadioData::startKeepalive(int32_t serial, const aidl::KeepaliveRequest& keepalive) {
+    LOG_NOT_SUPPORTED << keepalive;
+    respond()->startKeepaliveResponse(notSupported(serial), {});
+    return ok();
+}
+
+ScopedAStatus RadioData::stopKeepalive(int32_t serial, int32_t sessionHandle) {
+    LOG_NOT_SUPPORTED << sessionHandle;
+    respond()->stopKeepaliveResponse(notSupported(serial));
+    return ok();
+}
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/GuaranteedCallback.h b/radio/aidl/minradio/libminradio/include/libminradio/GuaranteedCallback.h
new file mode 100644
index 0000000..6c5d5eb
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/GuaranteedCallback.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 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 <android-base/logging.h>
+#include <android/binder_interface_utils.h>
+#include <utils/Mutex.h>
+
+namespace android::hardware::radio::minimal {
+
+template <typename Interface, typename DefaultImplementation, bool isIndication = false>
+class GuaranteedCallback {
+    mutable std::mutex mCallbackGuard;
+    std::shared_ptr<Interface> mCallback GUARDED_BY(mCallbackGuard);
+
+  public:
+    GuaranteedCallback<Interface, DefaultImplementation, isIndication>& operator=(
+            const std::shared_ptr<Interface>& callback) {
+        CHECK(callback);
+        const std::lock_guard<std::mutex> lock(mCallbackGuard);
+        mCallback = callback;
+        return *this;
+    }
+
+    std::shared_ptr<Interface> operator()() {
+        const std::lock_guard<std::mutex> lock(mCallbackGuard);
+        if (mCallback) return mCallback;
+
+        LOG(isIndication ? WARNING : ERROR) << "Callback is not set for " << Interface::descriptor;
+        mCallback = ndk::SharedRefBase::make<DefaultImplementation>();
+        return mCallback;
+    }
+
+    operator bool() const { return mCallback != nullptr; }
+};
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/RadioSlotBase.h b/radio/aidl/minradio/libminradio/include/libminradio/RadioSlotBase.h
new file mode 100644
index 0000000..d46357e
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/RadioSlotBase.h
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <libminradio/SlotContext.h>
+
+#include <memory>
+
+namespace android::hardware::radio::minimal {
+
+class RadioSlotBase {
+  protected:
+    std::shared_ptr<SlotContext> mContext;
+
+  public:
+    RadioSlotBase(std::shared_ptr<SlotContext> context);
+};
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/ResponseTracker.h b/radio/aidl/minradio/libminradio/include/libminradio/ResponseTracker.h
new file mode 100644
index 0000000..978d64c
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/ResponseTracker.h
@@ -0,0 +1,192 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <aidl/android/hardware/radio/RadioError.h>
+#include <aidl/android/hardware/radio/RadioResponseInfo.h>
+#include <android-base/logging.h>
+#include <android-base/thread_annotations.h>
+#include <android/binder_auto_utils.h>
+#include <libminradio/binder_printing.h>
+
+#include <map>
+#include <memory>
+
+namespace android::hardware::radio::minimal {
+
+class ResponseTrackerResultBase {
+  private:
+    const char* mDescriptor;
+    ::aidl::android::hardware::radio::RadioError mError;
+
+    static ::aidl::android::hardware::radio::RadioError toError(const ::ndk::ScopedAStatus& status);
+
+  protected:
+    ResponseTrackerResultBase(const char* descriptor);
+    ResponseTrackerResultBase(const char* descriptor,
+                              ::aidl::android::hardware::radio::RadioError error);
+    ResponseTrackerResultBase(const char* descriptor, ::ndk::ScopedAStatus st);
+
+  public:
+    virtual ~ResponseTrackerResultBase() = default;
+
+    bool isOk() const;
+    bool expectOk() const;
+    ::aidl::android::hardware::radio::RadioError getError() const;
+    const char* getDescriptor() const;
+};
+
+template <typename ResultData>
+class ResponseTrackerResult : public ResponseTrackerResultBase {
+  private:
+    ResultData mResultData;
+
+  public:
+    ResponseTrackerResult() : ResponseTrackerResultBase(ResultData::descriptor) {}
+    ResponseTrackerResult(::aidl::android::hardware::radio::RadioError error)
+        : ResponseTrackerResultBase(ResultData::descriptor, error) {}
+    ResponseTrackerResult(::ndk::ScopedAStatus st)
+        : ResponseTrackerResultBase(ResultData::descriptor, std::move(st)) {}
+    ResponseTrackerResult(ResultData data)
+        : ResponseTrackerResultBase(ResultData::descriptor,
+                                    ::aidl::android::hardware::radio::RadioError::NONE),
+          mResultData(data) {}
+
+    const ResultData& get() const {
+        CHECK(expectOk()) << "Request failed";
+        return mResultData;
+    }
+    const ResultData& operator*() const { return get(); }
+    const ResultData* operator->() const { return &get(); }
+};
+
+template <typename ResultData>
+std::ostream& operator<<(std::ostream& os, const ResponseTrackerResult<ResultData>& val) {
+    using namespace ::android::hardware::radio::minimal::binder_printing;
+    if (val.isOk()) {
+        return os << *val;
+    } else {
+        return os << "ResponseTrackerResult<" << val.getDescriptor()  //
+                  << ">{error=" << val.getError() << "}";
+    }
+}
+
+class ResponseTrackerBase {
+  protected:
+    class ScopedSerial;
+
+  private:
+    mutable std::mutex mSerialsGuard;
+    int32_t mSerial GUARDED_BY(mSerialsGuard) = initialSerial();
+    std::map<int32_t, std::unique_ptr<ResponseTrackerResultBase>> mTrackedSerials
+            GUARDED_BY(mSerialsGuard);
+
+    static int32_t initialSerial();
+    ::ndk::ScopedAStatus handle(const ::aidl::android::hardware::radio::RadioResponseInfo& info,
+                                std::unique_ptr<ResponseTrackerResultBase> result);
+    std::unique_ptr<ResponseTrackerResultBase> getResultBase(ScopedSerial& serial);
+
+  protected:
+    class ScopedSerial {
+      private:
+        int32_t mSerial;
+        bool mIsReleased = false;
+
+        /* Raw pointer to allow ResponseTrackerBase self-reference. DISALLOW_COPY_AND_ASSIGN and
+         * protected status of newSerial ensures ScopedSerial won't outlive mTracker. */
+        ResponseTrackerBase* mTracker;
+
+        DISALLOW_COPY_AND_ASSIGN(ScopedSerial);
+
+      public:
+        ScopedSerial(int32_t serial, ResponseTrackerBase* tracker);
+        ~ScopedSerial();
+        operator int32_t() const;
+        void release();
+    };
+
+    ScopedSerial newSerial();
+    bool isTracked(int32_t serial) const;
+    void cancelTracking(ScopedSerial& serial);
+
+    template <typename ResultData>
+    ::ndk::ScopedAStatus handle(const ::aidl::android::hardware::radio::RadioResponseInfo& info,
+                                const ResultData& data) {
+        std::unique_ptr<ResponseTrackerResultBase> result =
+                std::make_unique<ResponseTrackerResult<ResultData>>(data);
+        return handle(info, std::move(result));
+    }
+
+    template <typename ResultData>
+    ResponseTrackerResult<ResultData> getResult(ScopedSerial& serial) {
+        auto baseResult = getResultBase(serial);
+        if (!baseResult) return {};
+        CHECK(baseResult->getDescriptor() == ResultData::descriptor)
+                << "Failed to get ResponseTracker result. Expected " << ResultData::descriptor
+                << ", but got " << baseResult->getDescriptor();
+        return static_cast<ResponseTrackerResult<ResultData>&>(*baseResult);
+    }
+};
+
+template <typename RequestInterface, typename ResponseInterface>
+class ResponseTracker : public ResponseInterface::DefaultDelegator, protected ResponseTrackerBase {
+  private:
+    std::weak_ptr<RequestInterface> mRequest;
+
+  protected:
+    std::shared_ptr<RequestInterface> request() {
+        auto req = mRequest.lock();
+        CHECK(req) << "request() should only be called from RequestInterface context! "
+                   << "Failing this check means RequestInterface has been free'd.";
+        return req;
+    }
+
+  public:
+    ResponseTracker(std::shared_ptr<RequestInterface> req,
+                    const std::shared_ptr<ResponseInterface>& resp)
+        : ResponseInterface::DefaultDelegator(resp), mRequest(req) {}
+};
+
+template <typename ResponseTrackerT>
+class ResponseTrackerHolder {
+  private:
+    mutable std::mutex mResponseTrackerGuard;
+    std::shared_ptr<ResponseTrackerT> mTracker GUARDED_BY(mResponseTrackerGuard);
+
+  public:
+    operator bool() const {
+        std::unique_lock lck(mResponseTrackerGuard);
+        return mTracker != nullptr;
+    }
+
+    ResponseTrackerHolder& operator=(std::shared_ptr<ResponseTrackerT> tracker) {
+        std::unique_lock lck(mResponseTrackerGuard);
+        mTracker = std::move(tracker);
+        return *this;
+    }
+
+    std::shared_ptr<ResponseTrackerT> operator()() const {
+        std::unique_lock lck(mResponseTrackerGuard);
+        return mTracker;
+    }
+
+    std::shared_ptr<ResponseTrackerT> get() const {
+        std::unique_lock lck(mResponseTrackerGuard);
+        return mTracker;
+    }
+};
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/SlotContext.h b/radio/aidl/minradio/libminradio/include/libminradio/SlotContext.h
new file mode 100644
index 0000000..bc6f61e
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/SlotContext.h
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+#pragma once
+
+namespace android::hardware::radio::minimal {
+
+class SlotContext {
+  public:
+    SlotContext(unsigned slotIndex);
+
+    unsigned getSlotIndex() const;
+
+  private:
+    unsigned mSlotIndex;
+};
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/binder_printing.h b/radio/aidl/minradio/libminradio/include/libminradio/binder_printing.h
new file mode 100644
index 0000000..c583b3b
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/binder_printing.h
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <ostream>
+
+namespace android::hardware::radio::minimal::binder_printing {
+
+namespace details {
+
+template <typename _T>
+class LooksLikeBinderStruct {
+    template <typename _U>
+    static auto _test(int) -> decltype(std::declval<_U>().writeToParcel(nullptr), std::true_type());
+    template <typename _U>
+    static std::false_type _test(...);
+
+  public:
+    enum { value = decltype(_test<_T>(0))::value };
+};
+
+template <typename _T>
+class HasToStringMethod {
+    template <typename _U>
+    static auto _test(int) -> decltype(std::declval<_U>().toString(), std::true_type());
+    template <typename _U>
+    static std::false_type _test(...);
+
+  public:
+    enum { value = decltype(_test<_T>(0))::value };
+};
+
+template <typename _T>
+class HasToStringFunction {
+    template <typename _U>
+    static auto _test(int) -> decltype(toString(std::declval<_U>()), std::true_type());
+    template <typename _U>
+    static std::false_type _test(...);
+
+  public:
+    enum { value = decltype(_test<_T>(0))::value };
+};
+
+}  // namespace details
+
+template <typename T, typename = std::enable_if_t<details::LooksLikeBinderStruct<T>::value &&
+                                                  details::HasToStringMethod<T>::value>>
+std::ostream& operator<<(std::ostream& os, const T& val) {
+    return os << val.toString();
+}
+
+template <typename T, typename = std::enable_if_t<details::LooksLikeBinderStruct<T>::value &&
+                                                  details::HasToStringMethod<T>::value>>
+std::ostream& operator<<(std::ostream& os, const std::optional<T>& val) {
+    if (!val.has_value()) return os << "nullopt";
+    return os << *val;
+}
+
+template <typename T,
+          typename = std::enable_if_t<std::is_enum<T>::value &&
+                                      details::HasToStringFunction<T>::value>,
+          typename = void>
+std::ostream& operator<<(std::ostream& os, T val) {
+    return os << toString(val);
+}
+
+template <typename T, typename = std::enable_if_t<
+                              (details::LooksLikeBinderStruct<T>::value &&
+                               details::HasToStringMethod<T>::value) ||
+                              (std::is_enum<T>::value && details::HasToStringFunction<T>::value) ||
+                              std::is_same_v<T, int32_t> || std::is_same_v<T, std::string>>>
+std::ostream& operator<<(std::ostream& os, const std::vector<T>& val) {
+    os << '[';
+    bool first = true;
+    for (auto&& el : val) {
+        if (first) {
+            first = false;
+        } else {
+            os << ", ";
+        }
+        os << el;
+    }
+    return os << ']';
+}
+
+}  // namespace android::hardware::radio::minimal::binder_printing
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/config/RadioConfig.h b/radio/aidl/minradio/libminradio/include/libminradio/config/RadioConfig.h
new file mode 100644
index 0000000..16f0ca2
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/config/RadioConfig.h
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <libminradio/GuaranteedCallback.h>
+
+#include <aidl/android/hardware/radio/config/BnRadioConfig.h>
+
+namespace android::hardware::radio::minimal {
+
+class RadioConfig : public aidl::android::hardware::radio::config::BnRadioConfig {
+  public:
+    RadioConfig();
+
+  protected:
+    ::ndk::ScopedAStatus getHalDeviceCapabilities(int32_t serial) override;
+    ::ndk::ScopedAStatus getNumOfLiveModems(int32_t serial) override;
+    ::ndk::ScopedAStatus getPhoneCapability(int32_t serial) override;
+    ::ndk::ScopedAStatus setNumOfLiveModems(int32_t serial, int8_t numOfLiveModems) override;
+    ::ndk::ScopedAStatus setPreferredDataModem(int32_t serial, int8_t modemId) override;
+    ::ndk::ScopedAStatus setResponseFunctions(
+            const std::shared_ptr<aidl::android::hardware::radio::config::IRadioConfigResponse>&
+                    response,
+            const std::shared_ptr<aidl::android::hardware::radio::config::IRadioConfigIndication>&
+                    indication) override;
+    ::ndk::ScopedAStatus setSimSlotsMapping(
+            int32_t serial,
+            const std::vector<aidl::android::hardware::radio::config::SlotPortMapping>& slotMap)
+            override;
+    ::ndk::ScopedAStatus getSimultaneousCallingSupport(int32_t serial) override;
+    ::ndk::ScopedAStatus getSimTypeInfo(int32_t serial) override;
+    ::ndk::ScopedAStatus setSimType(
+            int32_t serial,
+            const std::vector<::aidl::android::hardware::radio::config::SimType>& simTypes)
+            override;
+
+    GuaranteedCallback<::aidl::android::hardware::radio::config::IRadioConfigIndication,
+                       ::aidl::android::hardware::radio::config::IRadioConfigIndicationDefault,
+                       true>
+            indicate;
+    GuaranteedCallback<::aidl::android::hardware::radio::config::IRadioConfigResponse,
+                       ::aidl::android::hardware::radio::config::IRadioConfigResponseDefault>
+            respond;
+};
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/data/RadioData.h b/radio/aidl/minradio/libminradio/include/libminradio/data/RadioData.h
new file mode 100644
index 0000000..2da71a2
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/data/RadioData.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 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 <libminradio/GuaranteedCallback.h>
+#include <libminradio/RadioSlotBase.h>
+
+#include <aidl/android/hardware/radio/data/BnRadioData.h>
+
+#include <map>
+
+namespace android::hardware::radio::minimal {
+
+class RadioData : public RadioSlotBase, public aidl::android::hardware::radio::data::BnRadioData {
+  public:
+    using RadioSlotBase::RadioSlotBase;
+
+  protected:
+    int32_t setupDataCallCid();
+    void setupDataCallBase(aidl::android::hardware::radio::data::SetupDataCallResult dataCall);
+    void deactivateDataCallBase(int32_t cid);
+    std::vector<aidl::android::hardware::radio::data::SetupDataCallResult> getDataCallListBase()
+            const;
+
+    ::ndk::ScopedAStatus allocatePduSessionId(int32_t serial) override;
+    ::ndk::ScopedAStatus cancelHandover(int32_t serial, int32_t callId) override;
+    ::ndk::ScopedAStatus deactivateDataCall(
+            int32_t serial, int32_t cid,
+            ::aidl::android::hardware::radio::data::DataRequestReason reason) override;
+    ::ndk::ScopedAStatus getDataCallList(int32_t serial) override;
+    ::ndk::ScopedAStatus getSlicingConfig(int32_t serial) override;
+    ::ndk::ScopedAStatus releasePduSessionId(int32_t serial, int32_t id) override;
+    ::ndk::ScopedAStatus responseAcknowledgement() override;
+    ::ndk::ScopedAStatus setDataAllowed(int32_t serial, bool allow) override;
+    ::ndk::ScopedAStatus setDataProfile(
+            int32_t serial,
+            const std::vector<::aidl::android::hardware::radio::data::DataProfileInfo>& profiles)
+            override;
+    ::ndk::ScopedAStatus setDataThrottling(
+            int32_t serial,
+            ::aidl::android::hardware::radio::data::DataThrottlingAction dataThrottlingAction,
+            int64_t completionDurationMillis) override;
+    ::ndk::ScopedAStatus setInitialAttachApn(
+            int32_t serial,
+            const std::optional<::aidl::android::hardware::radio::data::DataProfileInfo>& dpInfo)
+            override;
+    ::ndk::ScopedAStatus setResponseFunctions(
+            const std::shared_ptr<::aidl::android::hardware::radio::data::IRadioDataResponse>&
+                    radioDataResponse,
+            const std::shared_ptr<::aidl::android::hardware::radio::data::IRadioDataIndication>&
+                    radioDataIndication) override;
+    ::ndk::ScopedAStatus startHandover(int32_t serial, int32_t callId) override;
+    ::ndk::ScopedAStatus startKeepalive(
+            int32_t serial,
+            const ::aidl::android::hardware::radio::data::KeepaliveRequest& keepalive) override;
+    ::ndk::ScopedAStatus stopKeepalive(int32_t serial, int32_t sessionHandle) override;
+
+    GuaranteedCallback<::aidl::android::hardware::radio::data::IRadioDataIndication,
+                       ::aidl::android::hardware::radio::data::IRadioDataIndicationDefault, true>
+            indicate;
+    GuaranteedCallback<::aidl::android::hardware::radio::data::IRadioDataResponse,
+                       ::aidl::android::hardware::radio::data::IRadioDataResponseDefault>
+            respond;
+
+  private:
+    int32_t mLastDataCallCid = 0;
+    mutable std::mutex mDataCallListGuard;
+    std::map<int32_t, ::aidl::android::hardware::radio::data::SetupDataCallResult> mDataCallList
+            GUARDED_BY(mDataCallListGuard);
+};
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/debug.h b/radio/aidl/minradio/libminradio/include/libminradio/debug.h
new file mode 100644
index 0000000..9646aca
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/debug.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 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 "binder_printing.h"
+
+#include <android-base/logging.h>
+
+namespace android::hardware::radio::minimal::debug {
+
+static constexpr bool kSuperVerbose = true;
+static constexpr bool kSuperCrazyVerbose = false;
+
+// clang-format off
+#define LOG_CALL_ALWAYS \
+    LOG(VERBOSE) << '[' << serial << ("] " RADIO_MODULE ".") << __func__ << ' '
+
+#define LOG_CALL                                                             \
+    if constexpr (::android::hardware::radio::minimal::debug::kSuperVerbose) \
+        LOG_CALL_ALWAYS
+
+#define LOG_CALL_RESPONSE                                                    \
+    if constexpr (::android::hardware::radio::minimal::debug::kSuperCrazyVerbose) \
+        LOG(VERBOSE) << '[' << info.serial << ("] " RADIO_MODULE ".") << __func__ << ' '
+
+#define LOG_CALL_NOSERIAL                                                    \
+    if constexpr (::android::hardware::radio::minimal::debug::kSuperVerbose) \
+        LOG(VERBOSE) << (RADIO_MODULE ".") << __func__ << ' '
+// clang-format on
+
+/**
+ * Logs calls implemented to pretend doing the right thing, but doing nothing instead.
+ */
+#define LOG_CALL_IGNORED LOG_CALL_ALWAYS << "(ignored) "
+
+/**
+ * Logs calls always responding with REQUEST_NOT_SUPPORTED error.
+ */
+#define LOG_NOT_SUPPORTED LOG_CALL_ALWAYS << "(not supported) "
+
+/**
+ * Logs calls to deprecated methods. They should be never called by the framework nor xTS.
+ */
+#define LOG_AND_RETURN_DEPRECATED()                                                          \
+    LOG(ERROR) << '[' << serial << ("] " RADIO_MODULE ".") << __func__ << " (deprecated!) "; \
+    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION)
+
+}  // namespace android::hardware::radio::minimal::debug
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/modem/RadioModem.h b/radio/aidl/minradio/libminradio/include/libminradio/modem/RadioModem.h
new file mode 100644
index 0000000..739cd8c
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/modem/RadioModem.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 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 <libminradio/GuaranteedCallback.h>
+#include <libminradio/RadioSlotBase.h>
+#include <libminradio/modem/RadioModemResponseTracker.h>
+
+#include <aidl/android/hardware/radio/modem/BnRadioModem.h>
+
+namespace android::hardware::radio::minimal {
+
+class RadioModem : public RadioSlotBase,
+                   public aidl::android::hardware::radio::modem::BnRadioModem {
+  public:
+    RadioModem(std::shared_ptr<SlotContext> context,
+               std::vector<aidl::android::hardware::radio::RadioTechnology> rats);
+
+  protected:
+    ::ndk::ScopedAStatus enableModem(int32_t serial, bool on) override;
+    ::ndk::ScopedAStatus getBasebandVersion(int32_t serial) override;
+    ::ndk::ScopedAStatus getDeviceIdentity(int32_t serial) override;
+    ::ndk::ScopedAStatus getHardwareConfig(int32_t serial) override;
+    ::ndk::ScopedAStatus getModemActivityInfo(int32_t serial) override;
+    ::ndk::ScopedAStatus getModemStackStatus(int32_t serial) override;
+    ::ndk::ScopedAStatus getRadioCapability(int32_t serial) override;
+    ::ndk::ScopedAStatus nvReadItem(
+            int32_t serial, ::aidl::android::hardware::radio::modem::NvItem itemId) override;
+    ::ndk::ScopedAStatus nvResetConfig(
+            int32_t serial, ::aidl::android::hardware::radio::modem::ResetNvType type) override;
+    ::ndk::ScopedAStatus nvWriteCdmaPrl(int32_t serial, const std::vector<uint8_t>& prl) override;
+    ::ndk::ScopedAStatus nvWriteItem(
+            int32_t serial, const ::aidl::android::hardware::radio::modem::NvWriteItem& i) override;
+    ::ndk::ScopedAStatus requestShutdown(int32_t serial) override;
+    ::ndk::ScopedAStatus responseAcknowledgement() override;
+    ::ndk::ScopedAStatus sendDeviceState(
+            int32_t serial, ::aidl::android::hardware::radio::modem::DeviceStateType stateType,
+            bool state) override;
+    ::ndk::ScopedAStatus setRadioCapability(
+            int32_t s, const ::aidl::android::hardware::radio::modem::RadioCapability& rc) override;
+    ::ndk::ScopedAStatus setRadioPower(int32_t serial, bool powerOn, bool forEmergencyCall,
+                                       bool preferredForEmergencyCall) override;
+    ::ndk::ScopedAStatus setResponseFunctions(
+            const std::shared_ptr<::aidl::android::hardware::radio::modem::IRadioModemResponse>&
+                    radioModemResponse,
+            const std::shared_ptr<::aidl::android::hardware::radio::modem::IRadioModemIndication>&
+                    radioModemIndication) override;
+
+    GuaranteedCallback<::aidl::android::hardware::radio::modem::IRadioModemIndication,
+                       ::aidl::android::hardware::radio::modem::IRadioModemIndicationDefault, true>
+            indicate;
+    GuaranteedCallback<::aidl::android::hardware::radio::modem::IRadioModemResponse,
+                       ::aidl::android::hardware::radio::modem::IRadioModemResponseDefault>
+            respond;
+
+  private:
+    std::shared_ptr<RadioModemResponseTracker> mResponseTracker;
+    int32_t mRatBitmap;
+
+    std::string getModemUuid() const;
+    std::string getSimUuid() const;
+};
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/modem/RadioModemResponseTracker.h b/radio/aidl/minradio/libminradio/include/libminradio/modem/RadioModemResponseTracker.h
new file mode 100644
index 0000000..e6e7b9e
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/modem/RadioModemResponseTracker.h
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <libminradio/ResponseTracker.h>
+
+#include <aidl/android/hardware/radio/modem/BnRadioModemResponse.h>
+#include <aidl/android/hardware/radio/modem/IRadioModem.h>
+
+namespace android::hardware::radio::minimal {
+
+class RadioModemResponseTracker
+    : public ResponseTracker<::aidl::android::hardware::radio::modem::IRadioModem,
+                             ::aidl::android::hardware::radio::modem::IRadioModemResponse> {
+  public:
+    RadioModemResponseTracker(
+            std::shared_ptr<::aidl::android::hardware::radio::modem::IRadioModem> req,
+            const std::shared_ptr<::aidl::android::hardware::radio::modem::IRadioModemResponse>&
+                    resp);
+
+    // TODO(now): remove if not needed
+    ResponseTrackerResult<::aidl::android::hardware::radio::modem::ImeiInfo> getImei();
+
+  protected:
+    ::ndk::ScopedAStatus getImeiResponse(
+            const ::aidl::android::hardware::radio::RadioResponseInfo& info,
+            const std::optional<::aidl::android::hardware::radio::modem::ImeiInfo>& imei) override;
+};
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/network/RadioNetwork.h b/radio/aidl/minradio/libminradio/include/libminradio/network/RadioNetwork.h
new file mode 100644
index 0000000..4d3505a
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/network/RadioNetwork.h
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2023 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 <libminradio/GuaranteedCallback.h>
+#include <libminradio/RadioSlotBase.h>
+#include <libminradio/network/RadioNetworkResponseTracker.h>
+
+#include <aidl/android/hardware/radio/network/BnRadioNetwork.h>
+
+namespace android::hardware::radio::minimal {
+
+class RadioNetwork : public RadioSlotBase,
+                     public aidl::android::hardware::radio::network::BnRadioNetwork {
+  public:
+    using RadioSlotBase::RadioSlotBase;
+
+  protected:
+    std::vector<::aidl::android::hardware::radio::network::CellInfo> getCellInfoListBase();
+
+    ::ndk::ScopedAStatus getAllowedNetworkTypesBitmap(int32_t serial) override;
+    ::ndk::ScopedAStatus getAvailableBandModes(int32_t serial) override;
+    ::ndk::ScopedAStatus getAvailableNetworks(int32_t serial) override;
+    ::ndk::ScopedAStatus getBarringInfo(int32_t serial) override;
+    ::ndk::ScopedAStatus getCdmaRoamingPreference(int32_t serial) override;
+    ::ndk::ScopedAStatus getCellInfoList(int32_t serial) override;
+    ::ndk::ScopedAStatus getImsRegistrationState(int32_t serial) override;
+    ::ndk::ScopedAStatus getNetworkSelectionMode(int32_t serial) override;
+    ::ndk::ScopedAStatus getOperator(int32_t serial) override;
+    ::ndk::ScopedAStatus getSystemSelectionChannels(int32_t serial) override;
+    ::ndk::ScopedAStatus getVoiceRadioTechnology(int32_t serial) override;
+    ::ndk::ScopedAStatus getVoiceRegistrationState(int32_t serial) override;
+    ::ndk::ScopedAStatus isNrDualConnectivityEnabled(int32_t serial) override;
+    ::ndk::ScopedAStatus responseAcknowledgement() override;
+    ::ndk::ScopedAStatus setAllowedNetworkTypesBitmap(int32_t serial,
+                                                      int32_t networkTypeBitmap) override;
+    ::ndk::ScopedAStatus setBandMode(
+            int32_t serial, ::aidl::android::hardware::radio::network::RadioBandMode mode) override;
+    ::ndk::ScopedAStatus setBarringPassword(int32_t serial, const std::string& facility,
+                                            const std::string& oldPassword,
+                                            const std::string& newPassword) override;
+    ::ndk::ScopedAStatus setCdmaRoamingPreference(
+            int32_t serial,
+            ::aidl::android::hardware::radio::network::CdmaRoamingType type) override;
+    ::ndk::ScopedAStatus setCellInfoListRate(int32_t serial, int32_t rate) override;
+    ::ndk::ScopedAStatus setIndicationFilter(int32_t serial, int32_t indicationFilter) override;
+    ::ndk::ScopedAStatus setLinkCapacityReportingCriteria(
+            int32_t serial, int32_t hysteresisMs, int32_t hysteresisDlKbps,
+            int32_t hysteresisUlKbps, const std::vector<int32_t>& thresholdsDownlinkKbps,
+            const std::vector<int32_t>& thresholdsUplinkKbps,
+            ::aidl::android::hardware::radio::AccessNetwork accessNetwork) override;
+    ::ndk::ScopedAStatus setLocationUpdates(int32_t serial, bool enable) override;
+    ::ndk::ScopedAStatus setNetworkSelectionModeAutomatic(int32_t serial) override;
+    ::ndk::ScopedAStatus setNetworkSelectionModeManual(
+            int32_t serial, const std::string& operatorNumeric,
+            ::aidl::android::hardware::radio::AccessNetwork ran) override;
+    ::ndk::ScopedAStatus setNrDualConnectivityState(
+            int32_t serial,
+            ::aidl::android::hardware::radio::network::NrDualConnectivityState nrSt) override;
+    ::ndk::ScopedAStatus setResponseFunctions(
+            const std::shared_ptr<::aidl::android::hardware::radio::network::IRadioNetworkResponse>&
+                    radioNetworkResponse,
+            const std::shared_ptr<
+                    ::aidl::android::hardware::radio::network::IRadioNetworkIndication>&
+                    radioNetworkIndication) override;
+    ::ndk::ScopedAStatus setSignalStrengthReportingCriteria(
+            int32_t serial,
+            const std::vector<::aidl::android::hardware::radio::network::SignalThresholdInfo>&
+                    signalThresholdInfos) override;
+    ::ndk::ScopedAStatus setSuppServiceNotifications(int32_t serial, bool enable) override;
+    ::ndk::ScopedAStatus setSystemSelectionChannels(
+            int32_t serial, bool specifyChannels,
+            const std::vector<::aidl::android::hardware::radio::network::RadioAccessSpecifier>&
+                    specifiers) override;
+    ::ndk::ScopedAStatus startNetworkScan(
+            int32_t serial,
+            const ::aidl::android::hardware::radio::network::NetworkScanRequest& request) override;
+    ::ndk::ScopedAStatus stopNetworkScan(int32_t serial) override;
+    ::ndk::ScopedAStatus supplyNetworkDepersonalization(int32_t serial,
+                                                        const std::string& netPin) override;
+    ::ndk::ScopedAStatus setUsageSetting(
+            int32_t serial,
+            ::aidl::android::hardware::radio::network::UsageSetting usageSetting) override;
+    ::ndk::ScopedAStatus getUsageSetting(int32_t serial) override;
+
+    ::ndk::ScopedAStatus setEmergencyMode(
+            int32_t serial,
+            const ::aidl::android::hardware::radio::network::EmergencyMode emergencyMode) override;
+    ::ndk::ScopedAStatus triggerEmergencyNetworkScan(
+            int32_t serial,
+            const ::aidl::android::hardware::radio::network::EmergencyNetworkScanTrigger&
+                    scanTrigger) override;
+    ::ndk::ScopedAStatus cancelEmergencyNetworkScan(int32_t serial, bool resetScan) override;
+    ::ndk::ScopedAStatus exitEmergencyMode(int32_t serial) override;
+    ::ndk::ScopedAStatus setNullCipherAndIntegrityEnabled(int32_t serial, bool enabled) override;
+    ::ndk::ScopedAStatus isNullCipherAndIntegrityEnabled(int32_t serial) override;
+    ::ndk::ScopedAStatus isN1ModeEnabled(int32_t serial) override;
+    ::ndk::ScopedAStatus setN1ModeEnabled(int32_t serial, bool enable) override;
+    ::ndk::ScopedAStatus isCellularIdentifierTransparencyEnabled(int32_t serial) override;
+    ::ndk::ScopedAStatus setCellularIdentifierTransparencyEnabled(int32_t serial,
+                                                                  bool enabled) override;
+    ::ndk::ScopedAStatus setSecurityAlgorithmsUpdatedEnabled(int32_t serial, bool enabled) override;
+    ::ndk::ScopedAStatus isSecurityAlgorithmsUpdatedEnabled(int32_t serial) override;
+    ::ndk::ScopedAStatus setSatellitePlmn(
+            int32_t in_serial, const std::vector<std::string>& carrierPlmnArray,
+            const std::vector<std::string>& allSatellitePlmnArray) override;
+    ::ndk::ScopedAStatus setSatelliteEnabledForCarrier(int32_t serial,
+                                                       bool satelliteEnabled) override;
+    ::ndk::ScopedAStatus isSatelliteEnabledForCarrier(int32_t serial) override;
+
+    GuaranteedCallback<::aidl::android::hardware::radio::network::IRadioNetworkIndication,
+                       ::aidl::android::hardware::radio::network::IRadioNetworkIndicationDefault,
+                       true>
+            indicate;
+    GuaranteedCallback<::aidl::android::hardware::radio::network::IRadioNetworkResponse,
+                       ::aidl::android::hardware::radio::network::IRadioNetworkResponseDefault>
+            respond;
+
+  private:
+    int32_t mAllowedNetworkTypesBitmap = std::numeric_limits<int32_t>::max();
+
+    ResponseTrackerHolder<RadioNetworkResponseTracker> mResponseTracker;
+};
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/network/RadioNetworkResponseTracker.h b/radio/aidl/minradio/libminradio/include/libminradio/network/RadioNetworkResponseTracker.h
new file mode 100644
index 0000000..2978cd8
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/network/RadioNetworkResponseTracker.h
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <libminradio/ResponseTracker.h>
+
+#include <aidl/android/hardware/radio/network/BnRadioNetworkResponse.h>
+#include <aidl/android/hardware/radio/network/IRadioNetwork.h>
+
+namespace android::hardware::radio::minimal {
+
+class RadioNetworkResponseTracker
+    : public ResponseTracker<::aidl::android::hardware::radio::network::IRadioNetwork,
+                             ::aidl::android::hardware::radio::network::IRadioNetworkResponse> {
+  public:
+    RadioNetworkResponseTracker(
+            std::shared_ptr<::aidl::android::hardware::radio::network::IRadioNetwork> req,
+            const std::shared_ptr<::aidl::android::hardware::radio::network::IRadioNetworkResponse>&
+                    resp);
+
+    ResponseTrackerResult<::aidl::android::hardware::radio::network::RegStateResult>
+    getDataRegistrationState();
+    ResponseTrackerResult<::aidl::android::hardware::radio::network::SignalStrength>
+    getSignalStrength();
+
+  protected:
+    ::ndk::ScopedAStatus getDataRegistrationStateResponse(
+            const ::aidl::android::hardware::radio::RadioResponseInfo& info,
+            const ::aidl::android::hardware::radio::network::RegStateResult& dataRegResp) override;
+    ::ndk::ScopedAStatus getSignalStrengthResponse(
+            const ::aidl::android::hardware::radio::RadioResponseInfo& info,
+            const ::aidl::android::hardware::radio::network::SignalStrength& signalStrength)
+            override;
+};
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/network/structs.h b/radio/aidl/minradio/libminradio/include/libminradio/network/structs.h
new file mode 100644
index 0000000..4410924
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/network/structs.h
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <aidl/android/hardware/radio/network/CellInfo.h>
+#include <aidl/android/hardware/radio/network/RegStateResult.h>
+#include <aidl/android/hardware/radio/network/SignalStrength.h>
+#include <aidl/android/hardware/radio/network/SignalThresholdInfo.h>
+
+namespace android::hardware::radio::minimal::structs {
+
+::aidl::android::hardware::radio::network::SignalStrength makeSignalStrength();
+::aidl::android::hardware::radio::network::CellInfo makeCellInfo(
+        const ::aidl::android::hardware::radio::network::RegStateResult& regState,
+        const ::aidl::android::hardware::radio::network::SignalStrength& signalStrength);
+
+::aidl::android::hardware::radio::network::OperatorInfo getOperatorInfo(
+        const ::aidl::android::hardware::radio::network::CellIdentity& cellIdentity);
+
+int32_t rssiToSignalStrength(int32_t rssi);
+int32_t validateRsrp(int32_t rsrp);
+int32_t validateRsrq(int32_t rsrq);
+bool validateSignalThresholdInfos(
+        const std::vector<::aidl::android::hardware::radio::network::SignalThresholdInfo>& infos);
+
+}  // namespace android::hardware::radio::minimal::structs
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/response.h b/radio/aidl/minradio/libminradio/include/libminradio/response.h
new file mode 100644
index 0000000..5692628
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/response.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 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 <aidl/android/hardware/radio/RadioResponseInfo.h>
+
+namespace android::hardware::radio::minimal {
+
+aidl::android::hardware::radio::RadioResponseInfo noError(int32_t serial);
+aidl::android::hardware::radio::RadioResponseInfo notSupported(int32_t serial);
+aidl::android::hardware::radio::RadioResponseInfo errorResponse(
+        int32_t serial, aidl::android::hardware::radio::RadioError error);
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/sim/App.h b/radio/aidl/minradio/libminradio/include/libminradio/sim/App.h
new file mode 100644
index 0000000..9f0eebe
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/sim/App.h
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <aidl/android/hardware/radio/sim/IccIo.h>
+#include <aidl/android/hardware/radio/sim/IccIoResult.h>
+#include <aidl/android/hardware/radio/sim/SimApdu.h>
+#include <android-base/macros.h>
+
+namespace android::hardware::radio::minimal::sim {
+
+class App {
+  public:
+    class Channel {
+      public:
+        Channel(uint8_t channelId);
+        virtual ~Channel() = default;
+
+        uint8_t getId() const;
+        std::vector<uint8_t> getSelectResponse() const;
+
+        virtual ::aidl::android::hardware::radio::sim::IccIoResult transmit(
+                const ::aidl::android::hardware::radio::sim::SimApdu& message) = 0;
+
+      private:
+        uint8_t mChannelId;
+
+        DISALLOW_COPY_AND_ASSIGN(Channel);
+    };
+
+    virtual ~App() = default;
+
+    std::string_view getAid() const;
+
+    virtual std::shared_ptr<Channel> newChannel(int32_t id) = 0;
+
+    virtual ::aidl::android::hardware::radio::sim::IccIoResult iccIo(
+            const ::aidl::android::hardware::radio::sim::IccIo& iccIo);
+
+  protected:
+    App(std::string_view aid);
+
+  private:
+    std::string mAid;
+
+    DISALLOW_COPY_AND_ASSIGN(App);
+};
+
+}  // namespace android::hardware::radio::minimal::sim
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/sim/AppManager.h b/radio/aidl/minradio/libminradio/include/libminradio/sim/AppManager.h
new file mode 100644
index 0000000..c142fb9
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/sim/AppManager.h
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <aidl/android/hardware/radio/RadioError.h>
+#include <aidl/android/hardware/radio/sim/IccIo.h>
+#include <android-base/macros.h>
+#include <libminradio/sim/App.h>
+
+#include <map>
+
+namespace android::hardware::radio::minimal::sim {
+
+class AppManager {
+  public:
+    AppManager();
+
+    void addApp(std::shared_ptr<App> app);
+
+    std::pair<::aidl::android::hardware::radio::RadioError, std::shared_ptr<App::Channel>>
+    openLogicalChannel(std::string_view aid, int32_t p2);
+    ::aidl::android::hardware::radio::RadioError closeLogicalChannel(int32_t channelId);
+
+    ::aidl::android::hardware::radio::sim::IccIoResult transmit(
+            const ::aidl::android::hardware::radio::sim::SimApdu& message);
+    ::aidl::android::hardware::radio::sim::IccIoResult iccIo(
+            const ::aidl::android::hardware::radio::sim::IccIo& iccIo);
+
+  private:
+    std::map<std::string, std::shared_ptr<App>, std::less<>> mApps;
+    mutable std::mutex mChannelsGuard;
+    std::map<int32_t, std::shared_ptr<App::Channel>> mChannels;
+
+    ::aidl::android::hardware::radio::sim::IccIoResult commandManageChannel(int32_t p1, int32_t p2);
+
+    DISALLOW_COPY_AND_ASSIGN(AppManager);
+};
+
+}  // namespace android::hardware::radio::minimal::sim
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/sim/Filesystem.h b/radio/aidl/minradio/libminradio/include/libminradio/sim/Filesystem.h
new file mode 100644
index 0000000..489d59b
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/sim/Filesystem.h
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <android-base/macros.h>
+#include <android-base/thread_annotations.h>
+
+#include <map>
+#include <span>
+
+namespace android::hardware::radio::minimal::sim {
+
+class Filesystem {
+  public:
+    /** 3GPP TS 27.007 8.18 */
+    struct Path {
+        int32_t fileId;
+        std::string pathId;
+        auto operator<=>(const Path&) const = default;
+        std::string toString() const;
+    };
+
+    typedef std::span<uint8_t const> FileView;
+
+  private:
+    mutable std::mutex mFilesGuard;
+    std::map<Path, std::vector<uint8_t>> mFiles GUARDED_BY(mFilesGuard);
+
+    DISALLOW_COPY_AND_ASSIGN(Filesystem);
+
+  public:
+    Filesystem();
+
+    void write(const Path& path, FileView contents);
+    void write(const Path& path, std::string_view contents);
+    void write(const Path& path, std::vector<uint8_t>&& contents);
+    std::optional<FileView> read(const Path& path) const;
+
+    void writeBch(const Path& path, std::string_view contents);
+    std::optional<std::string> readBch(const Path& path) const;
+
+    std::optional<Path> find(uint16_t fileId);
+};
+
+namespace paths {
+
+extern const Filesystem::Path mf;
+extern const Filesystem::Path fplmn;
+extern const Filesystem::Path iccid;
+extern const Filesystem::Path msisdn;
+extern const Filesystem::Path pl;
+extern const Filesystem::Path arr;
+extern const Filesystem::Path ad;
+
+}  // namespace paths
+
+}  // namespace android::hardware::radio::minimal::sim
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/sim/IccConstants.h b/radio/aidl/minradio/libminradio/include/libminradio/sim/IccConstants.h
new file mode 100644
index 0000000..d33ae28
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/sim/IccConstants.h
@@ -0,0 +1,148 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <string>
+
+namespace android::hardware::radio::minimal::sim::constants {
+
+// From frameworks/opt/telephony/src/java/com/android/internal/telephony/uicc/IccConstants.java
+// 3GPP TS 51.011 Annex D
+// ETSI TS 131 102 Annex A
+constexpr int EF_ADN = 0x6F3A;
+constexpr int EF_FDN = 0x6F3B;
+constexpr int EF_GID1 = 0x6F3E;
+constexpr int EF_GID2 = 0x6F3F;
+constexpr int EF_SDN = 0x6F49;
+constexpr int EF_EXT1 = 0x6F4A;
+constexpr int EF_EXT2 = 0x6F4B;
+constexpr int EF_EXT3 = 0x6F4C;
+constexpr int EF_EXT5 = 0x6F4E;
+constexpr int EF_EXT6 = 0x6FC8;
+constexpr int EF_MWIS = 0x6FCA;
+constexpr int EF_MBDN = 0x6FC7;
+constexpr int EF_PNN = 0x6FC5;
+constexpr int EF_OPL = 0x6FC6;
+constexpr int EF_SPN = 0x6F46;
+constexpr int EF_SMS = 0x6F3C;
+constexpr int EF_ICCID = 0x2FE2;
+constexpr int EF_AD = 0x6FAD;
+constexpr int EF_MBI = 0x6FC9;
+constexpr int EF_MSISDN = 0x6F40;
+constexpr int EF_SPDI = 0x6FCD;
+constexpr int EF_SST = 0x6F38;
+constexpr int EF_CFIS = 0x6FCB;
+constexpr int EF_IMG = 0x4F20;
+constexpr int EF_PSISMSC = 0x6FE5;
+constexpr int EF_SMSS = 0x6F43;
+constexpr int EF_PBR = 0x4F30;
+constexpr int EF_LI = 0x6F05;
+constexpr int EF_MAILBOX_CPHS = 0x6F17;
+constexpr int EF_VOICE_MAIL_INDICATOR_CPHS = 0x6F11;
+constexpr int EF_CFF_CPHS = 0x6F13;
+constexpr int EF_SPN_CPHS = 0x6F14;
+constexpr int EF_SPN_SHORT_CPHS = 0x6F18;
+constexpr int EF_INFO_CPHS = 0x6F16;
+constexpr int EF_CSP_CPHS = 0x6F15;
+constexpr int EF_CST = 0x6F32;
+constexpr int EF_RUIM_SPN = 0x6F41;
+constexpr int EF_PL = 0x2F05;
+constexpr int EF_ARR = 0x2F06;
+constexpr int EF_CSIM_LI = 0x6F3A;
+constexpr int EF_CSIM_SPN = 0x6F41;
+constexpr int EF_CSIM_MDN = 0x6F44;
+constexpr int EF_CSIM_IMSIM = 0x6F22;
+constexpr int EF_CSIM_CDMAHOME = 0x6F28;
+constexpr int EF_CSIM_EPRL = 0x6F5A;
+constexpr int EF_CSIM_PRL = 0x6F30;
+constexpr int EF_CSIM_MLPL = 0x4F20;
+constexpr int EF_CSIM_MSPL = 0x4F21;
+constexpr int EF_CSIM_MIPUPP = 0x6F4D;
+constexpr int EF_IMPU = 0x6F04;
+constexpr int EF_IMPI = 0x6F02;
+constexpr int EF_DOMAIN = 0x6F03;
+constexpr int EF_IST = 0x6F07;
+constexpr int EF_PCSCF = 0x6F09;
+constexpr int EF_PLMN_W_ACT = 0x6F60;
+constexpr int EF_OPLMN_W_ACT = 0x6F61;
+constexpr int EF_HPLMN_W_ACT = 0x6F62;
+constexpr int EF_EHPLMN = 0x6FD9;
+constexpr int EF_FPLMN = 0x6F7B;
+constexpr int EF_LRPLMNSI = 0x6FDC;
+constexpr int EF_HPPLMN = 0x6F31;
+// 3GPP TS 51.011 10.7
+constexpr int MF_SIM_VAL = 0x3F00;
+constexpr std::string MF_SIM = "3F00";
+constexpr std::string DF_TELECOM = "7F10";
+constexpr std::string DF_PHONEBOOK = "5F3A";
+constexpr std::string DF_GRAPHICS = "5F50";
+constexpr std::string DF_GSM = "7F20";
+constexpr std::string DF_CDMA = "7F25";
+constexpr std::string DF_MMSS = "5F3C";
+constexpr std::string DF_ADF = "7FFF";
+
+// From frameworks/base/telephony/java/com/android/internal/telephony/uicc/IccUtils.java
+constexpr int FPLMN_BYTE_SIZE = 3;
+
+// From frameworks/opt/telephony/src/java/com/android/internal/telephony/uicc/IccFileHandler.java
+// 3GPP TS 11.11 9.2
+constexpr int COMMAND_READ_BINARY = 0xB0;     // 176
+constexpr int COMMAND_UPDATE_BINARY = 0xD6;   // 214
+constexpr int COMMAND_READ_RECORD = 0xB2;     // 178
+constexpr int COMMAND_UPDATE_RECORD = 0xDC;   // 220
+constexpr int COMMAND_SEEK = 0xA2;            // 162 (also: SEARCH RECORD)
+constexpr int COMMAND_SELECT = 0xA4;          // 164
+constexpr int COMMAND_GET_RESPONSE = 0xC0;    // 192
+constexpr int COMMAND_STATUS = 0xF2;          // 242
+constexpr int COMMAND_GET_DATA = 0xCA;        // 202 (ISO 7816 7.4.2)
+constexpr int COMMAND_MANAGE_CHANNEL = 0x70;  // 112
+constexpr int EF_TYPE_TRANSPARENT = 0;
+constexpr int EF_TYPE_LINEAR_FIXED = 1;
+constexpr int EF_TYPE_CYCLIC = 3;
+constexpr int TYPE_RFU = 0;
+constexpr int TYPE_MF = 1;
+constexpr int TYPE_DF = 2;
+constexpr int TYPE_EF = 4;
+constexpr int GET_RESPONSE_EF_SIZE_BYTES = 15;
+constexpr int RESPONSE_DATA_RFU_1 = 0;
+constexpr int RESPONSE_DATA_RFU_2 = 1;
+constexpr int RESPONSE_DATA_FILE_SIZE_1 = 2;
+constexpr int RESPONSE_DATA_FILE_SIZE_2 = 3;
+constexpr int RESPONSE_DATA_FILE_ID_1 = 4;
+constexpr int RESPONSE_DATA_FILE_ID_2 = 5;
+constexpr int RESPONSE_DATA_FILE_TYPE = 6;
+constexpr int RESPONSE_DATA_RFU_3 = 7;
+constexpr int RESPONSE_DATA_ACCESS_CONDITION_1 = 8;
+constexpr int RESPONSE_DATA_ACCESS_CONDITION_2 = 9;
+constexpr int RESPONSE_DATA_ACCESS_CONDITION_3 = 10;
+constexpr int RESPONSE_DATA_FILE_STATUS = 11;
+constexpr int RESPONSE_DATA_LENGTH = 12;
+constexpr int RESPONSE_DATA_STRUCTURE = 13;
+constexpr int RESPONSE_DATA_RECORD_LENGTH = 14;
+
+// From frameworks/opt/telephony/src/java/com/android/internal/telephony/uicc/IccIoResult.java
+// ISO 7816 5.1.3
+constexpr uint16_t IO_RESULT_SUCCESS = 0x9000;
+constexpr uint16_t IO_RESULT_NOT_SUPPORTED = 0x6A81;
+constexpr uint16_t IO_RESULT_FILE_NOT_FOUND = 0x6A82;  // file or application
+constexpr uint16_t IO_RESULT_INCORRECT_DATA = 0x6A80;
+constexpr uint16_t IO_RESULT_INCORRECT_P1_P2 = 0x6A86;
+constexpr uint16_t IO_RESULT_INCORRECT_LENGTH = 0x6C00;  // low byte is suggested length
+constexpr uint16_t IO_RESULT_CLASS_NOT_SUPPORTED = 0x6E00;
+constexpr uint16_t IO_RESULT_CHANNEL_NOT_SUPPORTED = 0x6881;
+constexpr uint16_t IO_RESULT_TECHNICAL_PROBLEM = 0x6F00;
+
+}  // namespace android::hardware::radio::minimal::sim::constants
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/sim/IccUtils.h b/radio/aidl/minradio/libminradio/include/libminradio/sim/IccUtils.h
new file mode 100644
index 0000000..9f56f72
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/sim/IccUtils.h
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <aidl/android/hardware/radio/sim/IccIoResult.h>
+
+#include <span>
+#include <string>
+
+namespace android::hardware::radio::minimal::sim {
+
+::aidl::android::hardware::radio::sim::IccIoResult toIccIoResult(std::span<uint8_t const> bytes);
+::aidl::android::hardware::radio::sim::IccIoResult toIccIoResult(std::vector<uint8_t>&& bytes);
+::aidl::android::hardware::radio::sim::IccIoResult toIccIoResult(std::string_view simResponse);
+::aidl::android::hardware::radio::sim::IccIoResult toIccIoResult(uint16_t errorCode);
+
+std::vector<uint8_t> hexStringToBytes(std::string_view str);
+std::vector<uint8_t> hexStringToBch(std::string_view str);
+std::string bytesToHexString(std::span<uint8_t const> bytes);
+std::string bytesToHexString(std::vector<uint8_t>&& bytes);
+std::string bchToHexString(std::span<uint8_t const> bytes);
+
+std::vector<uint8_t> uint8ToBytes(uint8_t val);
+std::vector<uint8_t> uint16ToBytes(uint16_t val);
+
+std::vector<uint8_t> encodeFplmns(std::span<std::string_view> fplmns);
+std::vector<uint8_t> encodeMsisdn(std::string_view phoneNumber);
+std::vector<uint8_t> encodeAd(uint8_t mncLength);
+
+}  // namespace android::hardware::radio::minimal::sim
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/sim/RadioSim.h b/radio/aidl/minradio/libminradio/include/libminradio/sim/RadioSim.h
new file mode 100644
index 0000000..cd138a1
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/sim/RadioSim.h
@@ -0,0 +1,137 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <libminradio/GuaranteedCallback.h>
+#include <libminradio/RadioSlotBase.h>
+#include <libminradio/sim/AppManager.h>
+#include <libminradio/sim/Filesystem.h>
+
+#include <aidl/android/hardware/radio/sim/BnRadioSim.h>
+
+#include <map>
+
+namespace android::hardware::radio::minimal {
+
+class RadioSim : public RadioSlotBase, public aidl::android::hardware::radio::sim::BnRadioSim {
+  public:
+    RadioSim(std::shared_ptr<SlotContext> context);
+
+  protected:
+    void setIccid(std::string iccid);
+    std::optional<std::string> getIccid() const;
+
+    /**
+     * Add CTS_UICC_2021 certificate to UICC.
+     *
+     * This *must not* be called on production build on user's device.
+     */
+    void addCtsCertificate();
+
+    ::ndk::ScopedAStatus areUiccApplicationsEnabled(int32_t serial) override;
+    ::ndk::ScopedAStatus changeIccPin2ForApp(int32_t serial, const std::string& oldPin2,
+                                             const std::string& newPin2,
+                                             const std::string& aid) override;
+    ::ndk::ScopedAStatus changeIccPinForApp(int32_t serial, const std::string& oldPin,
+                                            const std::string& newPin,
+                                            const std::string& aid) override;
+    ::ndk::ScopedAStatus enableUiccApplications(int32_t serial, bool enable) override;
+    ::ndk::ScopedAStatus getAllowedCarriers(int32_t serial) override;
+    ::ndk::ScopedAStatus getCdmaSubscription(int32_t serial) override;
+    ::ndk::ScopedAStatus getCdmaSubscriptionSource(int32_t serial) override;
+    ::ndk::ScopedAStatus getFacilityLockForApp(int32_t serial, const std::string& facility,
+                                               const std::string& password, int32_t serviceClass,
+                                               const std::string& appId) override;
+    ::ndk::ScopedAStatus getSimPhonebookCapacity(int32_t serial) override;
+    ::ndk::ScopedAStatus getSimPhonebookRecords(int32_t serial) override;
+    ::ndk::ScopedAStatus iccCloseLogicalChannel(int32_t serial, int32_t channelId) override;
+    ::ndk::ScopedAStatus iccCloseLogicalChannelWithSessionInfo(
+            int32_t serial,
+            const ::aidl::android::hardware::radio::sim::SessionInfo& recordInfo) override;
+    ::ndk::ScopedAStatus iccIoForApp(
+            int32_t serial, const ::aidl::android::hardware::radio::sim::IccIo& iccIo) override;
+    ::ndk::ScopedAStatus iccOpenLogicalChannel(int32_t serial, const std::string& aid,
+                                               int32_t p2) override;
+    ::ndk::ScopedAStatus iccTransmitApduBasicChannel(
+            int32_t serial, const ::aidl::android::hardware::radio::sim::SimApdu& message) override;
+    ::ndk::ScopedAStatus iccTransmitApduLogicalChannel(
+            int32_t serial, const ::aidl::android::hardware::radio::sim::SimApdu& message) override;
+    ::ndk::ScopedAStatus reportStkServiceIsRunning(int32_t serial) override;
+    ::ndk::ScopedAStatus requestIccSimAuthentication(int32_t serial, int32_t authContext,
+                                                     const std::string& authData,
+                                                     const std::string& aid) override;
+    ::ndk::ScopedAStatus responseAcknowledgement() override;
+    ::ndk::ScopedAStatus sendEnvelope(int32_t serial, const std::string& command) override;
+    ::ndk::ScopedAStatus sendEnvelopeWithStatus(int32_t serial,
+                                                const std::string& contents) override;
+    ::ndk::ScopedAStatus sendTerminalResponseToSim(int32_t serial,
+                                                   const std::string& commandResponse) override;
+    ::ndk::ScopedAStatus setAllowedCarriers(
+            int32_t serial,
+            const ::aidl::android::hardware::radio::sim::CarrierRestrictions& carriers,
+            ::aidl::android::hardware::radio::sim::SimLockMultiSimPolicy multiSimPolicy) override;
+    ::ndk::ScopedAStatus setCarrierInfoForImsiEncryption(
+            int32_t serial,
+            const ::aidl::android::hardware::radio::sim::ImsiEncryptionInfo& imsiEncryptionInfo)
+            override;
+    ::ndk::ScopedAStatus setCdmaSubscriptionSource(
+            int32_t serial,
+            ::aidl::android::hardware::radio::sim::CdmaSubscriptionSource cdmaSub) override;
+    ::ndk::ScopedAStatus setFacilityLockForApp(  //
+            int32_t serial, const std::string& facility, bool lockState, const std::string& passwd,
+            int32_t serviceClass, const std::string& appId) override;
+    ::ndk::ScopedAStatus setResponseFunctions(
+            const std::shared_ptr<::aidl::android::hardware::radio::sim::IRadioSimResponse>&
+                    radioSimResponse,
+            const std::shared_ptr<::aidl::android::hardware::radio::sim::IRadioSimIndication>&
+                    radioSimIndication) override;
+    ::ndk::ScopedAStatus setSimCardPower(
+            int32_t serial, ::aidl::android::hardware::radio::sim::CardPowerState powerUp) override;
+    ::ndk::ScopedAStatus setUiccSubscription(
+            int32_t serial,
+            const ::aidl::android::hardware::radio::sim::SelectUiccSub& uiccSub) override;
+    ::ndk::ScopedAStatus supplyIccPin2ForApp(int32_t serial, const std::string& pin2,
+                                             const std::string& aid) override;
+    ::ndk::ScopedAStatus supplyIccPinForApp(int32_t serial, const std::string& pin,
+                                            const std::string& aid) override;
+    ::ndk::ScopedAStatus supplyIccPuk2ForApp(int32_t serial, const std::string& puk2,
+                                             const std::string& pin2,
+                                             const std::string& aid) override;
+    ::ndk::ScopedAStatus supplyIccPukForApp(int32_t serial, const std::string& puk,
+                                            const std::string& pin,
+                                            const std::string& aid) override;
+    ::ndk::ScopedAStatus supplySimDepersonalization(
+            int32_t serial, ::aidl::android::hardware::radio::sim::PersoSubstate persoType,
+            const std::string& controlKey) override;
+    ::ndk::ScopedAStatus updateSimPhonebookRecords(
+            int32_t serial,
+            const ::aidl::android::hardware::radio::sim::PhonebookRecordInfo& recordInfo) override;
+
+    GuaranteedCallback<::aidl::android::hardware::radio::sim::IRadioSimIndication,
+                       ::aidl::android::hardware::radio::sim::IRadioSimIndicationDefault, true>
+            indicate;
+    GuaranteedCallback<::aidl::android::hardware::radio::sim::IRadioSimResponse,
+                       ::aidl::android::hardware::radio::sim::IRadioSimResponseDefault>
+            respond;
+
+    sim::AppManager mAppManager;
+    const std::shared_ptr<sim::Filesystem> mFilesystem = std::make_shared<sim::Filesystem>();
+
+  private:
+    bool mAreUiccApplicationsEnabled = true;
+};
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/sim/apps/AraM.h b/radio/aidl/minradio/libminradio/include/libminradio/sim/apps/AraM.h
new file mode 100644
index 0000000..e8c787f
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/sim/apps/AraM.h
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <libminradio/sim/App.h>
+
+#include <span>
+
+namespace android::hardware::radio::minimal::sim::apps {
+
+/**
+ * UICC carrier privileges app (ARA-M) implementation.
+ *
+ * https://source.android.com/docs/core/connect/uicc
+ */
+class AraM : public std::enable_shared_from_this<AraM>, public App {
+  public:
+    static constexpr char AID[] = "A00000015141434C00";
+
+    struct Rule {
+        std::vector<uint8_t> deviceAppID;
+        std::string pkg;
+    };
+
+    AraM();
+    std::shared_ptr<App::Channel> newChannel(int32_t id) override;
+
+    void addRule(Rule rule);
+    std::span<const Rule> getRules() const;
+
+  private:
+    std::vector<Rule> mRules;
+};
+
+}  // namespace android::hardware::radio::minimal::sim::apps
diff --git a/radio/aidl/minradio/libminradio/include/libminradio/sim/apps/FilesystemApp.h b/radio/aidl/minradio/libminradio/include/libminradio/sim/apps/FilesystemApp.h
new file mode 100644
index 0000000..36178e4
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/sim/apps/FilesystemApp.h
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <aidl/android/hardware/radio/sim/IccIo.h>
+#include <aidl/android/hardware/radio/sim/IccIoResult.h>
+#include <libminradio/sim/App.h>
+#include <libminradio/sim/Filesystem.h>
+
+namespace android::hardware::radio::minimal::sim::apps {
+
+class FilesystemApp : public App {
+  public:
+    static constexpr char AID[] = "";
+
+    FilesystemApp(const std::shared_ptr<Filesystem>& filesystem);
+    std::shared_ptr<App::Channel> newChannel(int32_t id) override;
+
+    ::aidl::android::hardware::radio::sim::IccIoResult iccIo(
+            const ::aidl::android::hardware::radio::sim::IccIo& iccIo) override;
+
+  private:
+    class FilesystemChannel;
+
+    std::shared_ptr<FilesystemChannel> mBasicChannel;
+    std::shared_ptr<Filesystem> mFilesystem;
+};
+
+}  // namespace android::hardware::radio::minimal::sim::apps
diff --git a/radio/aidl/minradio/libminradio/modem/RadioModem.cpp b/radio/aidl/minradio/libminradio/modem/RadioModem.cpp
new file mode 100644
index 0000000..1c1c5fa
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/modem/RadioModem.cpp
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2023 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 <libminradio/modem/RadioModem.h>
+
+#include <libminradio/debug.h>
+#include <libminradio/response.h>
+
+#define RADIO_MODULE "Modem"
+
+namespace android::hardware::radio::minimal {
+
+using namespace ::android::hardware::radio::minimal::binder_printing;
+using ::aidl::android::hardware::radio::RadioIndicationType;
+using ::ndk::ScopedAStatus;
+namespace aidl = ::aidl::android::hardware::radio::modem;
+namespace aidlRadio = ::aidl::android::hardware::radio;
+constexpr auto ok = &ScopedAStatus::ok;
+
+RadioModem::RadioModem(std::shared_ptr<SlotContext> context,
+                       std::vector<aidlRadio::RadioTechnology> rats)
+    : RadioSlotBase(context) {
+    int32_t ratBitmap = 0;
+    for (auto rat : rats) {
+        CHECK(rat > aidlRadio::RadioTechnology::UNKNOWN) << "Invalid RadioTechnology: " << rat;
+        CHECK(rat <= aidlRadio::RadioTechnology::NR)
+                << ": " << rat << " not supported yet: "
+                << "please verify if RadioAccessFamily for this RadioTechnology is a bit-shifted 1";
+        ratBitmap |= 1 << static_cast<int32_t>(rat);
+    }
+    mRatBitmap = ratBitmap;
+}
+
+std::string RadioModem::getModemUuid() const {
+    // Assumes one modem per slot.
+    return std::format("com.android.minradio.modem{}", mContext->getSlotIndex());
+}
+
+std::string RadioModem::getSimUuid() const {
+    // Assumes one SIM per slot.
+    return std::format("com.android.minradio.sim{}", mContext->getSlotIndex());
+}
+
+ScopedAStatus RadioModem::enableModem(int32_t serial, bool on) {
+    LOG_NOT_SUPPORTED << on;
+    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+ScopedAStatus RadioModem::getBasebandVersion(int32_t serial) {
+    LOG_CALL;
+    respond()->getBasebandVersionResponse(  //
+            noError(serial), std::format("libminradio V{}", IRadioModem::version));
+    return ok();
+}
+
+ScopedAStatus RadioModem::getDeviceIdentity(int32_t serial) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioModem::getHardwareConfig(int32_t serial) {
+    LOG_CALL;
+
+    aidl::HardwareConfig modem1Config{
+            .type = aidl::HardwareConfig::TYPE_MODEM,
+            .uuid = getModemUuid(),
+            .state = aidl::HardwareConfig::STATE_ENABLED,
+            .modem = {{
+                    .rilModel = 0,  // 0=single (one-to-one relationship for hw and ril daemon)
+                    .rat = static_cast<aidlRadio::RadioTechnology>(mRatBitmap),
+                    .maxVoiceCalls = 0,
+                    .maxDataCalls = 1,
+                    .maxStandby = 1,
+            }},
+    };
+
+    aidl::HardwareConfig sim1Config{
+            .type = aidl::HardwareConfig::TYPE_SIM,
+            .uuid = getSimUuid(),
+            .state = aidl::HardwareConfig::STATE_ENABLED,
+            .sim = {{
+                    .modemUuid = getModemUuid(),
+            }},
+    };
+
+    respond()->getHardwareConfigResponse(noError(serial), {modem1Config, sim1Config});
+    return ok();
+}
+
+ScopedAStatus RadioModem::getModemActivityInfo(int32_t serial) {
+    LOG_CALL_IGNORED;
+    const aidl::ActivityStatsTechSpecificInfo generalActivityStats{
+            .txmModetimeMs = {0, 0, 0, 0, 0},
+    };
+    const aidl::ActivityStatsInfo info{
+            // idleModeTimeMs doesn't make sense for external modem, but the framework
+            // doesn't allow for ModemActivityInfo.isEmpty
+            .idleModeTimeMs = 1,
+            .techSpecificInfo = {generalActivityStats},
+    };
+    respond()->getModemActivityInfoResponse(noError(serial), info);
+    return ok();
+}
+
+ScopedAStatus RadioModem::getModemStackStatus(int32_t serial) {
+    LOG_CALL;
+    respond()->getModemStackStatusResponse(noError(serial), true);
+    return ok();
+}
+
+ScopedAStatus RadioModem::getRadioCapability(int32_t serial) {
+    LOG_CALL;
+    aidl::RadioCapability cap{
+            .session = 0,
+            .phase = aidl::RadioCapability::PHASE_FINISH,
+            .raf = mRatBitmap,  // rafs are nothing else than rat masks
+            .logicalModemUuid = getModemUuid(),
+            .status = aidl::RadioCapability::STATUS_SUCCESS,
+    };
+    respond()->getRadioCapabilityResponse(noError(serial), cap);
+    return ok();
+}
+
+ScopedAStatus RadioModem::nvReadItem(int32_t serial, aidl::NvItem) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioModem::nvResetConfig(int32_t serial, aidl::ResetNvType resetType) {
+    LOG_CALL << resetType;  // RELOAD is the only non-deprecated argument
+    respond()->nvResetConfigResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioModem::nvWriteCdmaPrl(int32_t serial, const std::vector<uint8_t>&) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioModem::nvWriteItem(int32_t serial, const aidl::NvWriteItem&) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioModem::requestShutdown(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+ScopedAStatus RadioModem::responseAcknowledgement() {
+    LOG_CALL_NOSERIAL;
+    return ok();
+}
+
+ScopedAStatus RadioModem::sendDeviceState(int32_t serial, aidl::DeviceStateType type, bool state) {
+    LOG_CALL_IGNORED << type << ' ' << state;
+    respond()->sendDeviceStateResponse(noError(serial));
+    return ok();
+}
+
+ScopedAStatus RadioModem::setRadioCapability(int32_t serial, const aidl::RadioCapability& rc) {
+    LOG_NOT_SUPPORTED << rc;
+    respond()->setRadioCapabilityResponse(notSupported(serial), {});
+    return ok();
+}
+
+ScopedAStatus RadioModem::setRadioPower(int32_t serial, bool powerOn, bool forEmergencyCall,
+                                        bool preferredForEmergencyCall) {
+    LOG_CALL_IGNORED << powerOn << " " << forEmergencyCall << " " << preferredForEmergencyCall;
+    respond()->setRadioPowerResponse(noError(serial));
+    indicate()->radioStateChanged(RadioIndicationType::UNSOLICITED,
+                                  powerOn ? aidl::RadioState::ON : aidl::RadioState::OFF);
+    return ok();
+}
+
+ScopedAStatus RadioModem::setResponseFunctions(
+        const std::shared_ptr<aidl::IRadioModemResponse>& response,
+        const std::shared_ptr<aidl::IRadioModemIndication>& indication) {
+    LOG_CALL_NOSERIAL << response << ' ' << indication;
+    CHECK(response);
+    CHECK(indication);
+    respond = mResponseTracker =
+            ndk::SharedRefBase::make<RadioModemResponseTracker>(ref<aidl::IRadioModem>(), response);
+    indicate = indication;
+
+    indicate()->rilConnected(RadioIndicationType::UNSOLICITED);
+    indicate()->radioStateChanged(RadioIndicationType::UNSOLICITED, aidl::RadioState::ON);
+
+    return ok();
+}
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/modem/RadioModemResponseTracker.cpp b/radio/aidl/minradio/libminradio/modem/RadioModemResponseTracker.cpp
new file mode 100644
index 0000000..eb9bcf7
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/modem/RadioModemResponseTracker.cpp
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+// see assert2_no_op in ResponseTracker.cpp
+#define __assert2 assert2_no_op
+#define __noreturn__ const
+#include <aidl/android/hardware/radio/modem/BnRadioModemResponse.h>
+#undef __assert2
+#undef __noreturn__
+#include <cassert>
+
+#include <libminradio/modem/RadioModemResponseTracker.h>
+
+#include <libminradio/debug.h>
+
+#define RADIO_MODULE "ModemResponse"
+
+namespace android::hardware::radio::minimal {
+
+using namespace ::android::hardware::radio::minimal::binder_printing;
+using ::aidl::android::hardware::radio::RadioResponseInfo;
+using ::ndk::ScopedAStatus;
+namespace aidl = ::aidl::android::hardware::radio::modem;
+
+RadioModemResponseTracker::RadioModemResponseTracker(
+        std::shared_ptr<aidl::IRadioModem> req,
+        const std::shared_ptr<aidl::IRadioModemResponse>& resp)
+    : ResponseTracker(req, resp) {}
+
+ResponseTrackerResult<aidl::ImeiInfo> RadioModemResponseTracker::getImei() {
+    auto serial = newSerial();
+    if (auto status = request()->getImei(serial); !status.isOk()) return status;
+    return getResult<aidl::ImeiInfo>(serial);
+}
+
+ScopedAStatus RadioModemResponseTracker::getImeiResponse(
+        const RadioResponseInfo& info, const std::optional<aidl::ImeiInfo>& respData) {
+    LOG_CALL_RESPONSE << respData;
+    if (isTracked(info.serial)) return handle(info, respData.value_or(aidl::ImeiInfo{}));
+    return IRadioModemResponseDelegator::getImeiResponse(info, respData);
+}
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/network/RadioNetwork.cpp b/radio/aidl/minradio/libminradio/network/RadioNetwork.cpp
new file mode 100644
index 0000000..e2d2a56
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/network/RadioNetwork.cpp
@@ -0,0 +1,398 @@
+/*
+ * Copyright (C) 2023 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 <libminradio/network/RadioNetwork.h>
+
+#include <libminradio/debug.h>
+#include <libminradio/network/structs.h>
+#include <libminradio/response.h>
+
+#include <chrono>
+#include <thread>
+
+#define RADIO_MODULE "Network"
+
+namespace android::hardware::radio::minimal {
+
+using namespace ::android::hardware::radio::minimal::binder_printing;
+using ::aidl::android::hardware::radio::AccessNetwork;
+using ::aidl::android::hardware::radio::RadioError;
+using ::aidl::android::hardware::radio::RadioIndicationType;
+using ::ndk::ScopedAStatus;
+namespace aidl = ::aidl::android::hardware::radio::network;
+namespace aidlRadio = ::aidl::android::hardware::radio;
+constexpr auto ok = &ScopedAStatus::ok;
+
+std::vector<aidl::CellInfo> RadioNetwork::getCellInfoListBase() {
+    if (!mResponseTracker) return {};
+
+    // There's a slight race between get*RegistrationState and getSignalStrength, but
+    // getCellInfoListBase is best-effort anyway, so it's the best we can do here.
+    auto dataRegistrationState = mResponseTracker()->getDataRegistrationState();
+    auto signalStrength = mResponseTracker()->getSignalStrength();
+    if (!dataRegistrationState.expectOk() || !signalStrength.expectOk()) return {};
+
+    return {structs::makeCellInfo(*dataRegistrationState, *signalStrength)};
+}
+
+ScopedAStatus RadioNetwork::getAllowedNetworkTypesBitmap(int32_t serial) {
+    LOG_CALL;
+    respond()->getAllowedNetworkTypesBitmapResponse(noError(serial), mAllowedNetworkTypesBitmap);
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::getAvailableBandModes(int32_t serial) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioNetwork::getAvailableNetworks(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    respond()->getAvailableNetworksResponse(notSupported(serial), {});
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::getBarringInfo(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    respond()->getBarringInfoResponse(notSupported(serial), {}, {});
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::getCdmaRoamingPreference(int32_t serial) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioNetwork::getCellInfoList(int32_t serial) {
+    LOG_CALL;
+    respond()->getCellInfoListResponse(noError(serial), getCellInfoListBase());
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::getImsRegistrationState(int32_t serial) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioNetwork::getNetworkSelectionMode(int32_t serial) {
+    LOG_CALL;
+    respond()->getNetworkSelectionModeResponse(noError(serial), /*manual*/ false);
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::getOperator(int32_t serial) {
+    LOG_CALL;
+
+    auto dataRegistrationState = mResponseTracker()->getDataRegistrationState();
+    if (!dataRegistrationState.expectOk()) {
+        respond()->getOperatorResponse(errorResponse(serial, RadioError::INTERNAL_ERR), {}, {}, {});
+        return ok();
+    }
+
+    auto opInfo = structs::getOperatorInfo(dataRegistrationState->cellIdentity);
+    respond()->getOperatorResponse(noError(serial), opInfo.alphaLong, opInfo.alphaShort,
+                                   opInfo.operatorNumeric);
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::getSystemSelectionChannels(int32_t serial) {
+    LOG_CALL_IGNORED;
+    respond()->getSystemSelectionChannelsResponse(noError(serial), {});
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::getVoiceRadioTechnology(int32_t serial) {
+    LOG_CALL;
+    respond()->getVoiceRadioTechnologyResponse(noError(serial),
+                                               aidlRadio::RadioTechnology::UNKNOWN);
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::getVoiceRegistrationState(int32_t serial) {
+    LOG_CALL;
+    respond()->getVoiceRegistrationStateResponse(noError(serial),
+                                                 {aidl::RegState::NOT_REG_MT_NOT_SEARCHING_OP});
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::isNrDualConnectivityEnabled(int32_t serial) {
+    // Disabled with modemReducedFeatureSet1.
+    LOG_NOT_SUPPORTED;
+    respond()->isNrDualConnectivityEnabledResponse(notSupported(serial), false);
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::responseAcknowledgement() {
+    LOG_CALL_NOSERIAL;
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setAllowedNetworkTypesBitmap(int32_t serial, int32_t ntype) {
+    LOG_CALL_IGNORED << ntype;
+    mAllowedNetworkTypesBitmap = ntype;
+    respond()->setAllowedNetworkTypesBitmapResponse(noError(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setBandMode(int32_t serial, aidl::RadioBandMode) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioNetwork::setBarringPassword(int32_t serial, const std::string& facility,
+                                               const std::string& oldPw, const std::string& newPw) {
+    LOG_NOT_SUPPORTED << facility << ' ' << oldPw << ' ' << newPw;
+    respond()->setBarringPasswordResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setCdmaRoamingPreference(int32_t serial, aidl::CdmaRoamingType) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioNetwork::setCellInfoListRate(int32_t serial, int32_t rate) {
+    LOG_NOT_SUPPORTED << rate;
+    respond()->setCellInfoListRateResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setIndicationFilter(int32_t serial, int32_t indFilter) {
+    LOG_CALL_IGNORED << indFilter;
+    respond()->setIndicationFilterResponse(noError(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setLinkCapacityReportingCriteria(  //
+        int32_t serial, int32_t hysteresisMs, int32_t hysteresisDlKbps, int32_t hysteresisUlKbps,
+        const std::vector<int32_t>& thrDownlinkKbps, const std::vector<int32_t>& thrUplinkKbps,
+        AccessNetwork accessNetwork) {
+    LOG_NOT_SUPPORTED << hysteresisMs << ' ' << hysteresisDlKbps << ' ' << hysteresisUlKbps << ' '
+                      << thrDownlinkKbps << ' ' << thrUplinkKbps << ' ' << accessNetwork;
+    respond()->setLinkCapacityReportingCriteriaResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setLocationUpdates(int32_t serial, bool) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioNetwork::setNetworkSelectionModeAutomatic(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    respond()->setNetworkSelectionModeAutomaticResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setNetworkSelectionModeManual(  //
+        int32_t serial, const std::string& opNumeric, AccessNetwork ran) {
+    LOG_NOT_SUPPORTED << opNumeric << ' ' << ran;
+    respond()->setNetworkSelectionModeManualResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setNrDualConnectivityState(int32_t serial,
+                                                       aidl::NrDualConnectivityState st) {
+    // Disabled with modemReducedFeatureSet1.
+    LOG_NOT_SUPPORTED << st;
+    respond()->setNrDualConnectivityStateResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setResponseFunctions(
+        const std::shared_ptr<aidl::IRadioNetworkResponse>& response,
+        const std::shared_ptr<aidl::IRadioNetworkIndication>& indication) {
+    LOG_CALL_NOSERIAL << response << ' ' << indication;
+    CHECK(response);
+    CHECK(indication);
+    mResponseTracker = ndk::SharedRefBase::make<RadioNetworkResponseTracker>(
+            ref<aidl::IRadioNetwork>(), response);
+    respond = mResponseTracker.get();
+    indicate = indication;
+
+    indicate()->cellInfoList(RadioIndicationType::UNSOLICITED, getCellInfoListBase());
+    auto signalStrengthResponse = mResponseTracker()->getSignalStrength();
+    if (signalStrengthResponse.expectOk()) {
+        aidl::SignalStrength signalStrength = *signalStrengthResponse;
+        indicate()->currentSignalStrength(RadioIndicationType::UNSOLICITED, signalStrength);
+
+        // TODO(b/379302126): fix race condition in ServiceStateTracker which doesn't listen for
+        //       EVENT_UNSOL_CELL_INFO_LIST for the first ~1.3s after setResponseFunctions
+        // TODO(b/379302126): fix race condition in SignalStrengthController, starting to listen for
+        //       EVENT_SIGNAL_STRENGTH_UPDATE after ~3.7s
+        // This workaround thread would be a race condition itself (with use-after-free), but we can
+        // drop it once the two bugs mentioned above are fixed.
+        std::thread([this, signalStrength] {
+            for (int i = 0; i < 10; i++) {
+                using namespace std::chrono_literals;
+                std::this_thread::sleep_for(1s);
+                indicate()->cellInfoList(RadioIndicationType::UNSOLICITED, getCellInfoListBase());
+                indicate()->currentSignalStrength(RadioIndicationType::UNSOLICITED, signalStrength);
+            }
+        }).detach();
+    }
+
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setSignalStrengthReportingCriteria(
+        int32_t serial, const std::vector<aidl::SignalThresholdInfo>& infos) {
+    LOG_CALL_IGNORED << infos;
+    respond()->setSignalStrengthReportingCriteriaResponse(
+            structs::validateSignalThresholdInfos(infos)
+                    ? noError(serial)
+                    : errorResponse(serial, RadioError::INVALID_ARGUMENTS));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setSuppServiceNotifications(int32_t serial, bool) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioNetwork::setSystemSelectionChannels(  //
+        int32_t serial, bool specifyCh, const std::vector<aidl::RadioAccessSpecifier>& specifiers) {
+    LOG_CALL_IGNORED << specifyCh << ' ' << specifiers;
+    if (specifiers.empty()) {
+        respond()->setSystemSelectionChannelsResponse(noError(serial));
+    } else {
+        respond()->setSystemSelectionChannelsResponse(notSupported(serial));
+    }
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::startNetworkScan(int32_t serial, const aidl::NetworkScanRequest& req) {
+    LOG_NOT_SUPPORTED << req;
+    respond()->startNetworkScanResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::stopNetworkScan(int32_t serial) {
+    LOG_CALL_IGNORED;
+    respond()->stopNetworkScanResponse(noError(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::supplyNetworkDepersonalization(int32_t serial,
+                                                           const std::string& nPin) {
+    LOG_NOT_SUPPORTED << nPin;
+    respond()->supplyNetworkDepersonalizationResponse(notSupported(serial), -1);
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setUsageSetting(int32_t serial, aidl::UsageSetting usageSetting) {
+    LOG_CALL_IGNORED << usageSetting;
+    if (usageSetting == aidl::UsageSetting::DATA_CENTRIC) {
+        respond()->setUsageSettingResponse(noError(serial));
+    } else {
+        respond()->setUsageSettingResponse(errorResponse(serial, RadioError::INVALID_ARGUMENTS));
+    }
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::getUsageSetting(int32_t serial) {
+    LOG_CALL;
+    respond()->getUsageSettingResponse(noError(serial), aidl::UsageSetting::DATA_CENTRIC);
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setEmergencyMode(int32_t serial, aidl::EmergencyMode emergencyMode) {
+    LOG_NOT_SUPPORTED << emergencyMode;
+    respond()->setEmergencyModeResponse(notSupported(serial), {});
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::triggerEmergencyNetworkScan(
+        int32_t serial, const aidl::EmergencyNetworkScanTrigger& trigger) {
+    LOG_NOT_SUPPORTED << trigger;
+    respond()->triggerEmergencyNetworkScanResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::cancelEmergencyNetworkScan(int32_t serial, bool resetScan) {
+    LOG_NOT_SUPPORTED << resetScan;
+    respond()->cancelEmergencyNetworkScanResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::exitEmergencyMode(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    respond()->exitEmergencyModeResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setNullCipherAndIntegrityEnabled(int32_t serial, bool enabled) {
+    LOG_CALL_IGNORED << enabled;
+    respond()->setNullCipherAndIntegrityEnabledResponse(noError(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::isNullCipherAndIntegrityEnabled(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    respond()->isNullCipherAndIntegrityEnabledResponse(notSupported(serial), false);
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::isN1ModeEnabled(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    respond()->isN1ModeEnabledResponse(notSupported(serial), false);
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setN1ModeEnabled(int32_t serial, bool enable) {
+    LOG_NOT_SUPPORTED << enable;
+    respond()->setN1ModeEnabledResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::isCellularIdentifierTransparencyEnabled(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    respond()->isCellularIdentifierTransparencyEnabledResponse(notSupported(serial), false);
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setCellularIdentifierTransparencyEnabled(int32_t serial, bool enabled) {
+    LOG_CALL_IGNORED << enabled;
+    respond()->setCellularIdentifierTransparencyEnabledResponse(noError(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::isSecurityAlgorithmsUpdatedEnabled(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    respond()->isSecurityAlgorithmsUpdatedEnabledResponse(notSupported(serial), false);
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setSecurityAlgorithmsUpdatedEnabled(int32_t serial, bool enable) {
+    LOG_NOT_SUPPORTED << enable;
+    respond()->setSecurityAlgorithmsUpdatedEnabledResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setSatellitePlmn(
+        int32_t serial, const std::vector<std::string>& carrierPlmnArray,
+        const std::vector<std::string>& allSatellitePlmnArray) {
+    LOG_NOT_SUPPORTED << carrierPlmnArray << ' ' << allSatellitePlmnArray;
+    respond()->setSatellitePlmnResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::setSatelliteEnabledForCarrier(int32_t serial, bool satelliteEnabled) {
+    LOG_NOT_SUPPORTED << satelliteEnabled;
+    respond()->setSatelliteEnabledForCarrierResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::isSatelliteEnabledForCarrier(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    respond()->isSatelliteEnabledForCarrierResponse(notSupported(serial), false);
+    return ok();
+}
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/network/RadioNetworkResponseTracker.cpp b/radio/aidl/minradio/libminradio/network/RadioNetworkResponseTracker.cpp
new file mode 100644
index 0000000..d3a4ad8
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/network/RadioNetworkResponseTracker.cpp
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+// see assert2_no_op in ResponseTracker.cpp
+#define __assert2 assert2_no_op
+#define __noreturn__ const
+#include <aidl/android/hardware/radio/network/BnRadioNetworkResponse.h>
+#undef __assert2
+#undef __noreturn__
+#include <cassert>
+
+#include <libminradio/network/RadioNetworkResponseTracker.h>
+
+#include <libminradio/debug.h>
+
+#define RADIO_MODULE "NetworkResponse"
+
+namespace android::hardware::radio::minimal {
+
+using namespace ::android::hardware::radio::minimal::binder_printing;
+using ::aidl::android::hardware::radio::RadioResponseInfo;
+using ::ndk::ScopedAStatus;
+namespace aidl = ::aidl::android::hardware::radio::network;
+
+RadioNetworkResponseTracker::RadioNetworkResponseTracker(
+        std::shared_ptr<aidl::IRadioNetwork> req,
+        const std::shared_ptr<aidl::IRadioNetworkResponse>& resp)
+    : ResponseTracker(req, resp) {}
+
+ResponseTrackerResult<aidl::RegStateResult>
+RadioNetworkResponseTracker::getDataRegistrationState() {
+    auto serial = newSerial();
+    if (auto status = request()->getDataRegistrationState(serial); !status.isOk()) return status;
+    return getResult<aidl::RegStateResult>(serial);
+}
+
+ScopedAStatus RadioNetworkResponseTracker::getDataRegistrationStateResponse(
+        const RadioResponseInfo& info, const aidl::RegStateResult& respData) {
+    LOG_CALL_RESPONSE << respData;
+    if (isTracked(info.serial)) return handle(info, respData);
+    return IRadioNetworkResponseDelegator::getDataRegistrationStateResponse(info, respData);
+}
+
+ResponseTrackerResult<aidl::SignalStrength> RadioNetworkResponseTracker::getSignalStrength() {
+    auto serial = newSerial();
+    if (auto status = request()->getSignalStrength(serial); !status.isOk()) return status;
+    return getResult<aidl::SignalStrength>(serial);
+}
+
+ScopedAStatus RadioNetworkResponseTracker::getSignalStrengthResponse(
+        const RadioResponseInfo& info, const aidl::SignalStrength& respData) {
+    LOG_CALL_RESPONSE << respData;
+    if (isTracked(info.serial)) return handle(info, respData);
+    return IRadioNetworkResponseDelegator::getSignalStrengthResponse(info, respData);
+}
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/network/structs.cpp b/radio/aidl/minradio/libminradio/network/structs.cpp
new file mode 100644
index 0000000..2366c18
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/network/structs.cpp
@@ -0,0 +1,178 @@
+/*
+ * 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.
+ */
+#include <libminradio/network/structs.h>
+
+#include <android-base/logging.h>
+#include <libminradio/binder_printing.h>
+
+namespace android::hardware::radio::minimal::structs {
+
+using namespace ::android::hardware::radio::minimal::binder_printing;
+using ::aidl::android::hardware::radio::RadioConst;
+namespace aidl = ::aidl::android::hardware::radio::network;
+
+aidl::SignalStrength makeSignalStrength() {
+    constexpr aidl::GsmSignalStrength gsm{
+            RadioConst::VALUE_UNAVAILABLE,
+            RadioConst::VALUE_UNAVAILABLE,
+            RadioConst::VALUE_UNAVAILABLE,
+    };
+    constexpr aidl::LteSignalStrength lte{
+            RadioConst::VALUE_UNAVAILABLE, RadioConst::VALUE_UNAVAILABLE,
+            RadioConst::VALUE_UNAVAILABLE, RadioConst::VALUE_UNAVAILABLE,
+            RadioConst::VALUE_UNAVAILABLE, RadioConst::VALUE_UNAVAILABLE,
+            RadioConst::VALUE_UNAVAILABLE,
+    };
+    constexpr aidl::TdscdmaSignalStrength tdscdma{
+            RadioConst::VALUE_UNAVAILABLE,
+            RadioConst::VALUE_UNAVAILABLE,
+            RadioConst::VALUE_UNAVAILABLE,
+    };
+    constexpr aidl::WcdmaSignalStrength wcdma{
+            RadioConst::VALUE_UNAVAILABLE,
+            RadioConst::VALUE_UNAVAILABLE,
+            RadioConst::VALUE_UNAVAILABLE,
+            RadioConst::VALUE_UNAVAILABLE,
+    };
+    constexpr aidl::NrSignalStrength nr{
+            RadioConst::VALUE_UNAVAILABLE, RadioConst::VALUE_UNAVAILABLE,
+            RadioConst::VALUE_UNAVAILABLE, RadioConst::VALUE_UNAVAILABLE,
+            RadioConst::VALUE_UNAVAILABLE, RadioConst::VALUE_UNAVAILABLE,
+            RadioConst::VALUE_UNAVAILABLE, {},
+            RadioConst::VALUE_UNAVAILABLE,
+    };
+
+    return {
+            .gsm = gsm,
+            .lte = lte,
+            .tdscdma = tdscdma,
+            .wcdma = wcdma,
+            .nr = nr,
+    };
+}
+
+aidl::CellInfo makeCellInfo(const aidl::RegStateResult& regState,
+                            const aidl::SignalStrength& signalStrength) {
+    std::optional<aidl::CellInfoRatSpecificInfo> ratSpecificInfo;
+    auto& cellId = regState.cellIdentity;
+    switch (cellId.getTag()) {
+        case aidl::CellIdentity::Tag::noinit:
+            break;
+        case aidl::CellIdentity::Tag::gsm:
+            ratSpecificInfo = aidl::CellInfoGsm{
+                    .cellIdentityGsm = cellId.get<aidl::CellIdentity::Tag::gsm>(),
+                    .signalStrengthGsm = signalStrength.gsm,
+            };
+            break;
+        case aidl::CellIdentity::Tag::wcdma:
+            ratSpecificInfo = aidl::CellInfoWcdma{
+                    .cellIdentityWcdma = cellId.get<aidl::CellIdentity::Tag::wcdma>(),
+                    .signalStrengthWcdma = signalStrength.wcdma,
+            };
+            break;
+        case aidl::CellIdentity::Tag::tdscdma:
+            ratSpecificInfo = aidl::CellInfoTdscdma{
+                    .cellIdentityTdscdma = cellId.get<aidl::CellIdentity::Tag::tdscdma>(),
+                    .signalStrengthTdscdma = signalStrength.tdscdma,
+            };
+            break;
+        case aidl::CellIdentity::Tag::lte:
+            ratSpecificInfo = aidl::CellInfoLte{
+                    .cellIdentityLte = cellId.get<aidl::CellIdentity::Tag::lte>(),
+                    .signalStrengthLte = signalStrength.lte,
+            };
+            break;
+        case aidl::CellIdentity::Tag::nr:
+            ratSpecificInfo = aidl::CellInfoNr{
+                    .cellIdentityNr = cellId.get<aidl::CellIdentity::Tag::nr>(),
+                    .signalStrengthNr = signalStrength.nr,
+            };
+            break;
+    }
+    CHECK(ratSpecificInfo.has_value()) << "Cell identity not handled: " << cellId;
+
+    bool isRegistered = regState.regState == aidl::RegState::REG_HOME ||
+                        regState.regState == aidl::RegState::REG_ROAMING;
+
+    return aidl::CellInfo{
+            .registered = isRegistered,
+            .connectionStatus = isRegistered ? aidl::CellConnectionStatus::PRIMARY_SERVING
+                                             : aidl::CellConnectionStatus::NONE,
+            .ratSpecificInfo = *ratSpecificInfo,
+    };
+}
+
+aidl::OperatorInfo getOperatorInfo(const aidl::CellIdentity& cellId) {
+    switch (cellId.getTag()) {
+        case aidl::CellIdentity::Tag::noinit:
+            return {};
+        case aidl::CellIdentity::Tag::gsm:
+            return cellId.get<aidl::CellIdentity::Tag::gsm>().operatorNames;
+        case aidl::CellIdentity::Tag::wcdma:
+            return cellId.get<aidl::CellIdentity::Tag::wcdma>().operatorNames;
+        case aidl::CellIdentity::Tag::tdscdma:
+            return cellId.get<aidl::CellIdentity::Tag::tdscdma>().operatorNames;
+        case aidl::CellIdentity::Tag::lte:
+            return cellId.get<aidl::CellIdentity::Tag::lte>().operatorNames;
+        case aidl::CellIdentity::Tag::nr:
+            return cellId.get<aidl::CellIdentity::Tag::nr>().operatorNames;
+    }
+    LOG(FATAL) << "Cell identity not handled: " << cellId;
+}
+
+int32_t rssiToSignalStrength(int32_t rssi) {
+    // 3GPP TS 27.007 8.5
+    if (rssi <= -113) return 0;
+    if (rssi >= -51) return 31;
+    if (rssi >= -1) return 99;
+    return (rssi + 113) / 2;
+}
+
+int32_t validateRsrp(int32_t rsrp) {
+    // 3GPP TS 27.007 8.69
+    if (rsrp < -140 || rsrp > -44) return RadioConst::VALUE_UNAVAILABLE;
+    return -rsrp;
+}
+
+int32_t validateRsrq(int32_t rsrq) {
+    // 3GPP TS 27.007 8.69
+    if (rsrq < -20 || rsrq > -3) return RadioConst::VALUE_UNAVAILABLE;
+    return -rsrq;
+}
+
+static bool validateSignalThresholdInfo(const aidl::SignalThresholdInfo& info) {
+    if (info.signalMeasurement <= 0) return false;
+    if (info.hysteresisMs < 0) return false;
+    if (info.hysteresisDb != 0 && info.thresholds.size() > 1) {
+        int minThreshold = info.thresholds[1] - info.thresholds[0];
+        for (size_t i = 2; i < info.thresholds.size(); i++) {
+            int delta = info.thresholds[i] - info.thresholds[i - 1];
+            if (minThreshold < delta) minThreshold = delta;
+        }
+        if (minThreshold < 0) return false;
+        if (info.hysteresisDb > minThreshold) return false;
+    }
+    return true;
+}
+
+bool validateSignalThresholdInfos(const std::vector<aidl::SignalThresholdInfo>& infos) {
+    for (auto& info : infos) {
+        if (!validateSignalThresholdInfo(info)) return false;
+    }
+    return true;
+}
+
+}  // namespace android::hardware::radio::minimal::structs
diff --git a/radio/aidl/minradio/libminradio/response.cpp b/radio/aidl/minradio/libminradio/response.cpp
new file mode 100644
index 0000000..ab33a7f
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/response.cpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 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 <libminradio/response.h>
+
+namespace android::hardware::radio::minimal {
+
+namespace aidl = ::aidl::android::hardware::radio;
+
+aidl::RadioResponseInfo noError(int32_t serial) {
+    return {
+            .type = aidl::RadioResponseType::SOLICITED,
+            .serial = serial,
+            .error = aidl::RadioError::NONE,
+    };
+}
+
+aidl::RadioResponseInfo notSupported(int32_t serial) {
+    return {
+            .type = aidl::RadioResponseType::SOLICITED,
+            .serial = serial,
+            .error = aidl::RadioError::REQUEST_NOT_SUPPORTED,
+    };
+}
+
+aidl::RadioResponseInfo errorResponse(int32_t serial, aidl::RadioError error) {
+    return {
+            .type = aidl::RadioResponseType::SOLICITED,
+            .serial = serial,
+            .error = error,
+    };
+}
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/sim/App.cpp b/radio/aidl/minradio/libminradio/sim/App.cpp
new file mode 100644
index 0000000..8007769
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/sim/App.cpp
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+#include <libminradio/sim/App.h>
+
+#include <android-base/logging.h>
+#include <libminradio/sim/IccConstants.h>
+#include <libminradio/sim/IccUtils.h>
+
+namespace android::hardware::radio::minimal::sim {
+
+using namespace ::android::hardware::radio::minimal::sim::constants;
+namespace aidl = ::aidl::android::hardware::radio::sim;
+
+App::App(std::string_view aid) : mAid(aid) {}
+
+std::string_view App::getAid() const {
+    return mAid;
+}
+
+App::Channel::Channel(uint8_t channelId) : mChannelId(channelId) {}
+
+uint8_t App::Channel::getId() const {
+    return mChannelId;
+}
+
+std::vector<uint8_t> App::Channel::getSelectResponse() const {
+    return {IO_RESULT_SUCCESS >> 8, IO_RESULT_SUCCESS & 0xFF};
+}
+
+aidl::IccIoResult App::iccIo(const aidl::IccIo&) {
+    return toIccIoResult(IO_RESULT_NOT_SUPPORTED);
+}
+
+}  // namespace android::hardware::radio::minimal::sim
diff --git a/radio/aidl/minradio/libminradio/sim/AppManager.cpp b/radio/aidl/minradio/libminradio/sim/AppManager.cpp
new file mode 100644
index 0000000..fe7d7bc
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/sim/AppManager.cpp
@@ -0,0 +1,185 @@
+/*
+ * 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.
+ */
+
+#include <libminradio/sim/AppManager.h>
+
+#include <aidl/android/hardware/radio/RadioConst.h>
+#include <android-base/logging.h>
+#include <libminradio/binder_printing.h>
+#include <libminradio/sim/IccConstants.h>
+#include <libminradio/sim/IccUtils.h>
+#include <libminradio/sim/apps/FilesystemApp.h>
+
+#include <set>
+
+namespace android::hardware::radio::minimal::sim {
+
+using namespace ::android::hardware::radio::minimal::binder_printing;
+using namespace ::android::hardware::radio::minimal::sim::constants;
+using ::aidl::android::hardware::radio::RadioConst;
+using ::aidl::android::hardware::radio::RadioError;
+namespace aidl = ::aidl::android::hardware::radio::sim;
+
+// ETSI TS 102 221 10.1.2 (table 10.5)
+static std::map<uint8_t, std::set<uint8_t>> mCommandClasses = {
+        {COMMAND_READ_BINARY, {0}},                  //
+        {COMMAND_UPDATE_BINARY, {0}},                //
+        {COMMAND_READ_RECORD, {0}},                  //
+        {COMMAND_UPDATE_RECORD, {0}},                //
+        {COMMAND_SEEK, {0}},                         //
+        {COMMAND_SELECT, {0}},                       //
+        {COMMAND_GET_RESPONSE, {0}},                 //
+        {COMMAND_STATUS, {0x80, 0x81, 0x82, 0x83}},  //
+        {COMMAND_GET_DATA, {0x80}},                  //
+        {COMMAND_MANAGE_CHANNEL, {0}},               //
+};
+
+static constexpr uint8_t MANAGE_CHANNEL_OPEN = 0x00;
+static constexpr uint8_t MANAGE_CHANNEL_CLOSE = 0x80;
+
+AppManager::AppManager() {}
+
+void AppManager::addApp(std::shared_ptr<App> app) {
+    mApps[std::string{app->getAid()}] = app;
+
+    // Channel 0 is always available per 3GPP TS 102 221 11.1.17
+    if (app->getAid() == apps::FilesystemApp::AID) {
+        std::unique_lock lck(mChannelsGuard);
+        mChannels[0] = app->newChannel(0);
+    }
+}
+
+std::pair<RadioError, std::shared_ptr<App::Channel>> AppManager::openLogicalChannel(
+        std::string_view aid, int32_t p2) {
+    auto appIt = mApps.find(aid);
+    if (appIt == mApps.end()) {
+        LOG(WARNING) << "App " << aid << " not found";
+        return {RadioError::NO_SUCH_ELEMENT, nullptr};
+    }
+
+    // ETSI TS 102 221 11.1.1.2 Table 11.2
+    // P2 == 0x00: Application activation / reset; First or only occurrence
+    //       0x0C: No data returned
+    if (p2 != 0x00 && p2 != 0x0C && p2 != RadioConst::P2_CONSTANT_NO_P2) {
+        LOG(ERROR) << "P2 != 0x00 or 0x0C not supported";
+        return {RadioError::INVALID_ARGUMENTS, nullptr};
+    }
+
+    std::unique_lock lck(mChannelsGuard);
+
+    // Find available channel. It must be in 1-3 range per 3GPP TS 102 221 11.1.17.1
+    std::optional<unsigned> channelId;
+    for (uint8_t i = 1; i <= 3; i++) {
+        if (mChannels.find(i) == mChannels.end()) {
+            channelId = i;
+            break;
+        }
+    }
+    if (!channelId.has_value()) {
+        LOG(ERROR) << "AppManager: All channels are busy";
+        return {RadioError::MISSING_RESOURCE, nullptr};
+    }
+
+    auto channel = appIt->second->newChannel(*channelId);
+    mChannels[*channelId] = channel;
+    LOG(DEBUG) << "AppManager: opened logical channel " << *channelId;
+    return {RadioError::NONE, std::move(channel)};
+}
+
+RadioError AppManager::closeLogicalChannel(int32_t channelId) {
+    if (channelId == 0) {
+        // 3GPP TS 102 221 11.1.17: channel 0 is guaranteed to be always available
+        return RadioError::INVALID_ARGUMENTS;
+    }
+
+    std::unique_lock lck(mChannelsGuard);
+    auto it = mChannels.find(channelId);
+    if (it == mChannels.end()) {
+        return RadioError::MISSING_RESOURCE;
+    }
+    mChannels.erase(it);
+    LOG(DEBUG) << "AppManager: closed logical channel " << channelId;
+    return RadioError::NONE;
+}
+
+aidl::IccIoResult AppManager::transmit(const aidl::SimApdu& message) {
+    // Fetch channel
+    std::shared_ptr<App::Channel> channel;
+    {
+        std::unique_lock lck(mChannelsGuard);
+        auto chIt = mChannels.find(message.sessionId);
+        if (chIt == mChannels.end()) {
+            return toIccIoResult(IO_RESULT_CHANNEL_NOT_SUPPORTED);
+        }
+        channel = chIt->second;
+    }
+
+    // Verify instruction matching command class
+    auto classIt = mCommandClasses.find(message.instruction);
+    if (classIt == mCommandClasses.end()) {
+        LOG(ERROR) << "Command not found for " << message;
+        return toIccIoResult(IO_RESULT_NOT_SUPPORTED);
+    }
+    if (!classIt->second.contains(message.cla)) {
+        LOG(ERROR) << "Unsupported command class: " << message;
+        return toIccIoResult(IO_RESULT_CLASS_NOT_SUPPORTED);
+    }
+
+    switch (message.instruction) {
+        case COMMAND_MANAGE_CHANNEL:
+            return commandManageChannel(message.p1, message.p2);
+        default:
+            // Pass the message to the channel
+            return channel->transmit(message);
+    }
+}
+
+aidl::IccIoResult AppManager::iccIo(const aidl::IccIo& iccIo) {
+    auto appIt = mApps.find(iccIo.aid);
+    if (appIt == mApps.end()) {
+        LOG(WARNING) << "App " << iccIo.aid << " not found";
+        return toIccIoResult(IO_RESULT_FILE_NOT_FOUND);
+    }
+
+    return appIt->second->iccIo(iccIo);
+}
+
+// ISO 7816 7.1.2
+aidl::IccIoResult AppManager::commandManageChannel(int32_t operation, int32_t channelId) {
+    if (operation == MANAGE_CHANNEL_OPEN) {
+        if (channelId != 0) {
+            LOG(ERROR) << "Not implemented: opening explicit channel IDs: " << channelId;
+            return toIccIoResult(IO_RESULT_INCORRECT_P1_P2);
+        }
+        auto [status, channel] = openLogicalChannel("", 0);
+        if (channel) {
+            return toIccIoResult(uint8ToBytes(channel->getId()));
+        } else {
+            return toIccIoResult(IO_RESULT_CHANNEL_NOT_SUPPORTED);
+        }
+    } else if (operation == MANAGE_CHANNEL_CLOSE) {
+        auto status = closeLogicalChannel(channelId);
+        if (status == RadioError::NONE) {
+            return toIccIoResult("");
+        }
+        return toIccIoResult(IO_RESULT_INCORRECT_P1_P2);
+    } else {
+        LOG(ERROR) << "Invalid MANAGE_CHANNEL operation: " << operation;
+        return toIccIoResult(IO_RESULT_INCORRECT_P1_P2);
+    }
+}
+
+}  // namespace android::hardware::radio::minimal::sim
diff --git a/radio/aidl/minradio/libminradio/sim/Filesystem.cpp b/radio/aidl/minradio/libminradio/sim/Filesystem.cpp
new file mode 100644
index 0000000..65c92b1
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/sim/Filesystem.cpp
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+
+#include <libminradio/sim/Filesystem.h>
+
+#include <libminradio/sim/IccConstants.h>
+#include <libminradio/sim/IccUtils.h>
+
+#include <format>
+
+namespace android::hardware::radio::minimal::sim {
+
+using namespace ::android::hardware::radio::minimal::sim::constants;
+using FileView = Filesystem::FileView;
+
+namespace paths {
+
+// 3GPP TS 51.011 10.7
+const Filesystem::Path mf{MF_SIM_VAL, ""};
+const Filesystem::Path fplmn{EF_FPLMN, MF_SIM + DF_ADF};
+const Filesystem::Path iccid{EF_ICCID, MF_SIM};
+const Filesystem::Path msisdn{EF_MSISDN, MF_SIM + DF_ADF};
+const Filesystem::Path pl{EF_PL, MF_SIM};
+const Filesystem::Path arr{EF_ARR, MF_SIM};
+const Filesystem::Path ad{EF_AD, MF_SIM + DF_ADF};
+
+}  // namespace paths
+
+Filesystem::Filesystem() {
+    write(paths::mf, "");  // Directories are not implemented.
+    write(paths::arr, "");
+}
+
+void Filesystem::write(const Path& path, FileView contents) {
+    std::unique_lock lck(mFilesGuard);
+    mFiles[path].assign(contents.begin(), contents.end());  // C++23: assign_range
+}
+
+void Filesystem::write(const Path& path, std::string_view contents) {
+    std::unique_lock lck(mFilesGuard);
+    mFiles[path].assign(contents.begin(), contents.end());  // C++23: assign_range
+}
+
+void Filesystem::write(const Path& path, std::vector<uint8_t>&& contents) {
+    write(path, FileView(contents));
+}
+
+std::optional<FileView> Filesystem::read(const Path& path) const {
+    std::unique_lock lck(mFilesGuard);
+    auto it = mFiles.find(path);
+    if (it == mFiles.end()) return std::nullopt;
+
+    return FileView(it->second);
+}
+
+void Filesystem::writeBch(const Path& path, std::string_view contents) {
+    write(path, hexStringToBch(contents));
+}
+
+std::optional<std::string> Filesystem::readBch(const Path& path) const {
+    auto contents = read(path);
+    if (!contents.has_value()) return std::nullopt;
+    return bchToHexString(*contents);
+}
+
+std::optional<Filesystem::Path> Filesystem::find(uint16_t fileId) {
+    std::unique_lock lck(mFilesGuard);
+    for (auto& [path, content] : mFiles) {
+        if (path.fileId == fileId) return path;
+    }
+    return std::nullopt;
+}
+
+std::string Filesystem::Path::toString() const {
+    return std::format("{:s}/{:X}", pathId, fileId);
+}
+
+}  // namespace android::hardware::radio::minimal::sim
diff --git a/radio/aidl/minradio/libminradio/sim/IccUtils.cpp b/radio/aidl/minradio/libminradio/sim/IccUtils.cpp
new file mode 100644
index 0000000..9458760
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/sim/IccUtils.cpp
@@ -0,0 +1,216 @@
+/*
+ * 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.
+ */
+
+// C++ reimplementation of f/b/telephony/java/com/android/internal/telephony/uicc/IccUtils.java
+
+#include <libminradio/sim/IccUtils.h>
+
+#include <android-base/logging.h>
+#include <libminradio/sim/IccConstants.h>
+
+namespace android::hardware::radio::minimal::sim {
+
+using namespace ::android::hardware::radio::minimal::sim::constants;
+namespace aidl = ::aidl::android::hardware::radio::sim;
+
+// frameworks/opt/telephony/src/java/com/android/internal/telephony/uicc/AdnRecord.java
+// 3GPP TS 31.102 4.2.26
+constexpr int ADN_FOOTER_SIZE_BYTES = 14;
+constexpr uint8_t ADN_UNUSED = 0xFF;
+constexpr int ADN_BCD_NUMBER_LENGTH = 0;
+constexpr int ADN_TON_AND_NPI = 1;
+constexpr int ADN_DIALING_NUMBER_START = 2;
+constexpr int ADN_DIALING_NUMBER_END = 11;
+
+// com.android.internal.telephony.uicc.IccUtils.charToByte
+// com.android.internal.telephony.uicc.IccUtils.hexCharToInt
+static uint8_t charToByte(char c) {
+    if (c >= '0' && c <= '9') {
+        return c - '0';
+    }
+    if (c >= 'A' && c <= 'F') {
+        return c - 'A' + 10;
+    }
+    if (c >= 'a' && c <= 'f') {
+        return c - 'a' + 10;
+    }
+    LOG(FATAL) << "IccUtils.charToByte: invalid hex character: " << static_cast<int>(c);
+    return 0;
+}
+
+static constexpr char kHexChars[] = "0123456789ABCDEF";
+
+static aidl::IccIoResult toIccIoResult(uint16_t errorCode, std::string_view simResponse) {
+    return {
+            .sw1 = errorCode >> 8,
+            .sw2 = errorCode & 0xFF,
+            .simResponse = std::string(simResponse),
+    };
+}
+
+aidl::IccIoResult toIccIoResult(std::span<uint8_t const> bytes) {
+    return toIccIoResult(IO_RESULT_SUCCESS, sim::bytesToHexString(bytes));
+}
+
+aidl::IccIoResult toIccIoResult(std::vector<uint8_t>&& bytes) {
+    return toIccIoResult(IO_RESULT_SUCCESS, sim::bytesToHexString(bytes));
+}
+
+aidl::IccIoResult toIccIoResult(std::string_view simResponse) {
+    return toIccIoResult(IO_RESULT_SUCCESS, simResponse);
+}
+
+aidl::IccIoResult toIccIoResult(uint16_t errorCode) {
+    return toIccIoResult(errorCode, "");
+}
+
+// com.android.internal.telephony.uicc.IccUtils.hexStringToBytes
+std::vector<uint8_t> hexStringToBytes(std::string_view str) {
+    CHECK(str.size() % 2 == 0) << "Hex string length not even";
+    std::vector<uint8_t> bytes(str.size() / 2);
+    for (size_t i = 0; i < bytes.size(); i++) {
+        bytes[i] = charToByte(str[i * 2]) << 4 | charToByte(str[i * 2 + 1]);
+    }
+    return bytes;
+}
+
+// com.android.internal.telephony.uicc.IccUtils.bchToString (inversion)
+// NOTE: BCH is a nibble-swizzled bytes reprezentation
+std::vector<uint8_t> hexStringToBch(std::string_view str) {
+    CHECK(str.size() % 2 == 0) << "Hex string length not even";
+    std::vector<uint8_t> bch(str.size() / 2);
+    for (size_t i = 0; i < bch.size(); i++) {
+        bch[i] = charToByte(str[i * 2]) | charToByte(str[i * 2 + 1]) << 4;
+    }
+    return bch;
+}
+
+// com.android.internal.telephony.uicc.IccUtils.bytesToHexString
+std::string bytesToHexString(std::span<uint8_t const> bytes) {
+    std::string ret(bytes.size() * 2, '0');
+    for (size_t i = 0; i < bytes.size(); i++) {
+        ret[i * 2 + 0] = kHexChars[0x0F & (bytes[i] >> 4)];
+        ret[i * 2 + 1] = kHexChars[0x0F & (bytes[i])];
+    }
+    return ret;
+}
+
+std::string bytesToHexString(std::vector<uint8_t>&& bytes) {
+    std::span<uint8_t> bytesSpan(bytes);
+    return bytesToHexString(bytesSpan);
+}
+
+// com.android.internal.telephony.uicc.IccUtils.bchToString
+std::string bchToHexString(std::span<uint8_t const> bytes) {
+    std::string ret(bytes.size() * 2, '0');
+    for (size_t i = 0; i < bytes.size(); i++) {
+        ret[i * 2 + 0] = kHexChars[0x0F & (bytes[i])];
+        ret[i * 2 + 1] = kHexChars[0x0F & (bytes[i] >> 4)];
+    }
+    return ret;
+}
+
+std::vector<uint8_t> uint8ToBytes(uint8_t val) {
+    return {val};
+}
+
+std::vector<uint8_t> uint16ToBytes(uint16_t val) {
+    return {
+            static_cast<uint8_t>(val >> 8),
+            static_cast<uint8_t>(val & 0xFF),
+    };
+}
+
+// com.android.internal.telephony.uicc.IccUtils.bcdToString (inversion)
+// integerString is a number with possible leading zeros
+static std::vector<uint8_t> stringToBcd(std::string_view intString) {
+    // Note: 3GPP TS 31.102 Table 4.4 describes BCD coding for characters * and # (not implemented)
+    bool isOdd = intString.size() % 2 == 1;
+    std::vector<uint8_t> ret(intString.size() / 2 + (isOdd ? 1 : 0), 0);
+    for (size_t i = 0; i < intString.size(); i++) {
+        const char digitC = intString[i];
+        CHECK(digitC >= '0' && digitC <= '9') << "Invalid numeric string: " << intString;
+        uint8_t digit = digitC - '0';
+
+        if (i % 2 == 1) digit <<= 4;
+        ret[i / 2] |= digit;
+    }
+    if (isOdd) {
+        *ret.rbegin() |= 0xF0;
+    }
+    return ret;
+}
+
+// com.android.internal.telephony.uicc.IccUtils.stringToBcdPlmn
+static void stringToBcdPlmn(std::string_view plmn, std::vector<uint8_t>& data, size_t offset) {
+    char digit6 = plmn.length() > 5 ? plmn[5] : 'F';
+    data[offset] = (charToByte(plmn[1]) << 4) | charToByte(plmn[0]);
+    data[offset + 1] = (charToByte(digit6) << 4) | charToByte(plmn[2]);
+    data[offset + 2] = (charToByte(plmn[4]) << 4) | charToByte(plmn[3]);
+}
+
+// com.android.internal.telephony.uicc.IccUtils.encodeFplmns
+std::vector<uint8_t> encodeFplmns(std::span<std::string_view> fplmns) {
+    // 3GPP TS 31.102 4.2.16
+    auto recordsCount = std::max<size_t>(fplmns.size(), 4);
+    std::vector<uint8_t> serializedFplmns(recordsCount * FPLMN_BYTE_SIZE, 0xFF);
+
+    size_t record = 0;
+    for (auto&& fplmn : fplmns) {
+        stringToBcdPlmn(fplmn, serializedFplmns, FPLMN_BYTE_SIZE * record++);
+    }
+    return serializedFplmns;
+}
+
+std::vector<uint8_t> encodeMsisdn(std::string_view phoneNumber) {
+    // 3GPP TS 31.102 4.2.26
+    std::vector<uint8_t> msisdn(ADN_FOOTER_SIZE_BYTES, ADN_UNUSED);
+    bool isInternational = phoneNumber.size() >= 1 && phoneNumber[0] == '+';
+    if (isInternational) phoneNumber = phoneNumber.substr(1);
+
+    auto encodedNumber = stringToBcd(phoneNumber);
+    constexpr int numberMaxSize = ADN_DIALING_NUMBER_END - ADN_DIALING_NUMBER_START + 1;
+    if (encodedNumber.size() > numberMaxSize) {
+        encodedNumber.resize(numberMaxSize);
+    }
+
+    msisdn[ADN_BCD_NUMBER_LENGTH] = 1 + encodedNumber.size();
+
+    // 3GPP TS 24.008 Table 10.5.91:
+    // 0b1xxxxxx - mandatory bit
+    // ton (type of number):
+    //  - 0bx001xxxx - international number (with +)
+    //  - 0bx010xxxx - national number
+    // npi (numbering plan identification):
+    //  - 0bxxxx0001 - ISDN/telephony numbering plan
+    msisdn[ADN_TON_AND_NPI] = isInternational ? 0b10010001 : 0b10100001;
+
+    std::copy(encodedNumber.begin(), encodedNumber.end(),
+              std::next(msisdn.begin(), ADN_DIALING_NUMBER_START));
+
+    return msisdn;
+}
+
+std::vector<uint8_t> encodeAd(uint8_t mncLength) {
+    // ETSI TS 131 102 4.2.18
+    CHECK(mncLength == 2 || mncLength == 3) << "Invalid MNC length: " << mncLength;
+
+    std::vector<uint8_t> ad(4);
+    ad[3] = mncLength;
+    return ad;
+}
+
+}  // namespace android::hardware::radio::minimal::sim
diff --git a/radio/aidl/minradio/libminradio/sim/RadioSim.cpp b/radio/aidl/minradio/libminradio/sim/RadioSim.cpp
new file mode 100644
index 0000000..0365a88
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/sim/RadioSim.cpp
@@ -0,0 +1,296 @@
+/*
+ * 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.
+ */
+
+#include <libminradio/sim/RadioSim.h>
+
+#include <libminradio/debug.h>
+#include <libminradio/response.h>
+#include <libminradio/sim/IccUtils.h>
+#include <libminradio/sim/apps/AraM.h>
+#include <libminradio/sim/apps/FilesystemApp.h>
+
+#define RADIO_MODULE "Sim"
+
+namespace android::hardware::radio::minimal {
+
+using namespace ::android::hardware::radio::minimal::binder_printing;
+using ::aidl::android::hardware::radio::RadioError;
+using ::ndk::ScopedAStatus;
+namespace aidl = ::aidl::android::hardware::radio::sim;
+constexpr auto ok = &ScopedAStatus::ok;
+
+RadioSim::RadioSim(std::shared_ptr<SlotContext> context) : RadioSlotBase(context) {
+    mAppManager.addApp(std::make_shared<sim::apps::FilesystemApp>(mFilesystem));
+
+    mFilesystem->write(sim::paths::fplmn, sim::encodeFplmns({}));
+    mFilesystem->write(sim::paths::pl, "en");
+}
+
+void RadioSim::setIccid(std::string iccid) {
+    mFilesystem->writeBch(sim::paths::iccid, iccid);
+}
+
+std::optional<std::string> RadioSim::getIccid() const {
+    return mFilesystem->readBch(sim::paths::iccid);
+}
+
+void RadioSim::addCtsCertificate() {
+    static constexpr char CTS_UICC_2021[] =
+            "CE7B2B47AE2B7552C8F92CC29124279883041FB623A5F194A82C9BF15D492AA0";
+
+    auto aram = std::make_shared<sim::apps::AraM>();
+    mAppManager.addApp(aram);
+    aram->addRule({
+            .deviceAppID = sim::hexStringToBytes(CTS_UICC_2021),
+            .pkg = "android.carrierapi.cts",
+    });
+}
+
+ScopedAStatus RadioSim::areUiccApplicationsEnabled(int32_t serial) {
+    LOG_CALL;
+    respond()->areUiccApplicationsEnabledResponse(noError(serial), mAreUiccApplicationsEnabled);
+    return ok();
+}
+
+ScopedAStatus RadioSim::changeIccPin2ForApp(int32_t serial, const std::string& oldPin2,
+                                            const std::string& newPin2, const std::string& aid) {
+    LOG_NOT_SUPPORTED << oldPin2 << ' ' << newPin2 << ' ' << aid;
+    respond()->changeIccPin2ForAppResponse(notSupported(serial), -1);
+    return ok();
+}
+
+ScopedAStatus RadioSim::changeIccPinForApp(int32_t serial, const std::string& oldPin,
+                                           const std::string& newPin, const std::string& aid) {
+    LOG_NOT_SUPPORTED << oldPin << ' ' << newPin << ' ' << aid;
+    respond()->changeIccPinForAppResponse(notSupported(serial), -1);
+    return ok();
+}
+
+ScopedAStatus RadioSim::enableUiccApplications(int32_t serial, bool enable) {
+    LOG_CALL_IGNORED << enable;
+    mAreUiccApplicationsEnabled = enable;
+    respond()->enableUiccApplicationsResponse(noError(serial));
+    return ok();
+}
+
+ScopedAStatus RadioSim::getAllowedCarriers(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    respond()->getAllowedCarriersResponse(notSupported(serial), {}, {});
+    return ok();
+}
+
+ScopedAStatus RadioSim::getCdmaSubscription(int32_t serial) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioSim::getCdmaSubscriptionSource(int32_t serial) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioSim::getFacilityLockForApp(  //
+        int32_t serial, const std::string& facility, const std::string& password,
+        int32_t serviceClass, const std::string& appId) {
+    LOG_CALL << facility << ' ' << password << ' ' << serviceClass << ' ' << appId;
+    respond()->getFacilityLockForAppResponse(noError(serial), 0);  // 0 means "disabled for all"
+    return ok();
+}
+
+ScopedAStatus RadioSim::getSimPhonebookCapacity(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    respond()->getSimPhonebookCapacityResponse(notSupported(serial), {});
+    return ok();
+}
+
+ScopedAStatus RadioSim::getSimPhonebookRecords(int32_t serial) {
+    LOG_NOT_SUPPORTED;
+    respond()->getSimPhonebookRecordsResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioSim::iccCloseLogicalChannel(int32_t serial, int32_t) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioSim::iccCloseLogicalChannelWithSessionInfo(
+        int32_t serial, const aidl::SessionInfo& sessionInfo) {
+    LOG_CALL << sessionInfo;
+    auto status = mAppManager.closeLogicalChannel(sessionInfo.sessionId);
+    respond()->iccCloseLogicalChannelWithSessionInfoResponse(errorResponse(serial, status));
+    return ok();
+}
+
+ScopedAStatus RadioSim::iccIoForApp(int32_t serial, const aidl::IccIo& iccIo) {
+    LOG_CALL << iccIo;
+    respond()->iccIoForAppResponse(noError(serial), mAppManager.iccIo(iccIo));
+    return ok();
+}
+
+ScopedAStatus RadioSim::iccOpenLogicalChannel(int32_t serial, const std::string& aid, int32_t p2) {
+    LOG_CALL << aid << ' ' << p2;
+    auto [status, channel] = mAppManager.openLogicalChannel(aid, p2);
+    respond()->iccOpenLogicalChannelResponse(
+            errorResponse(serial, status), channel ? channel->getId() : 0,
+            channel ? channel->getSelectResponse() : std::vector<uint8_t>{});
+    return ok();
+}
+
+ScopedAStatus RadioSim::iccTransmitApduBasicChannel(int32_t serial, const aidl::SimApdu& message) {
+    LOG_CALL << message;
+    if (message.sessionId != 0) {
+        LOG(ERROR) << "Basic channel session ID should be zero, but was " << message.sessionId;
+        respond()->iccTransmitApduBasicChannelResponse(
+                errorResponse(serial, RadioError::INVALID_ARGUMENTS), {});
+        return ok();
+    }
+    respond()->iccTransmitApduBasicChannelResponse(noError(serial), mAppManager.transmit(message));
+    return ok();
+}
+
+ScopedAStatus RadioSim::iccTransmitApduLogicalChannel(int32_t serial,
+                                                      const aidl::SimApdu& message) {
+    LOG_CALL << message;
+    respond()->iccTransmitApduLogicalChannelResponse(noError(serial),
+                                                     mAppManager.transmit(message));
+    return ok();
+}
+
+ScopedAStatus RadioSim::reportStkServiceIsRunning(int32_t serial) {
+    LOG_CALL_IGNORED;
+    respond()->reportStkServiceIsRunningResponse(noError(serial));
+    return ok();
+}
+
+ScopedAStatus RadioSim::requestIccSimAuthentication(  //
+        int32_t serial, int32_t authContext, const std::string& authData, const std::string& aid) {
+    LOG_NOT_SUPPORTED << authContext << ' ' << authData << ' ' << aid;
+    respond()->requestIccSimAuthenticationResponse(notSupported(serial), {});
+    return ok();
+}
+
+ScopedAStatus RadioSim::responseAcknowledgement() {
+    LOG_CALL_NOSERIAL;
+    return ok();
+}
+
+ScopedAStatus RadioSim::sendEnvelope(int32_t serial, const std::string& command) {
+    LOG_NOT_SUPPORTED << command;
+    respond()->sendEnvelopeResponse(notSupported(serial), {});
+    return ok();
+}
+
+ScopedAStatus RadioSim::sendEnvelopeWithStatus(int32_t serial, const std::string& contents) {
+    LOG_NOT_SUPPORTED << contents;
+    respond()->sendEnvelopeWithStatusResponse(notSupported(serial), {});
+    return ok();
+}
+
+ScopedAStatus RadioSim::sendTerminalResponseToSim(int32_t serial,
+                                                  const std::string& commandResponse) {
+    LOG_NOT_SUPPORTED << commandResponse;
+    respond()->sendTerminalResponseToSimResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioSim::setAllowedCarriers(  //
+        int32_t serial, const aidl::CarrierRestrictions& carriers, aidl::SimLockMultiSimPolicy mp) {
+    LOG_NOT_SUPPORTED << carriers << ' ' << mp;
+    respond()->setAllowedCarriersResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioSim::setCarrierInfoForImsiEncryption(
+        int32_t serial, const aidl::ImsiEncryptionInfo& imsiEncryptionInfo) {
+    LOG_NOT_SUPPORTED << imsiEncryptionInfo;
+    respond()->setCarrierInfoForImsiEncryptionResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioSim::setCdmaSubscriptionSource(int32_t serial, aidl::CdmaSubscriptionSource) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioSim::setFacilityLockForApp(  //
+        int32_t serial, const std::string& facility, bool lockState, const std::string& password,
+        int32_t serviceClass, const std::string& appId) {
+    LOG_NOT_SUPPORTED << facility << ' ' << lockState << ' ' << password << ' ' << serviceClass
+                      << ' ' << appId;
+    respond()->setFacilityLockForAppResponse(notSupported(serial), -1);
+    return ok();
+}
+
+ScopedAStatus RadioSim::setResponseFunctions(
+        const std::shared_ptr<aidl::IRadioSimResponse>& response,
+        const std::shared_ptr<aidl::IRadioSimIndication>& indication) {
+    LOG_CALL_NOSERIAL << response << ' ' << indication;
+    CHECK(response);
+    CHECK(indication);
+    respond = response;
+    indicate = indication;
+    return ok();
+}
+
+ScopedAStatus RadioSim::setSimCardPower(int32_t serial, aidl::CardPowerState powerUp) {
+    LOG_NOT_SUPPORTED << powerUp;
+    respond()->setSimCardPowerResponse(notSupported(serial));
+    return ok();
+}
+
+ScopedAStatus RadioSim::setUiccSubscription(int32_t serial, const aidl::SelectUiccSub&) {
+    LOG_AND_RETURN_DEPRECATED();
+}
+
+ScopedAStatus RadioSim::supplyIccPin2ForApp(int32_t serial, const std::string& pin2,
+                                            const std::string& aid) {
+    LOG_NOT_SUPPORTED << pin2 << ' ' << aid;
+    respond()->supplyIccPin2ForAppResponse(notSupported(serial), -1);
+    return ok();
+}
+
+ScopedAStatus RadioSim::supplyIccPinForApp(int32_t serial, const std::string& pin,
+                                           const std::string& aid) {
+    LOG_CALL << "string[" << pin.size() << "] " << aid
+             << " (should not be called with PinState::DISABLED)";
+    respond()->supplyIccPinForAppResponse(notSupported(serial), -1);
+    return ok();
+}
+
+ScopedAStatus RadioSim::supplyIccPuk2ForApp(int32_t serial, const std::string& puk2,
+                                            const std::string& pin2, const std::string& aid) {
+    LOG_NOT_SUPPORTED << puk2 << ' ' << pin2 << ' ' << aid;
+    respond()->supplyIccPuk2ForAppResponse(notSupported(serial), -1);
+    return ok();
+}
+
+ScopedAStatus RadioSim::supplyIccPukForApp(int32_t serial, const std::string& puk,
+                                           const std::string& pin, const std::string& aid) {
+    LOG_NOT_SUPPORTED << puk << ' ' << pin << ' ' << aid;
+    respond()->supplyIccPukForAppResponse(notSupported(serial), -1);
+    return ok();
+}
+
+ScopedAStatus RadioSim::supplySimDepersonalization(int32_t serial, aidl::PersoSubstate pss,
+                                                   const std::string& controlKey) {
+    LOG_NOT_SUPPORTED << pss << ' ' << controlKey;
+    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+ScopedAStatus RadioSim::updateSimPhonebookRecords(int32_t serial,
+                                                  const aidl::PhonebookRecordInfo& recordInfo) {
+    LOG_NOT_SUPPORTED << recordInfo;
+    return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+}  // namespace android::hardware::radio::minimal
diff --git a/radio/aidl/minradio/libminradio/sim/apps/AraM.cpp b/radio/aidl/minradio/libminradio/sim/apps/AraM.cpp
new file mode 100644
index 0000000..7aa1439
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/sim/apps/AraM.cpp
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+
+#include <libminradio/sim/apps/AraM.h>
+
+#include "tlv.h"
+
+#include <android-base/logging.h>
+#include <libminradio/binder_printing.h>
+#include <libminradio/sim/IccConstants.h>
+#include <libminradio/sim/IccUtils.h>
+
+namespace android::hardware::radio::minimal::sim::apps {
+
+using namespace ::android::hardware::radio::minimal::binder_printing;
+using namespace ::android::hardware::radio::minimal::sim::constants;
+using namespace ::android::hardware::radio::minimal::sim::tlv_operators;
+namespace aidl = ::aidl::android::hardware::radio::sim;
+
+// From https://source.android.com/docs/core/connect/uicc
+static constexpr uint16_t TAG_ALL_REF_AR_DO = 0xFF40;
+static constexpr uint8_t TAG_REF_AR_DO = 0xE2;
+static constexpr uint8_t TAG_REF_DO = 0xE1;
+static constexpr uint8_t TAG_DEVICE_APP_ID_REF_DO = 0xC1;
+static constexpr uint8_t TAG_PKG_REF_DO = 0xCA;
+static constexpr uint8_t TAG_AR_DO = 0xE3;
+static constexpr uint8_t TAG_PERM_AR_DO = 0xDB;
+
+class AraMChannel : public App::Channel {
+  public:
+    AraMChannel(int32_t channelId, std::shared_ptr<AraM> app);
+
+    aidl::IccIoResult transmit(const aidl::SimApdu& message) override;
+
+  private:
+    std::weak_ptr<AraM> mApp;
+};
+
+AraM::AraM() : App(AID) {}
+
+std::shared_ptr<App::Channel> AraM::newChannel(int32_t id) {
+    return std::make_shared<AraMChannel>(id, shared_from_this());
+}
+
+void AraM::addRule(Rule rule) {
+    mRules.push_back(rule);
+}
+
+std::span<const AraM::Rule> AraM::getRules() const {
+    return mRules;
+}
+
+AraMChannel::AraMChannel(int32_t channelId, std::shared_ptr<AraM> app)
+    : App::Channel(channelId), mApp(app) {}
+
+aidl::IccIoResult AraMChannel::transmit(const aidl::SimApdu& message) {
+    auto app = mApp.lock();
+    if (!app) {
+        LOG(ERROR) << "AraM: App shut down, channel not valid anymore.";
+        return toIccIoResult(IO_RESULT_TECHNICAL_PROBLEM);
+    }
+    if (message.instruction != COMMAND_GET_DATA) {
+        LOG(ERROR) << "AraM: Unsupported instruction: " << message;
+        return toIccIoResult(IO_RESULT_NOT_SUPPORTED);
+    }
+    if (message.p1 != (TAG_ALL_REF_AR_DO >> 8) || message.p2 != (TAG_ALL_REF_AR_DO & 0xFF)) {
+        LOG(ERROR) << "AraM: Incorrect parameters: " << std::hex << message.p1 << message.p2;
+        return toIccIoResult(IO_RESULT_INCORRECT_P1_P2);
+    }
+    if (message.p3 != 0) {
+        return toIccIoResult(IO_RESULT_INCORRECT_LENGTH | 0);
+    }
+
+    std::vector<uint8_t> rules;
+    for (auto& rule : app->getRules()) {
+        // Encoding rules as described in https://source.android.com/docs/core/connect/uicc
+        // clang-format off
+        rules = rules + makeTlv(TAG_REF_AR_DO,
+            makeTlv(TAG_REF_DO,
+                makeTlv(TAG_DEVICE_APP_ID_REF_DO, rule.deviceAppID) +
+                makeTlv(TAG_PKG_REF_DO, std::vector<uint8_t>(rule.pkg.begin(), rule.pkg.end()))
+            ) +
+            makeTlv(TAG_AR_DO,
+                makeTlv(TAG_PERM_AR_DO, std::vector<uint8_t>{0, 0, 0, 0, 0, 0, 0, 1})
+            )
+        );
+        // clang-format on
+    }
+
+    return toIccIoResult(bytesToHexString(makeTlv(TAG_ALL_REF_AR_DO, rules)));
+}
+
+}  // namespace android::hardware::radio::minimal::sim::apps
diff --git a/radio/aidl/minradio/libminradio/sim/apps/FilesystemApp.cpp b/radio/aidl/minradio/libminradio/sim/apps/FilesystemApp.cpp
new file mode 100644
index 0000000..0a32e6c
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/sim/apps/FilesystemApp.cpp
@@ -0,0 +1,231 @@
+/*
+ * 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.
+ */
+
+#include <libminradio/sim/apps/FilesystemApp.h>
+
+#include "tlv.h"
+
+#include <android-base/logging.h>
+#include <libminradio/binder_printing.h>
+#include <libminradio/sim/IccConstants.h>
+#include <libminradio/sim/IccUtils.h>
+
+#include <unordered_set>
+
+namespace android::hardware::radio::minimal::sim::apps {
+
+using namespace ::android::hardware::radio::minimal::binder_printing;
+using namespace ::android::hardware::radio::minimal::sim::constants;
+namespace aidl = ::aidl::android::hardware::radio::sim;
+
+// ETSI TS 102 221 11.1.1.2 Table 11.1: Coding of P1 for SELECT
+static constexpr uint8_t SELECT_BY_FILE_ID = 0x00;
+
+// ETSI TS 102 221 11.1.1.2 Table 11.2: Coding of P2 for SELECT
+static constexpr uint8_t SELECT_RETURN_FCP_TEMPLATE = 0x04;
+static constexpr uint8_t SELECT_RETURN_NOTHING = 0x0C;
+
+// From android.carrierapi.cts.FcpTemplate
+static constexpr uint8_t BER_TAG_FCP_TEMPLATE = 0x62;
+static constexpr uint8_t FILE_IDENTIFIER = 0x83;
+
+static const std::unordered_set<int32_t> kLinearFixedFiles{EF_MSISDN};
+
+class FilesystemApp::FilesystemChannel : public App::Channel {
+  public:
+    FilesystemChannel(int32_t channelId, std::shared_ptr<Filesystem> filesystem);
+
+    void select(Filesystem::Path path);
+    aidl::IccIoResult transmit(const aidl::SimApdu& message) override;
+
+  private:
+    std::shared_ptr<Filesystem> mFilesystem;
+    Filesystem::Path mSelectedFile = paths::mf;
+
+    aidl::IccIoResult commandSelect(int32_t p1, int32_t p2, int32_t p3, const std::string& data);
+    aidl::IccIoResult commandStatus(int32_t p1) const;
+    aidl::IccIoResult commandReadBinary(int32_t p1, int32_t p2) const;
+    aidl::IccIoResult commandUpdateBinary(int32_t p1, int32_t p2, std::string_view data);
+    aidl::IccIoResult commandReadRecord(int32_t p1, int32_t p2, int32_t p3);
+    aidl::IccIoResult commandGetResponse() const;
+};
+
+FilesystemApp::FilesystemApp(const std::shared_ptr<Filesystem>& filesystem)
+    : App(AID), mFilesystem(filesystem) {}
+
+std::shared_ptr<App::Channel> FilesystemApp::newChannel(int32_t id) {
+    auto channel = std::make_shared<FilesystemApp::FilesystemChannel>(id, mFilesystem);
+    if (id == 0) mBasicChannel = channel;
+    return channel;
+}
+
+FilesystemApp::FilesystemChannel::FilesystemChannel(  //
+        int32_t channelId, std::shared_ptr<Filesystem> filesystem)
+    : App::Channel(channelId), mFilesystem(filesystem) {}
+
+void FilesystemApp::FilesystemChannel::select(Filesystem::Path path) {
+    mSelectedFile = path;
+}
+
+// android.carrierapi.cts.FcpTemplate.parseFcpTemplate (inversion)
+static std::vector<uint8_t> makeFcpTemplate(const Filesystem::Path& path) {
+    // clang-format off
+    return makeTlv(BER_TAG_FCP_TEMPLATE,
+        makeTlv(FILE_IDENTIFIER, uint16ToBytes(path.fileId))
+    );
+    // clang-format on
+}
+
+// ETSI TS 102 221 11.1.1
+aidl::IccIoResult FilesystemApp::FilesystemChannel::commandSelect(  //
+        int32_t p1, int32_t p2, int32_t length, const std::string& data) {
+    if (p1 != SELECT_BY_FILE_ID ||
+        (p2 != SELECT_RETURN_FCP_TEMPLATE && p2 != SELECT_RETURN_NOTHING)) {
+        return toIccIoResult(IO_RESULT_INCORRECT_P1_P2);
+    }
+    if (length != 2) {  // file ids are 2 byte long
+        return toIccIoResult(IO_RESULT_INCORRECT_LENGTH | 2);
+    }
+
+    auto fileId = strtol(data.c_str(), nullptr, 16);
+    if (fileId <= 0 || fileId > 0xFFFF) {
+        LOG(WARNING) << "Incorrect file ID: " << data;
+        return toIccIoResult(IO_RESULT_INCORRECT_DATA);
+    }
+
+    auto path = mFilesystem->find(fileId);
+    if (!path.has_value()) {
+        LOG(WARNING) << "FilesystemChannel: file " << std::hex << fileId << " not found";
+        return toIccIoResult(IO_RESULT_FILE_NOT_FOUND);
+    }
+    select(*path);
+
+    if (p2 == SELECT_RETURN_FCP_TEMPLATE) {
+        return toIccIoResult(bytesToHexString(makeFcpTemplate(mSelectedFile)));
+    }
+    return toIccIoResult("");
+}
+
+// ETSI TS 102 221 11.1.2
+aidl::IccIoResult FilesystemApp::FilesystemChannel::commandStatus(int32_t p1) const {
+    if (p1 != 0x00 && p1 != 0x01) {  // 0x02 (termination) not implemented
+        return toIccIoResult(IO_RESULT_INCORRECT_P1_P2);
+    }
+    return toIccIoResult(bytesToHexString(makeFcpTemplate(mSelectedFile)));
+}
+
+// ETSI TS 102 221 11.1.3
+aidl::IccIoResult FilesystemApp::FilesystemChannel::commandReadBinary(  //
+        int32_t offsetHi, int32_t offsetLo) const {
+    CHECK(offsetHi == 0 && offsetLo == 0) << "Offset not supported";
+    if (auto contents = mFilesystem->read(mSelectedFile); contents.has_value()) {
+        return toIccIoResult(*contents);
+    }
+    LOG(DEBUG) << "Missing ICC file (READ_BINARY): " << mSelectedFile.toString();
+    return toIccIoResult(IO_RESULT_FILE_NOT_FOUND);
+}
+
+// ETSI TS 102 221 11.1.4
+aidl::IccIoResult FilesystemApp::FilesystemChannel::commandUpdateBinary(  //
+        int32_t offsetHi, int32_t offsetLo, std::string_view data) {
+    CHECK(offsetHi == 0 && offsetLo == 0) << "Offset not supported";
+    mFilesystem->write(mSelectedFile, hexStringToBytes(data));
+    return toIccIoResult("");
+}
+
+// ETSI TS 102 221 11.1.5
+aidl::IccIoResult FilesystemApp::FilesystemChannel::commandReadRecord(  //
+        int32_t recordId, int32_t mode, int32_t length) {
+    CHECK(recordId == 1) << "Records other than no 1 are not supported";
+    CHECK(mode == 4) << "Unsupported record mode";  // absolute is the only currently supported mode
+    CHECK(length >= 0);
+    if (auto contents = mFilesystem->read(mSelectedFile); contents.has_value()) {
+        CHECK(static_cast<size_t>(length) == contents->size())
+                << "Partial reads not supported (" << length << " != " << contents->size() << ")";
+        return toIccIoResult(*contents);
+    }
+    LOG(DEBUG) << "Missing ICC file (READ_RECORD): " << mSelectedFile.toString();
+    return toIccIoResult(IO_RESULT_FILE_NOT_FOUND);
+}
+
+// com.android.internal.telephony.uicc.IccFileHandler (inversion)
+// ETSI TS 102 221 12.1.1
+aidl::IccIoResult FilesystemApp::FilesystemChannel::commandGetResponse() const {
+    auto file = mSelectedFile;
+    auto contents = mFilesystem->read(file);
+    if (!contents.has_value()) {
+        LOG(DEBUG) << "Missing ICC file (GET_RESPONSE): " << file.toString();
+        return toIccIoResult(IO_RESULT_FILE_NOT_FOUND);
+    }
+    auto fileSize = contents->size();
+    CHECK(fileSize <= 0xFFFF) << "File size won't fit in GET_RESPONSE";
+
+    // 3GPP TS 51.011 9.2.1
+    std::vector<uint8_t> response(GET_RESPONSE_EF_SIZE_BYTES, 0);
+    response[RESPONSE_DATA_FILE_SIZE_1] = fileSize >> 8;
+    response[RESPONSE_DATA_FILE_SIZE_2] = 0xFF & fileSize;
+    response[RESPONSE_DATA_FILE_ID_1] = file.fileId >> 8;
+    response[RESPONSE_DATA_FILE_ID_2] = 0xFF & file.fileId;
+    response[RESPONSE_DATA_FILE_TYPE] = TYPE_EF;
+    response[RESPONSE_DATA_LENGTH] = GET_RESPONSE_EF_SIZE_BYTES - RESPONSE_DATA_STRUCTURE;
+    if (kLinearFixedFiles.contains(file.fileId)) {
+        response[RESPONSE_DATA_STRUCTURE] = EF_TYPE_LINEAR_FIXED;
+        response[RESPONSE_DATA_RECORD_LENGTH] = fileSize;  // single record support only
+    } else {
+        response[RESPONSE_DATA_STRUCTURE] = EF_TYPE_TRANSPARENT;
+    }
+
+    return toIccIoResult(response);
+}
+
+aidl::IccIoResult FilesystemApp::FilesystemChannel::transmit(const aidl::SimApdu& message) {
+    switch (message.instruction) {
+        case COMMAND_SELECT:
+            return commandSelect(message.p1, message.p2, message.p3, message.data);
+        case COMMAND_STATUS:
+            return commandStatus(message.p1);
+        case COMMAND_READ_BINARY:
+            return commandReadBinary(message.p1, message.p2);
+        case COMMAND_UPDATE_BINARY:
+            return commandUpdateBinary(message.p1, message.p2, message.data);
+        case COMMAND_READ_RECORD:
+            return commandReadRecord(message.p1, message.p2, message.p3);
+        case COMMAND_GET_RESPONSE:
+            return commandGetResponse();
+        default:
+            LOG(ERROR) << "Unsupported filesystem instruction: " << message;
+            return toIccIoResult(IO_RESULT_NOT_SUPPORTED);
+    }
+}
+
+aidl::IccIoResult FilesystemApp::iccIo(const aidl::IccIo& iccIo) {
+    CHECK(mBasicChannel) << "Basic channel must always be present";
+
+    if (iccIo.fileId != 0) {
+        mBasicChannel->select({iccIo.fileId, iccIo.path});
+    }
+
+    aidl::SimApdu message = {
+            .instruction = iccIo.command,
+            .p1 = iccIo.p1,
+            .p2 = iccIo.p2,
+            .p3 = iccIo.p3,
+            .data = iccIo.data,
+    };
+    return mBasicChannel->transmit(message);
+}
+
+}  // namespace android::hardware::radio::minimal::sim::apps
diff --git a/radio/aidl/minradio/libminradio/sim/apps/tlv.cpp b/radio/aidl/minradio/libminradio/sim/apps/tlv.cpp
new file mode 100644
index 0000000..28f00e7
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/sim/apps/tlv.cpp
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+#include "tlv.h"
+
+#include <android-base/logging.h>
+
+namespace android::hardware::radio::minimal::sim {
+
+std::vector<uint8_t> makeTlv(uint32_t tag, std::span<uint8_t const> value) {
+    // If needed, implement ISO 7816 5.2.2.1
+    CHECK(tag <= 0xFFFF) << "3-byte tag numbers (" << tag << ") are not implemented";
+
+    // If we end up needing more, implement ISO 7816 5.2.2.2
+    CHECK(value.size() <= 0x7F) << "Large tag lengths are not implemented: " << value.size()
+                                << " for " << tag;
+
+    std::vector<uint8_t> serialized;
+    if (tag <= 0xFF) {
+        serialized = {static_cast<uint8_t>(tag), static_cast<uint8_t>(value.size())};
+    } else {
+        serialized = {static_cast<uint8_t>(tag >> 8), static_cast<uint8_t>(tag & 0xFF),
+                      static_cast<uint8_t>(value.size())};
+    }
+
+    serialized.insert(serialized.end(), value.begin(), value.end());
+    return serialized;
+}
+
+namespace tlv_operators {
+
+std::vector<uint8_t> operator+(std::span<uint8_t const> a, std::span<uint8_t const> b) {
+    std::vector<uint8_t> concatenated;
+    concatenated.insert(concatenated.end(), a.begin(), a.end());
+    concatenated.insert(concatenated.end(), b.begin(), b.end());
+    return concatenated;
+}
+
+}  // namespace tlv_operators
+
+}  // namespace android::hardware::radio::minimal::sim
diff --git a/radio/aidl/minradio/libminradio/sim/apps/tlv.h b/radio/aidl/minradio/libminradio/sim/apps/tlv.h
new file mode 100644
index 0000000..6d39bc7
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/sim/apps/tlv.h
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <span>
+#include <vector>
+
+namespace android::hardware::radio::minimal::sim {
+
+/* makeTlv and operator+ are a very inefficient (and incomplete) implementation of
+ * BER-TLV encoding. This is fine here, because the data set is very small and used infrequently.
+ *
+ * @tag Tag number (already encoded per ISO 7816 5.2.2.1)
+ */
+std::vector<uint8_t> makeTlv(uint32_t tag, std::span<uint8_t const> value);
+
+namespace tlv_operators {
+
+std::vector<uint8_t> operator+(std::span<uint8_t const> a, std::span<uint8_t const> b);
+
+}  // namespace tlv_operators
+
+}  // namespace android::hardware::radio::minimal::sim
diff --git a/radio/aidl/minradio/minradio-example/Android.bp b/radio/aidl/minradio/minradio-example/Android.bp
new file mode 100644
index 0000000..7051972
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/Android.bp
@@ -0,0 +1,77 @@
+// 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "hardware_interfaces_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["hardware_interfaces_license"],
+}
+
+cc_binary {
+    name: "android.hardware.radio-service.minradio-example",
+    defaults: ["android.hardware.radio-minradio@defaults"],
+    vintf_fragment_modules: ["android.hardware.radio-service.minradio-example.vintf"],
+    installable: false,
+    apex_available: ["com.android.hardware.radio.minradio.virtual"],
+    shared_libs: [
+        "android.hardware.radio-library.minradio",
+    ],
+    static_libs: [
+        "libnetdevice",
+    ],
+    srcs: [
+        "impl/RadioConfig.cpp",
+        "impl/RadioData.cpp",
+        "impl/RadioModem.cpp",
+        "impl/RadioNetwork.cpp",
+        "impl/RadioSim.cpp",
+        "service.cpp",
+    ],
+}
+
+vintf_fragment {
+    name: "android.hardware.radio-service.minradio-example.vintf",
+    src: "minradio-example.xml",
+    vendor: true,
+}
+
+apex {
+    name: "com.android.hardware.radio.minradio.virtual",
+    manifest: "apex_manifest.json",
+    file_contexts: "file_contexts",
+    key: "com.android.hardware.key",
+    certificate: ":com.android.hardware.certificate",
+    updatable: false,
+    soc_specific: true,
+
+    binaries: [
+        "android.hardware.radio-service.minradio-example",
+    ],
+    prebuilts: [
+        "android.hardware.telephony.data.prebuilt.xml",
+
+        // TODO(b/369726708): adding to init_rc field of cc_binary doesn't work in apex yet
+        "minradio-example.rc",
+    ],
+    overrides: ["rild"],
+}
+
+prebuilt_etc {
+    name: "minradio-example.rc",
+    src: "minradio-example.rc",
+    installable: false,
+}
diff --git a/radio/aidl/minradio/minradio-example/apex_manifest.json b/radio/aidl/minradio/minradio-example/apex_manifest.json
new file mode 100644
index 0000000..85ba9a3
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/apex_manifest.json
@@ -0,0 +1,4 @@
+{
+    "name": "com.android.hardware.radio.minradio.virtual",
+    "version": 1
+}
diff --git a/radio/aidl/minradio/minradio-example/file_contexts b/radio/aidl/minradio/minradio-example/file_contexts
new file mode 100644
index 0000000..1b7544d
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/file_contexts
@@ -0,0 +1,3 @@
+(/.*)?                        u:object_r:vendor_file:s0
+/etc(/.*)?                    u:object_r:vendor_configs_file:s0
+/bin/hw/android.hardware.radio-service.minradio-.*  u:object_r:hal_radio_default_exec:s0
diff --git a/radio/aidl/minradio/minradio-example/impl/RadioConfig.cpp b/radio/aidl/minradio/minradio-example/impl/RadioConfig.cpp
new file mode 100644
index 0000000..12e8ede
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/impl/RadioConfig.cpp
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+#include "RadioConfig.h"
+
+#include <aidl/android/hardware/radio/sim/CardStatus.h>
+#include <libminradio/debug.h>
+#include <libminradio/response.h>
+
+#define RADIO_MODULE "ConfigImpl"
+
+namespace android::hardware::radio::service {
+
+using ::android::hardware::radio::minimal::noError;
+using ::ndk::ScopedAStatus;
+namespace aidl = ::aidl::android::hardware::radio::config;
+namespace aidlSim = ::aidl::android::hardware::radio::sim;
+constexpr auto ok = &ScopedAStatus::ok;
+
+ScopedAStatus RadioConfig::getSimSlotsStatus(int32_t serial) {
+    LOG_CALL;
+    aidl::SimSlotStatus simslot1Status{
+            .cardState = aidlSim::CardStatus::STATE_PRESENT,
+            .atr = "",
+            .eid = "eUICC-simslot1",
+            .portInfo = {{
+                    .iccId = "12345678901234567890",
+                    .logicalSlotId = 0,
+                    .portActive = true,
+            }},
+    };
+    respond()->getSimSlotsStatusResponse(noError(serial), {simslot1Status});
+    return ok();
+}
+
+}  // namespace android::hardware::radio::service
diff --git a/radio/aidl/minradio/minradio-example/impl/RadioConfig.h b/radio/aidl/minradio/minradio-example/impl/RadioConfig.h
new file mode 100644
index 0000000..e31ce19
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/impl/RadioConfig.h
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <libminradio/config/RadioConfig.h>
+
+namespace android::hardware::radio::service {
+
+class RadioConfig : public minimal::RadioConfig {
+  protected:
+    ::ndk::ScopedAStatus getSimSlotsStatus(int32_t serial) override;
+};
+
+}  // namespace android::hardware::radio::service
diff --git a/radio/aidl/minradio/minradio-example/impl/RadioData.cpp b/radio/aidl/minradio/minradio-example/impl/RadioData.cpp
new file mode 100644
index 0000000..1335bd9
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/impl/RadioData.cpp
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+#include "RadioData.h"
+
+#include <aidl/android/hardware/radio/RadioConst.h>
+#include <libminradio/debug.h>
+#include <libminradio/response.h>
+#include <libnetdevice/libnetdevice.h>
+
+#define RADIO_MODULE "DataImpl"
+
+namespace android::hardware::radio::service {
+
+using namespace ::android::hardware::radio::minimal::binder_printing;
+using ::aidl::android::hardware::radio::RadioConst;
+using ::aidl::android::hardware::radio::RadioError;
+using ::android::hardware::radio::minimal::errorResponse;
+using ::android::hardware::radio::minimal::noError;
+using ::ndk::ScopedAStatus;
+namespace aidl = ::aidl::android::hardware::radio::data;
+namespace aidlCommon = ::aidl::android::hardware::radio;
+constexpr auto ok = &ScopedAStatus::ok;
+
+ScopedAStatus RadioData::setupDataCall(int32_t serial, aidlCommon::AccessNetwork accessNetwork,
+                                       const aidl::DataProfileInfo& dataProfileInfo,
+                                       bool roamingAllowed, aidl::DataRequestReason reason,
+                                       const std::vector<aidl::LinkAddress>& addresses,
+                                       const std::vector<std::string>& dnses, int32_t pduSessId,
+                                       const std::optional<aidl::SliceInfo>& sliceInfo,
+                                       bool matchAllRuleAllowed) {
+    LOG_CALL << accessNetwork                             //
+             << " {" << dataProfileInfo.profileId << '}'  //
+             << ' ' << roamingAllowed                     //
+             << ' ' << reason                             //
+             << ' ' << addresses.size()                   //
+             << ' ' << dnses.size() << ' ' << pduSessId   //
+             << ' ' << sliceInfo.has_value()              //
+             << ' ' << matchAllRuleAllowed;
+
+    bool ifaceOk = netdevice::setAddr4("buried_eth0", "192.168.97.2", 30);
+    ifaceOk = ifaceOk && netdevice::up("buried_eth0");
+    if (!ifaceOk) {
+        respond()->setupDataCallResponse(errorResponse(serial, RadioError::INTERNAL_ERR), {});
+        return ok();
+    }
+
+    aidl::SetupDataCallResult result{
+            .cause = aidl::DataCallFailCause::NONE,
+            .suggestedRetryTime = RadioConst::VALUE_UNAVAILABLE_LONG,
+            .cid = setupDataCallCid(),
+            .active = aidl::SetupDataCallResult::DATA_CONNECTION_STATUS_ACTIVE,
+            .type = aidl::PdpProtocolType::IP,
+            .ifname = "buried_eth0",
+            .addresses = {{
+                    .address = "192.168.97.2/30",
+                    .addressProperties = 0,
+                    .deprecationTime = RadioConst::VALUE_UNAVAILABLE_LONG,
+                    .expirationTime = RadioConst::VALUE_UNAVAILABLE_LONG,
+            }},
+            .dnses = {"8.8.8.8"},
+            .gateways = {"192.168.97.1"},
+            .pcscf = {},
+            .mtuV4 = 0,
+            .mtuV6 = 0,
+            .defaultQos = {},
+            .qosSessions = {},
+            .handoverFailureMode = aidl::SetupDataCallResult::HANDOVER_FAILURE_MODE_LEGACY,
+            .pduSessionId = 0,
+            .sliceInfo = std::nullopt,
+            .trafficDescriptors = {},
+    };
+
+    setupDataCallBase(result);
+
+    respond()->setupDataCallResponse(noError(serial), result);
+    return ok();
+}
+
+}  // namespace android::hardware::radio::service
diff --git a/radio/aidl/minradio/minradio-example/impl/RadioData.h b/radio/aidl/minradio/minradio-example/impl/RadioData.h
new file mode 100644
index 0000000..89b331e
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/impl/RadioData.h
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <libminradio/data/RadioData.h>
+
+namespace android::hardware::radio::service {
+
+class RadioData : public minimal::RadioData {
+  public:
+    using minimal::RadioData::RadioData;
+
+  protected:
+    ::ndk::ScopedAStatus setupDataCall(
+            int32_t serial, ::aidl::android::hardware::radio::AccessNetwork accessNetwork,
+            const ::aidl::android::hardware::radio::data::DataProfileInfo& dataProfileInfo,
+            bool roamingAllowed, ::aidl::android::hardware::radio::data::DataRequestReason reason,
+            const std::vector<::aidl::android::hardware::radio::data::LinkAddress>& addresses,
+            const std::vector<std::string>& dnses, int32_t pduSessionId,
+            const std::optional<::aidl::android::hardware::radio::data::SliceInfo>& sliceInfo,
+            bool matchAllRuleAllowed) override;
+};
+
+}  // namespace android::hardware::radio::service
diff --git a/radio/aidl/minradio/minradio-example/impl/RadioModem.cpp b/radio/aidl/minradio/minradio-example/impl/RadioModem.cpp
new file mode 100644
index 0000000..dc8c1aa
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/impl/RadioModem.cpp
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+#include "RadioModem.h"
+
+#include <libminradio/debug.h>
+#include <libminradio/response.h>
+
+#define RADIO_MODULE "ModemImpl"
+
+namespace android::hardware::radio::service {
+
+using ::aidl::android::hardware::radio::RadioTechnology;
+using ::android::hardware::radio::minimal::noError;
+using ::ndk::ScopedAStatus;
+namespace aidl = ::aidl::android::hardware::radio::modem;
+constexpr auto ok = &ScopedAStatus::ok;
+
+RadioModem::RadioModem(std::shared_ptr<minimal::SlotContext> context)
+    : minimal::RadioModem(context, {{RadioTechnology::LTE, RadioTechnology::HSPA}}) {}
+
+ScopedAStatus RadioModem::getImei(int32_t serial) {
+    LOG_CALL;
+    aidl::ImeiInfo info{
+            .type = aidl::ImeiInfo::ImeiType::PRIMARY,
+            .imei = "867400022047199",
+            .svn = "01",
+    };
+    respond()->getImeiResponse(noError(serial), info);
+    return ok();
+}
+
+}  // namespace android::hardware::radio::service
diff --git a/radio/aidl/minradio/minradio-example/impl/RadioModem.h b/radio/aidl/minradio/minradio-example/impl/RadioModem.h
new file mode 100644
index 0000000..1102188
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/impl/RadioModem.h
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <libminradio/modem/RadioModem.h>
+
+namespace android::hardware::radio::service {
+
+class RadioModem : public minimal::RadioModem {
+  public:
+    RadioModem(std::shared_ptr<minimal::SlotContext> context);
+
+  protected:
+    // Note: getBasebandVersion is optional, but recommended to implement on production devices.
+    //       It's just returning a version of the cellular implementation (e.g. modem software).
+    ::ndk::ScopedAStatus getImei(int32_t serial) override;
+};
+
+}  // namespace android::hardware::radio::service
diff --git a/radio/aidl/minradio/minradio-example/impl/RadioNetwork.cpp b/radio/aidl/minradio/minradio-example/impl/RadioNetwork.cpp
new file mode 100644
index 0000000..4ad9eb8
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/impl/RadioNetwork.cpp
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+#include "RadioNetwork.h"
+
+#include <libminradio/debug.h>
+#include <libminradio/network/structs.h>
+#include <libminradio/response.h>
+
+#define RADIO_MODULE "NetworkImpl"
+
+namespace android::hardware::radio::service {
+
+using ::aidl::android::hardware::radio::RadioConst;
+using ::android::hardware::radio::minimal::noError;
+using ::ndk::ScopedAStatus;
+namespace aidl = ::aidl::android::hardware::radio::network;
+namespace aidlRadio = ::aidl::android::hardware::radio;
+constexpr auto ok = &ScopedAStatus::ok;
+
+ScopedAStatus RadioNetwork::getDataRegistrationState(int32_t serial) {
+    LOG_CALL;
+
+    aidl::CellIdentityLte cellid{
+            .mcc = "310",
+            .mnc = "555",
+            .ci = 12345,
+            .pci = 102,
+            .tac = 1040,
+            .earfcn = 103,
+            .operatorNames =
+                    {
+                            .alphaLong = "Minradio",
+                            .alphaShort = "MR",
+                            .operatorNumeric = "310555",
+                            .status = aidl::OperatorInfo::STATUS_CURRENT,
+                    },
+            .bandwidth = 1400,
+            .additionalPlmns = {},
+            .csgInfo = std::nullopt,
+            .bands =
+                    {
+                            aidl::EutranBands::BAND_1,
+                            aidl::EutranBands::BAND_88,
+                    },
+    };
+    aidl::RegStateResult res{
+            .regState = aidl::RegState::REG_HOME,
+            .rat = aidlRadio::RadioTechnology::LTE,
+            .reasonForDenial = aidl::RegistrationFailCause::NONE,
+            .cellIdentity = cellid,
+            .registeredPlmn = "310555",
+            .accessTechnologySpecificInfo = aidl::EutranRegistrationInfo{},
+    };
+    respond()->getDataRegistrationStateResponse(noError(serial), res);
+    return ok();
+}
+
+ScopedAStatus RadioNetwork::getSignalStrength(int32_t serial) {
+    LOG_CALL;
+
+    auto signal = minimal::structs::makeSignalStrength();
+    signal.lte = {
+            30,   // (0-31, 99)
+            100,  // Range: 44 to 140 dBm
+            10,   // Range: 20 to 3 dB
+            100, 10, RadioConst::VALUE_UNAVAILABLE, RadioConst::VALUE_UNAVAILABLE,
+    };
+
+    respond()->getSignalStrengthResponse(noError(serial), signal);
+    return ok();
+}
+
+}  // namespace android::hardware::radio::service
diff --git a/radio/aidl/minradio/minradio-example/impl/RadioNetwork.h b/radio/aidl/minradio/minradio-example/impl/RadioNetwork.h
new file mode 100644
index 0000000..c07b281
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/impl/RadioNetwork.h
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <libminradio/network/RadioNetwork.h>
+
+namespace android::hardware::radio::service {
+
+class RadioNetwork : public minimal::RadioNetwork {
+  public:
+    using minimal::RadioNetwork::RadioNetwork;
+
+  protected:
+    ::ndk::ScopedAStatus getDataRegistrationState(int32_t serial) override;
+    ::ndk::ScopedAStatus getSignalStrength(int32_t serial) override;
+};
+
+}  // namespace android::hardware::radio::service
diff --git a/radio/aidl/minradio/minradio-example/impl/RadioSim.cpp b/radio/aidl/minradio/minradio-example/impl/RadioSim.cpp
new file mode 100644
index 0000000..a5e1167
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/impl/RadioSim.cpp
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+
+#include "RadioSim.h"
+
+#include <libminradio/debug.h>
+#include <libminradio/response.h>
+#include <libminradio/sim/IccUtils.h>
+
+#define RADIO_MODULE "SimImpl"
+
+namespace android::hardware::radio::service {
+
+using ::android::hardware::radio::minimal::noError;
+using ::ndk::ScopedAStatus;
+namespace aidl = ::aidl::android::hardware::radio::sim;
+namespace aidlConfig = ::aidl::android::hardware::radio::config;
+constexpr auto ok = &ScopedAStatus::ok;
+
+RadioSim::RadioSim(std::shared_ptr<minimal::SlotContext> context) : minimal::RadioSim(context) {
+    addCtsCertificate();  // do NOT call on real device's production build
+    setIccid("98683081462002318379");
+    mFilesystem->write(minimal::sim::paths::msisdn, minimal::sim::encodeMsisdn("+16500000000"));
+}
+
+ScopedAStatus RadioSim::getIccCardStatus(int32_t serial) {
+    LOG_CALL;
+
+    aidl::CardStatus cardStatus{
+            .cardState = aidl::CardStatus::STATE_PRESENT,
+            .universalPinState = aidl::PinState::DISABLED,
+            .gsmUmtsSubscriptionAppIndex = 0,
+            .imsSubscriptionAppIndex = -1,
+            .applications =
+                    {
+                            aidl::AppStatus{
+                                    .appType = aidl::AppStatus::APP_TYPE_USIM,
+                                    .appState = aidl::AppStatus::APP_STATE_READY,
+                                    .persoSubstate = aidl::PersoSubstate::READY,
+                            },
+                    },
+            .atr = "",
+            .iccid = getIccid().value_or(""),
+            .eid = "eUICC-simslot1",
+            .slotMap =
+                    {
+                            .physicalSlotId = 0,
+                            .portId = 0,
+                    },
+            .supportedMepMode = aidlConfig::MultipleEnabledProfilesMode::NONE,
+    };
+    respond()->getIccCardStatusResponse(noError(serial), cardStatus);
+    return ok();
+}
+
+ScopedAStatus RadioSim::getImsiForApp(int32_t serial, const std::string& aid) {
+    LOG_CALL << aid;
+    // 6-digit IMSI prefix has to be a valid mccmnc
+    respond()->getImsiForAppResponse(noError(serial), "311740123456789");
+    return ok();
+}
+
+}  // namespace android::hardware::radio::service
diff --git a/radio/aidl/minradio/minradio-example/impl/RadioSim.h b/radio/aidl/minradio/minradio-example/impl/RadioSim.h
new file mode 100644
index 0000000..138c7fd
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/impl/RadioSim.h
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <libminradio/sim/RadioSim.h>
+
+namespace android::hardware::radio::service {
+
+class RadioSim : public minimal::RadioSim {
+  public:
+    RadioSim(std::shared_ptr<minimal::SlotContext> context);
+
+  protected:
+    ::ndk::ScopedAStatus getIccCardStatus(int32_t serial) override;
+    ::ndk::ScopedAStatus getImsiForApp(int32_t serial, const std::string& aid) override;
+};
+
+}  // namespace android::hardware::radio::service
diff --git a/radio/aidl/minradio/minradio-example/minradio-example.rc b/radio/aidl/minradio/minradio-example/minradio-example.rc
new file mode 100644
index 0000000..47e7da3
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/minradio-example.rc
@@ -0,0 +1,5 @@
+service vendor.minradio-example /apex/com.android.hardware.radio.minradio.virtual/bin/hw/android.hardware.radio-service.minradio-example
+    class main
+    user radio
+    group radio inet misc audio log readproc wakelock
+    capabilities BLOCK_SUSPEND NET_ADMIN NET_RAW
diff --git a/radio/aidl/minradio/minradio-example/minradio-example.xml b/radio/aidl/minradio/minradio-example/minradio-example.xml
new file mode 100644
index 0000000..3ef129e
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/minradio-example.xml
@@ -0,0 +1,27 @@
+<manifest version="1.0" type="device">
+    <hal format="aidl">
+        <name>android.hardware.radio.config</name>
+        <fqname>IRadioConfig/default</fqname>
+        <version>4</version>
+    </hal>
+    <hal format="aidl">
+        <name>android.hardware.radio.data</name>
+        <fqname>IRadioData/slot1</fqname>
+        <version>4</version>
+    </hal>
+    <hal format="aidl">
+        <name>android.hardware.radio.modem</name>
+        <fqname>IRadioModem/slot1</fqname>
+        <version>4</version>
+    </hal>
+    <hal format="aidl">
+        <name>android.hardware.radio.network</name>
+        <fqname>IRadioNetwork/slot1</fqname>
+        <version>4</version>
+    </hal>
+    <hal format="aidl">
+        <name>android.hardware.radio.sim</name>
+        <fqname>IRadioSim/slot1</fqname>
+        <version>4</version>
+    </hal>
+</manifest>
diff --git a/radio/aidl/minradio/minradio-example/service.cpp b/radio/aidl/minradio/minradio-example/service.cpp
new file mode 100644
index 0000000..6d3c020
--- /dev/null
+++ b/radio/aidl/minradio/minradio-example/service.cpp
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 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 "impl/RadioConfig.h"
+#include "impl/RadioData.h"
+#include "impl/RadioModem.h"
+#include "impl/RadioNetwork.h"
+#include "impl/RadioSim.h"
+
+#include <android-base/logging.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
+
+namespace android::hardware::radio::service {
+
+using namespace std::string_literals;
+
+static std::vector<std::shared_ptr<ndk::ICInterface>> gPublishedHals;
+
+static void publishRadioConfig() {
+    auto aidlHal = ndk::SharedRefBase::make<RadioConfig>();
+    gPublishedHals.push_back(aidlHal);
+    const auto instance = RadioConfig::descriptor + "/default"s;
+    const auto status = AServiceManager_addService(aidlHal->asBinder().get(), instance.c_str());
+    CHECK_EQ(status, STATUS_OK);
+}
+
+template <typename T>
+static void publishRadioHal(const std::string& slot,
+                            std::shared_ptr<minimal::SlotContext> context) {
+    const auto instance = T::descriptor + "/"s + slot;
+    if (!AServiceManager_isDeclared(instance.c_str())) {
+        LOG(INFO) << instance << " is not declared in VINTF (this may be intentional)";
+        return;
+    }
+    LOG(DEBUG) << "Publishing " << instance;
+
+    auto aidlHal = ndk::SharedRefBase::make<T>(context);
+    gPublishedHals.push_back(aidlHal);
+    const auto status = AServiceManager_addService(aidlHal->asBinder().get(), instance.c_str());
+    CHECK_EQ(status, STATUS_OK);
+}
+
+void main() {
+    base::InitLogging(nullptr, base::LogdLogger(base::RADIO));
+    base::SetDefaultTag("minradio");
+    base::SetMinimumLogSeverity(base::VERBOSE);
+    LOG(DEBUG) << "Minimal Radio HAL service starting...";
+    ABinderProcess_setThreadPoolMaxThreadCount(1);
+    ABinderProcess_startThreadPool();
+
+    auto slot1Context = std::make_shared<minimal::SlotContext>(1);
+
+    publishRadioConfig();
+    publishRadioHal<RadioData>("slot1", slot1Context);
+    publishRadioHal<RadioModem>("slot1", slot1Context);
+    publishRadioHal<RadioNetwork>("slot1", slot1Context);
+    publishRadioHal<RadioSim>("slot1", slot1Context);
+
+    LOG(DEBUG) << "Minimal Radio HAL service is operational";
+    ABinderProcess_joinThreadPool();
+    LOG(FATAL) << "Minimal Radio HAL service has stopped";
+}
+
+}  // namespace android::hardware::radio::service
+
+int main() {
+    android::hardware::radio::service::main();
+    return EXIT_FAILURE;  // should not reach
+}