Merge "ExternalCamera: Fix a deadlock crash using std::thread" into main
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 8c53006..c18983e 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -8,7 +8,4 @@
 gofmt = true
 
 [Hook Scripts]
-aosp_hook_confirmationui = ${REPO_ROOT}/frameworks/base/tools/aosp/aosp_sha.sh ${PREUPLOAD_COMMIT} confirmationui
-aosp_hook_gatekeeper = ${REPO_ROOT}/frameworks/base/tools/aosp/aosp_sha.sh ${PREUPLOAD_COMMIT} gatekeeper
-aosp_hook_keymaster = ${REPO_ROOT}/frameworks/base/tools/aosp/aosp_sha.sh ${PREUPLOAD_COMMIT} keymaster
 generate_vehicle_property_enums = ${REPO_ROOT}/hardware/interfaces/automotive/vehicle/tools/generate_annotation_enums.py --android_build_top ${REPO_ROOT} --preupload_files ${PREUPLOAD_FILES} --check_only
diff --git a/audio/aidl/default/Module.cpp b/audio/aidl/default/Module.cpp
index 123a5ec..aa624ff 100644
--- a/audio/aidl/default/Module.cpp
+++ b/audio/aidl/default/Module.cpp
@@ -214,24 +214,33 @@
     StreamContext::DebugParameters params{mDebug.streamTransientStateDelayMs,
                                           mVendorDebug.forceTransientBurst,
                                           mVendorDebug.forceSynchronousDrain};
-    std::unique_ptr<StreamContext::DataMQ> dataMQ = nullptr;
-    std::shared_ptr<IStreamCallback> streamAsyncCallback = nullptr;
     std::shared_ptr<ISoundDose> soundDose;
     if (!getSoundDose(&soundDose).isOk()) {
         LOG(ERROR) << __func__ << ": could not create sound dose instance";
         return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
     }
-    if (!hasMmapFlag(flags)) {
-        dataMQ = std::make_unique<StreamContext::DataMQ>(frameSize * in_bufferSizeFrames);
-        streamAsyncCallback = asyncCallback;
+    StreamContext temp;
+    if (hasMmapFlag(flags)) {
+        MmapBufferDescriptor mmapDesc;
+        RETURN_STATUS_IF_ERROR(
+                createMmapBuffer(*portConfigIt, in_bufferSizeFrames, frameSize, &mmapDesc));
+        temp = StreamContext(
+                std::make_unique<StreamContext::CommandMQ>(1, true /*configureEventFlagWord*/),
+                std::make_unique<StreamContext::ReplyMQ>(1, true /*configureEventFlagWord*/),
+                portConfigIt->format.value(), portConfigIt->channelMask.value(),
+                portConfigIt->sampleRate.value().value, flags, nominalLatencyMs,
+                portConfigIt->ext.get<AudioPortExt::mix>().handle, std::move(mmapDesc),
+                outEventCallback, mSoundDose.getInstance(), params);
+    } else {
+        temp = StreamContext(
+                std::make_unique<StreamContext::CommandMQ>(1, true /*configureEventFlagWord*/),
+                std::make_unique<StreamContext::ReplyMQ>(1, true /*configureEventFlagWord*/),
+                portConfigIt->format.value(), portConfigIt->channelMask.value(),
+                portConfigIt->sampleRate.value().value, flags, nominalLatencyMs,
+                portConfigIt->ext.get<AudioPortExt::mix>().handle,
+                std::make_unique<StreamContext::DataMQ>(frameSize * in_bufferSizeFrames),
+                asyncCallback, outEventCallback, mSoundDose.getInstance(), params);
     }
-    StreamContext temp(
-            std::make_unique<StreamContext::CommandMQ>(1, true /*configureEventFlagWord*/),
-            std::make_unique<StreamContext::ReplyMQ>(1, true /*configureEventFlagWord*/),
-            portConfigIt->format.value(), portConfigIt->channelMask.value(),
-            portConfigIt->sampleRate.value().value, flags, nominalLatencyMs,
-            portConfigIt->ext.get<AudioPortExt::mix>().handle, std::move(dataMQ),
-            streamAsyncCallback, outEventCallback, mSoundDose.getInstance(), params);
     if (temp.isValid()) {
         *out_context = std::move(temp);
     } else {
@@ -394,9 +403,10 @@
     return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
 }
 
-ndk::ScopedAStatus Module::createMmapBuffer(
-        const ::aidl::android::hardware::audio::core::StreamContext& context __unused,
-        ::aidl::android::hardware::audio::core::StreamDescriptor* desc __unused) {
+ndk::ScopedAStatus Module::createMmapBuffer(const AudioPortConfig& portConfig __unused,
+                                            int32_t bufferSizeFrames __unused,
+                                            int32_t frameSizeBytes __unused,
+                                            MmapBufferDescriptor* desc __unused) {
     LOG(ERROR) << __func__ << ": " << mType << ": is not implemented";
     return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
 }
@@ -977,9 +987,6 @@
     RETURN_STATUS_IF_ERROR(createStreamContext(in_args.portConfigId, in_args.bufferSizeFrames,
                                                nullptr, nullptr, &context));
     context.fillDescriptor(&_aidl_return->desc);
-    if (hasMmapFlag(context.getFlags())) {
-        RETURN_STATUS_IF_ERROR(createMmapBuffer(context, &_aidl_return->desc));
-    }
     std::shared_ptr<StreamIn> stream;
     RETURN_STATUS_IF_ERROR(createInputStream(std::move(context), in_args.sinkMetadata,
                                              getMicrophoneInfos(), &stream));
@@ -1027,9 +1034,6 @@
                                                isNonBlocking ? in_args.callback : nullptr,
                                                in_args.eventCallback, &context));
     context.fillDescriptor(&_aidl_return->desc);
-    if (hasMmapFlag(context.getFlags())) {
-        RETURN_STATUS_IF_ERROR(createMmapBuffer(context, &_aidl_return->desc));
-    }
     std::shared_ptr<StreamOut> stream;
     RETURN_STATUS_IF_ERROR(createOutputStream(std::move(context), in_args.sourceMetadata,
                                               in_args.offloadInfo, &stream));
diff --git a/audio/aidl/default/Stream.cpp b/audio/aidl/default/Stream.cpp
index 2800bed..873fc48 100644
--- a/audio/aidl/default/Stream.cpp
+++ b/audio/aidl/default/Stream.cpp
@@ -65,18 +65,26 @@
     if (mReplyMQ) {
         desc->reply = mReplyMQ->dupeDesc();
     }
+    desc->frameSizeBytes = getFrameSize();
+    desc->bufferSizeFrames = getBufferSizeInFrames();
     if (mDataMQ) {
-        desc->frameSizeBytes = getFrameSize();
-        desc->bufferSizeFrames = getBufferSizeInFrames();
         desc->audio.set<StreamDescriptor::AudioBuffer::Tag::fmq>(mDataMQ->dupeDesc());
+    } else {
+        MmapBufferDescriptor mmapDesc;  // Move-only due to `fd`.
+        mmapDesc.sharedMemory.fd = mMmapBufferDesc.sharedMemory.fd.dup();
+        mmapDesc.sharedMemory.size = mMmapBufferDesc.sharedMemory.size;
+        mmapDesc.burstSizeFrames = mMmapBufferDesc.burstSizeFrames;
+        mmapDesc.flags = mMmapBufferDesc.flags;
+        desc->audio.set<StreamDescriptor::AudioBuffer::Tag::mmap>(std::move(mmapDesc));
     }
 }
 
 size_t StreamContext::getBufferSizeInFrames() const {
     if (mDataMQ) {
         return mDataMQ->getQuantumCount() * mDataMQ->getQuantumSize() / getFrameSize();
+    } else {
+        return mMmapBufferDesc.sharedMemory.size / getFrameSize();
     }
-    return 0;
 }
 
 size_t StreamContext::getFrameSize() const {
@@ -96,9 +104,13 @@
         LOG(ERROR) << "frame size is invalid";
         return false;
     }
-    if (!hasMmapFlag(mFlags) && mDataMQ && !mDataMQ->isValid()) {
+    if (!isMmap() && mDataMQ && !mDataMQ->isValid()) {
         LOG(ERROR) << "data FMQ is invalid";
         return false;
+    } else if (isMmap() &&
+               (mMmapBufferDesc.sharedMemory.fd.get() == -1 ||
+                mMmapBufferDesc.sharedMemory.size == 0 || mMmapBufferDesc.burstSizeFrames == 0)) {
+        LOG(ERROR) << "mmap info is invalid" << mMmapBufferDesc.toString();
     }
     return true;
 }
@@ -115,6 +127,7 @@
     mCommandMQ.reset();
     mReplyMQ.reset();
     mDataMQ.reset();
+    mMmapBufferDesc.sharedMemory.fd.set(-1);
 }
 
 pid_t StreamWorkerCommonLogic::getTid() const {
@@ -128,7 +141,7 @@
 std::string StreamWorkerCommonLogic::init() {
     if (mContext->getCommandMQ() == nullptr) return "Command MQ is null";
     if (mContext->getReplyMQ() == nullptr) return "Reply MQ is null";
-    if (!hasMmapFlag(mContext->getFlags())) {
+    if (!mContext->isMmap()) {
         StreamContext::DataMQ* const dataMQ = mContext->getDataMQ();
         if (dataMQ == nullptr) return "Data MQ is null";
         if (sizeof(DataBufferElement) != dataMQ->getQuantumSize()) {
@@ -167,7 +180,7 @@
     } else {
         reply->observable = reply->hardware = kUnknownPosition;
     }
-    if (hasMmapFlag(mContext->getFlags())) {
+    if (mContext->isMmap()) {
         if (auto status = mDriver->getMmapPositionAndLatency(&reply->hardware, &reply->latencyMs);
             status != ::android::OK) {
             reply->hardware = kUnknownPosition;
@@ -252,9 +265,8 @@
                     mState == StreamDescriptor::State::ACTIVE ||
                     mState == StreamDescriptor::State::PAUSED ||
                     mState == StreamDescriptor::State::DRAINING) {
-                    if (bool success = hasMmapFlag(mContext->getFlags())
-                                               ? readMmap(&reply)
-                                               : read(fmqByteCount, &reply);
+                    if (bool success =
+                                mContext->isMmap() ? readMmap(&reply) : read(fmqByteCount, &reply);
                         !success) {
                         mState = StreamDescriptor::State::ERROR;
                     }
@@ -548,9 +560,8 @@
                 if (mState != StreamDescriptor::State::ERROR &&
                     mState != StreamDescriptor::State::TRANSFERRING &&
                     mState != StreamDescriptor::State::TRANSFER_PAUSED) {
-                    if (bool success = hasMmapFlag(mContext->getFlags())
-                                               ? writeMmap(&reply)
-                                               : write(fmqByteCount, &reply);
+                    if (bool success = mContext->isMmap() ? writeMmap(&reply)
+                                                          : write(fmqByteCount, &reply);
                         !success) {
                         mState = StreamDescriptor::State::ERROR;
                     }
diff --git a/audio/aidl/default/include/core-impl/Module.h b/audio/aidl/default/include/core-impl/Module.h
index 0661015..379264d 100644
--- a/audio/aidl/default/include/core-impl/Module.h
+++ b/audio/aidl/default/include/core-impl/Module.h
@@ -212,8 +212,8 @@
             const ::aidl::android::media::audio::common::AudioFormatDescription &format,
             int32_t latencyMs, int32_t sampleRateHz, int32_t *bufferSizeFrames);
     virtual ndk::ScopedAStatus createMmapBuffer(
-            const ::aidl::android::hardware::audio::core::StreamContext& context,
-            ::aidl::android::hardware::audio::core::StreamDescriptor* desc);
+            const ::aidl::android::media::audio::common::AudioPortConfig& portConfig,
+            int32_t bufferSizeFrames, int32_t frameSizeBytes, MmapBufferDescriptor* desc);
 
     // Utility and helper functions accessible to subclasses.
     static int32_t calculateBufferSizeFramesForPcm(int32_t latencyMs, int32_t sampleRateHz) {
diff --git a/audio/aidl/default/include/core-impl/Stream.h b/audio/aidl/default/include/core-impl/Stream.h
index 376c684..bb790e9 100644
--- a/audio/aidl/default/include/core-impl/Stream.h
+++ b/audio/aidl/default/include/core-impl/Stream.h
@@ -104,6 +104,27 @@
           mOutEventCallback(outEventCallback),
           mStreamDataProcessor(streamDataProcessor),
           mDebugParameters(debugParameters) {}
+    StreamContext(std::unique_ptr<CommandMQ> commandMQ, std::unique_ptr<ReplyMQ> replyMQ,
+                  const ::aidl::android::media::audio::common::AudioFormatDescription& format,
+                  const ::aidl::android::media::audio::common::AudioChannelLayout& channelLayout,
+                  int sampleRate, const ::aidl::android::media::audio::common::AudioIoFlags& flags,
+                  int32_t nominalLatencyMs, int32_t mixPortHandle, MmapBufferDescriptor&& mmapDesc,
+                  std::shared_ptr<IStreamOutEventCallback> outEventCallback,
+                  std::weak_ptr<sounddose::StreamDataProcessorInterface> streamDataProcessor,
+                  DebugParameters debugParameters)
+        : mCommandMQ(std::move(commandMQ)),
+          mInternalCommandCookie(std::rand() | 1 /* make sure it's not 0 */),
+          mReplyMQ(std::move(replyMQ)),
+          mFormat(format),
+          mChannelLayout(channelLayout),
+          mSampleRate(sampleRate),
+          mFlags(flags),
+          mNominalLatencyMs(nominalLatencyMs),
+          mMixPortHandle(mixPortHandle),
+          mMmapBufferDesc(std::move(mmapDesc)),
+          mOutEventCallback(outEventCallback),
+          mStreamDataProcessor(streamDataProcessor),
+          mDebugParameters(debugParameters) {}
 
     void fillDescriptor(StreamDescriptor* desc);
     std::shared_ptr<IStreamCallback> getAsyncCallback() const { return mAsyncCallback; }
@@ -136,6 +157,7 @@
     bool isInput() const {
         return mFlags.getTag() == ::aidl::android::media::audio::common::AudioIoFlags::input;
     }
+    bool isMmap() const { return ::aidl::android::hardware::audio::common::hasMmapFlag(mFlags); }
     bool isValid() const;
     // 'reset' is called on a Binder thread when closing the stream. Does not use
     // locking because it only cleans MQ pointers which were also set on the Binder thread.
@@ -155,7 +177,9 @@
     ::aidl::android::media::audio::common::AudioIoFlags mFlags;
     int32_t mNominalLatencyMs;
     int32_t mMixPortHandle;
+    // Only one of `mDataMQ` or `mMapBufferDesc` can be active, depending on `isMmap`
     std::unique_ptr<DataMQ> mDataMQ;
+    MmapBufferDescriptor mMmapBufferDesc;
     std::shared_ptr<IStreamCallback> mAsyncCallback;
     std::shared_ptr<IStreamOutEventCallback> mOutEventCallback;  // Only used by output streams
     std::weak_ptr<sounddose::StreamDataProcessorInterface> mStreamDataProcessor;
diff --git a/audio/aidl/vts/VtsHalAudioEffectTargetTestTemplate.xml b/audio/aidl/vts/VtsHalAudioEffectTargetTestTemplate.xml
index 4170b4c..c92e852 100644
--- a/audio/aidl/vts/VtsHalAudioEffectTargetTestTemplate.xml
+++ b/audio/aidl/vts/VtsHalAudioEffectTargetTestTemplate.xml
@@ -33,6 +33,6 @@
     <test class="com.android.tradefed.testtype.GTest" >
         <option name="native-test-device-path" value="/data/local/tmp" />
         <option name="module-name" value="{MODULE}" />
-        <option name="native-test-timeout" value="30m" />
+        <option name="native-test-timeout" value="10m" />
     </test>
 </configuration>
diff --git a/audio/aidl/vts/VtsHalDynamicsProcessingTest.cpp b/audio/aidl/vts/VtsHalDynamicsProcessingTest.cpp
index 2ce7b51..98f7d79 100644
--- a/audio/aidl/vts/VtsHalDynamicsProcessingTest.cpp
+++ b/audio/aidl/vts/VtsHalDynamicsProcessingTest.cpp
@@ -465,6 +465,7 @@
                                                  float fullScaleSineDb) {
     ASSERT_NO_FATAL_FAILURE(SetUpDynamicsProcessingEffect());
     SKIP_TEST_IF_DATA_UNSUPPORTED(mDescriptor.common.flags);
+    mInput.resize(kFrameCount * mChannelCount);
     ASSERT_NO_FATAL_FAILURE(
             generateSineWave(testFrequencies, mInput, 1.0, kSamplingFrequency, mChannelLayout));
     mInputDb = calculateDb(mInput);
@@ -722,13 +723,10 @@
       public DynamicsProcessingTestHelper {
   public:
     DynamicsProcessingInputGainDataTest()
-        : DynamicsProcessingTestHelper((GetParam()), AudioChannelLayout::LAYOUT_MONO) {
-        mInput.resize(kFrameCount * mChannelCount);
-    }
+        : DynamicsProcessingTestHelper((GetParam()), AudioChannelLayout::LAYOUT_MONO) {}
 
     void SetUp() override {
-        ASSERT_NO_FATAL_FAILURE(
-                setUpDataTest({static_cast<int>(kInputFrequency)}, kSineFullScaleDb));
+        ASSERT_NO_FATAL_FAILURE(setUpDataTest({kInputFrequency}, kSineFullScaleDb));
     }
 
     void TearDown() override { TearDownDynamicsProcessingEffect(); }
@@ -851,15 +849,12 @@
     : public ::testing::TestWithParam<LimiterConfigDataTestParams>,
       public DynamicsProcessingTestHelper {
   public:
-    DynamicsProcessingLimiterConfigDataTest()
-        : DynamicsProcessingTestHelper(GetParam(), AudioChannelLayout::LAYOUT_MONO) {
-        mBufferSize = kFrameCount * mChannelCount;
-        mInput.resize(mBufferSize);
-    }
+    DynamicsProcessingLimiterConfigDataTest(LimiterConfigDataTestParams param = GetParam(),
+                                            int32_t layout = AudioChannelLayout::LAYOUT_MONO)
+        : DynamicsProcessingTestHelper(param, layout) {}
 
     void SetUp() override {
-        ASSERT_NO_FATAL_FAILURE(
-                setUpDataTest({static_cast<int>(kInputFrequency)}, kSineFullScaleDb));
+        ASSERT_NO_FATAL_FAILURE(setUpDataTest({kInputFrequency}, kSineFullScaleDb));
     }
 
     void TearDown() override { TearDownDynamicsProcessingEffect(); }
@@ -876,12 +871,35 @@
         ratio = inputOverThreshold / outputOverThreshold;
     }
 
-    void setLimiterParamsAndProcess(std::vector<float>& input, std::vector<float>& output) {
+    void setLimiterParamsAndProcess(std::vector<float>& input, std::vector<float>& output,
+                                    bool isEngineLimiterEnabled = true) {
+        mEngineConfigPreset.limiterInUse = isEngineLimiterEnabled;
         addEngineConfig(mEngineConfigPreset);
         addLimiterConfig(mLimiterConfigList);
         EXPECT_NO_FATAL_FAILURE(setParamsAndProcess(input, output));
     }
 
+    void testEnableDisableConfiguration(bool isLimiterEnabled, bool isEngineLimiterEnabled) {
+        cleanUpLimiterConfig();
+        std::vector<float> output(mInput.size());
+        for (int i = 0; i < mChannelCount; i++) {
+            // Set non-default values
+            fillLimiterConfig(mLimiterConfigList, i, isLimiterEnabled, kDefaultLinkerGroup,
+                              5 /*attack time*/, 5 /*release time*/, 10 /*ratio*/,
+                              -20 /*threshold*/, 5 /*postgain*/);
+        }
+        ASSERT_NO_FATAL_FAILURE(setLimiterParamsAndProcess(mInput, output, isEngineLimiterEnabled));
+        float outputdB = calculateDb(output, kStartIndex);
+        if (isAllParamsValid()) {
+            if (isLimiterEnabled && isEngineLimiterEnabled) {
+                EXPECT_GT(std::abs(mInputDb - outputdB), kMinDifferenceDb)
+                        << "Input level: " << mInputDb << " Output level: " << outputdB;
+            } else {
+                EXPECT_NEAR(mInputDb, outputdB, kLimiterTestToleranceDb);
+            }
+        }
+    }
+
     void cleanUpLimiterConfig() {
         CleanUp();
         mLimiterConfigList.clear();
@@ -892,8 +910,9 @@
     static constexpr float kDefaultRatio = 4;
     static constexpr float kDefaultThreshold = -10;
     static constexpr float kDefaultPostGain = 0;
-    static constexpr float kInputFrequency = 1000;
     static constexpr float kLimiterTestToleranceDb = 0.05;
+    static constexpr float kMinDifferenceDb = 5;
+    const std::vector<bool> kEnableValues = {true, false, true};
     std::vector<DynamicsProcessing::LimiterConfig> mLimiterConfigList;
     int mBufferSize;
 };
@@ -975,25 +994,16 @@
 }
 
 TEST_P(DynamicsProcessingLimiterConfigDataTest, LimiterEnableDisable) {
-    std::vector<bool> limiterEnableValues = {false, true};
-    std::vector<float> output(mInput.size());
-    for (bool isEnabled : limiterEnableValues) {
-        cleanUpLimiterConfig();
-        for (int i = 0; i < mChannelCount; i++) {
-            // Set non-default values
-            fillLimiterConfig(mLimiterConfigList, i, isEnabled, kDefaultLinkerGroup,
-                              5 /*attack time*/, 5 /*release time*/, 10 /*ratio*/,
-                              -10 /*threshold*/, 5 /*postgain*/);
-        }
-        ASSERT_NO_FATAL_FAILURE(setLimiterParamsAndProcess(mInput, output));
-        if (!isAllParamsValid()) {
-            continue;
-        }
-        if (isEnabled) {
-            EXPECT_NE(mInputDb, calculateDb(output, kStartIndex));
-        } else {
-            EXPECT_NEAR(mInputDb, calculateDb(output, kStartIndex), kLimiterTestToleranceDb);
-        }
+    for (bool isLimiterEnabled : kEnableValues) {
+        ASSERT_NO_FATAL_FAILURE(
+                testEnableDisableConfiguration(isLimiterEnabled, true /*Engine Enabled*/));
+    }
+}
+
+TEST_P(DynamicsProcessingLimiterConfigDataTest, LimiterEnableDisableViaEngine) {
+    for (bool isEngineLimiterEnabled : kEnableValues) {
+        ASSERT_NO_FATAL_FAILURE(
+                testEnableDisableConfiguration(true /*Limiter Enabled*/, isEngineLimiterEnabled));
     }
 }
 
@@ -1010,6 +1020,103 @@
                          });
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(DynamicsProcessingLimiterConfigDataTest);
 
+class DynamicsProcessingLimiterLinkerDataTest : public DynamicsProcessingLimiterConfigDataTest {
+  public:
+    DynamicsProcessingLimiterLinkerDataTest()
+        : DynamicsProcessingLimiterConfigDataTest(GetParam(), AudioChannelLayout::LAYOUT_STEREO) {}
+
+    void calculateExpectedOutputDb(std::vector<float>& expectedOutputDb) {
+        std::vector<float> inputDbValues = calculateStereoDb(mInput, kStartIndex);
+        ASSERT_EQ(inputDbValues.size(), kRatioThresholdPairValues.size());
+        EXPECT_NEAR(inputDbValues[0], inputDbValues[1], kToleranceDb);
+        for (size_t i = 0; i < kRatioThresholdPairValues.size(); i++) {
+            const auto& [ratio, threshold] = kRatioThresholdPairValues[i];
+            expectedOutputDb.push_back((inputDbValues[i] - threshold) / ratio + threshold);
+        }
+    }
+
+    std::vector<float> calculateStereoDb(const std::vector<float>& input,
+                                         size_t startSamplePos = 0) {
+        std::vector<float> leftChannel;
+        std::vector<float> rightChannel;
+        for (size_t i = 0; i < input.size(); i += 2) {
+            leftChannel.push_back(input[i]);
+            if (i + 1 < input.size()) {
+                rightChannel.push_back(input[i + 1]);
+            }
+        }
+        return {calculateDb(leftChannel, startSamplePos),
+                calculateDb(rightChannel, startSamplePos)};
+    }
+
+    void setLinkGroupAndProcess(std::vector<float>& output, bool hasSameLinkGroup) {
+        for (int i = 0; i < mChannelCount; i++) {
+            const auto& [ratio, threshold] = kRatioThresholdPairValues[i];
+            ASSERT_NE(ratio, 0);
+            int linkGroup = hasSameLinkGroup ? kDefaultLinkerGroup : i;
+            fillLimiterConfig(mLimiterConfigList, i, true, linkGroup, kDefaultAttackTime,
+                              kDefaultReleaseTime, ratio, threshold, kDefaultPostGain);
+        }
+
+        ASSERT_NO_FATAL_FAILURE(setLimiterParamsAndProcess(mInput, output));
+
+        if (!isAllParamsValid()) {
+            GTEST_SKIP() << "Invalid parameters. Skipping the test\n";
+        }
+    }
+
+    const std::vector<std::pair<float, float>> kRatioThresholdPairValues = {{2, -10}, {5, -20}};
+};
+
+TEST_P(DynamicsProcessingLimiterLinkerDataTest, SameLinkGroupDifferentConfigs) {
+    std::vector<float> output(mInput.size());
+
+    ASSERT_NO_FATAL_FAILURE(setLinkGroupAndProcess(output, true));
+
+    std::vector<float> outputDbValues = calculateStereoDb(output, kStartIndex);
+
+    std::vector<float> expectedOutputDbValues;
+    ASSERT_NO_FATAL_FAILURE(calculateExpectedOutputDb(expectedOutputDbValues));
+
+    // Verify that the actual output dB is same as the calculated maximum attenuation.
+    float expectedOutputDb = std::min(expectedOutputDbValues[0], expectedOutputDbValues[1]);
+    EXPECT_NEAR(outputDbValues[0], expectedOutputDb, kToleranceDb);
+    EXPECT_NEAR(outputDbValues[1], expectedOutputDb, kToleranceDb);
+}
+
+TEST_P(DynamicsProcessingLimiterLinkerDataTest, DifferentLinkGroupDifferentConfigs) {
+    std::vector<float> output(mInput.size());
+
+    ASSERT_NO_FATAL_FAILURE(setLinkGroupAndProcess(output, false));
+
+    std::vector<float> outputDbValues = calculateStereoDb(output, kStartIndex);
+
+    std::vector<float> expectedOutputDbValues;
+    ASSERT_NO_FATAL_FAILURE(calculateExpectedOutputDb(expectedOutputDbValues));
+
+    // Verify that both channels have different compression levels
+    EXPECT_GT(abs(expectedOutputDbValues[0] - expectedOutputDbValues[1]), kMinDifferenceDb)
+            << "Left channel level: " << expectedOutputDbValues[0]
+            << " Right channel level: " << expectedOutputDbValues[1];
+
+    // Verify that the actual output and the calculated dB values are same
+    EXPECT_NEAR(outputDbValues[0], expectedOutputDbValues[0], kToleranceDb);
+    EXPECT_NEAR(outputDbValues[1], expectedOutputDbValues[1], kToleranceDb);
+}
+
+INSTANTIATE_TEST_SUITE_P(DynamicsProcessingTest, DynamicsProcessingLimiterLinkerDataTest,
+                         testing::ValuesIn(EffectFactoryHelper::getAllEffectDescriptors(
+                                 IFactory::descriptor, getEffectTypeUuidDynamicsProcessing())),
+                         [](const auto& info) {
+                             auto descriptor = info.param;
+                             std::string name = getPrefix(descriptor.second);
+                             std::replace_if(
+                                     name.begin(), name.end(),
+                                     [](const char c) { return !std::isalnum(c); }, '_');
+                             return name;
+                         });
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(DynamicsProcessingLimiterLinkerDataTest);
+
 /**
  * Test DynamicsProcessing ChannelConfig
  */
@@ -1215,7 +1322,6 @@
   public:
     DynamicsProcessingEqBandConfigDataTest()
         : DynamicsProcessingTestHelper(GetParam(), AudioChannelLayout::LAYOUT_MONO) {
-        mInput.resize(kFrameCount * mChannelCount);
         mBinOffsets.resize(mMultitoneTestFrequencies.size());
     }
 
@@ -1444,7 +1550,6 @@
   public:
     DynamicsProcessingMbcBandConfigDataTest()
         : DynamicsProcessingTestHelper(GetParam(), AudioChannelLayout::LAYOUT_MONO) {
-        mInput.resize(kFrameCount * mChannelCount);
         mBinOffsets.resize(mMultitoneTestFrequencies.size());
     }
 
diff --git a/automotive/TEST_MAPPING b/automotive/TEST_MAPPING
index f041ca6..4a4c3e2 100644
--- a/automotive/TEST_MAPPING
+++ b/automotive/TEST_MAPPING
@@ -1,9 +1,6 @@
 {
   "auto-presubmit": [
     {
-      "name": "AndroidCarApiTest"
-    },
-    {
       "name": "CarHiddenApiTest"
     },
     {
diff --git a/automotive/can/1.0/default/libnetdevice/libnetdevice.cpp b/automotive/can/1.0/default/libnetdevice/libnetdevice.cpp
index f149c45..f8f102a 100644
--- a/automotive/can/1.0/default/libnetdevice/libnetdevice.cpp
+++ b/automotive/can/1.0/default/libnetdevice/libnetdevice.cpp
@@ -161,6 +161,8 @@
 }
 
 bool rename(std::string_view from, std::string_view to) {
+    if (!down(from)) return false;
+
     nl::MessageFactory<ifinfomsg> req(RTM_SETLINK);
     req.add(IFLA_IFNAME, to);
 
diff --git a/automotive/vehicle/aidl/impl/current/fake_impl/hardware/src/FakeVehicleHardware.cpp b/automotive/vehicle/aidl/impl/current/fake_impl/hardware/src/FakeVehicleHardware.cpp
index 4eb84dd..be909c5 100644
--- a/automotive/vehicle/aidl/impl/current/fake_impl/hardware/src/FakeVehicleHardware.cpp
+++ b/automotive/vehicle/aidl/impl/current/fake_impl/hardware/src/FakeVehicleHardware.cpp
@@ -2091,7 +2091,7 @@
     }
 
     triggerSupportedValueChange(propId, areaId);
-    return StringPrintf("Min/Max supported value for propId: %s, areaId: %s set",
+    return StringPrintf("Min/Max supported value for propId: %s, areaId: %s set\n",
                         maybeInfo->propIdStr.c_str(), maybeInfo->propIdStr.c_str());
 }
 
@@ -2135,7 +2135,7 @@
                 *maybeSupportedValues;
     }
     triggerSupportedValueChange(maybeInfo->propId, maybeInfo->areaId);
-    return StringPrintf("Supported values list for propId: %s, areaId: %s set",
+    return StringPrintf("Supported values list for propId: %s, areaId: %s set\n",
                         maybeInfo->propIdStr.c_str(), maybeInfo->propIdStr.c_str());
 }
 
diff --git a/bluetooth/audio/flags/btaudiohal.aconfig b/bluetooth/audio/flags/btaudiohal.aconfig
index 13e2116..35e84de 100644
--- a/bluetooth/audio/flags/btaudiohal.aconfig
+++ b/bluetooth/audio/flags/btaudiohal.aconfig
@@ -13,4 +13,11 @@
     namespace: "pixel_bluetooth"
     description: "Flag for reporting lea broadcast audio config to HAL"
     bug: "321168976"
-}
\ No newline at end of file
+}
+
+flag {
+    name: "leaudio_sw_offload"
+    namespace: "pixel_bluetooth"
+    description: "Flag for using sw offload path to send premium audio"
+    bug: "398885696"
+}
diff --git a/broadcastradio/aidl/android/hardware/broadcastradio/AlertUrgency.aidl b/broadcastradio/aidl/android/hardware/broadcastradio/AlertUrgency.aidl
index c7bfdbc..daebb4e 100644
--- a/broadcastradio/aidl/android/hardware/broadcastradio/AlertUrgency.aidl
+++ b/broadcastradio/aidl/android/hardware/broadcastradio/AlertUrgency.aidl
@@ -17,7 +17,7 @@
 package android.hardware.broadcastradio;
 
 /**
- * The severity of the subject event of the emergency alert message.
+ * The urgency of the subject event of the emergency alert message.
  *
  * <p>(see ITU-T X.1303 bis for more info).
  */
diff --git a/camera/provider/aidl/vts/VtsAidlHalCameraProvider_TargetTest.cpp b/camera/provider/aidl/vts/VtsAidlHalCameraProvider_TargetTest.cpp
index 92f69bd..e2c7208 100644
--- a/camera/provider/aidl/vts/VtsAidlHalCameraProvider_TargetTest.cpp
+++ b/camera/provider/aidl/vts/VtsAidlHalCameraProvider_TargetTest.cpp
@@ -1628,10 +1628,10 @@
         Stream previewStream;
         std::shared_ptr<DeviceCb> cb;
 
-        configurePreviewStreams(
+        ASSERT_NO_FATAL_FAILURE(configurePreviewStreams(
                 name, mProvider, &previewThreshold, physicalIds, &mSession, &previewStream,
                 &halStreams /*out*/, &supportsPartialResults /*out*/, &partialResultCount /*out*/,
-                &halBufManagedStreamIds /*out*/, &cb /*out*/, 0 /*streamConfigCounter*/, true);
+                &halBufManagedStreamIds /*out*/, &cb /*out*/, 0 /*streamConfigCounter*/, true));
         if (mSession == nullptr) {
             // stream combination not supported by HAL, skip test for device
             continue;
@@ -2244,10 +2244,10 @@
         Stream previewStream;
         std::vector<HalStream> halStreams;
         std::shared_ptr<DeviceCb> cb;
-        configurePreviewStream(name, mProvider, &previewThreshold, &mSession /*out*/,
-                               &previewStream /*out*/, &halStreams /*out*/,
-                               &supportsPartialResults /*out*/, &partialResultCount /*out*/,
-                               &useHalBufManager /*out*/, &cb /*out*/);
+        ASSERT_NO_FATAL_FAILURE(configurePreviewStream(
+                name, mProvider, &previewThreshold, &mSession /*out*/, &previewStream /*out*/,
+                &halStreams /*out*/, &supportsPartialResults /*out*/, &partialResultCount /*out*/,
+                &useHalBufManager /*out*/, &cb /*out*/));
 
         ::aidl::android::hardware::common::fmq::MQDescriptor<
                 int8_t, aidl::android::hardware::common::fmq::SynchronizedReadWrite>
@@ -2373,10 +2373,10 @@
         bool supportsPartialResults = false;
         bool useHalBufManager = false;
         int32_t partialResultCount = 0;
-        configurePreviewStream(name, mProvider, &previewThreshold, &mSession /*out*/,
-                               &previewStream /*out*/, &halStreams /*out*/,
-                               &supportsPartialResults /*out*/, &partialResultCount /*out*/,
-                               &useHalBufManager /*out*/, &cb /*out*/);
+        ASSERT_NO_FATAL_FAILURE(configurePreviewStream(
+                name, mProvider, &previewThreshold, &mSession /*out*/, &previewStream /*out*/,
+                &halStreams /*out*/, &supportsPartialResults /*out*/, &partialResultCount /*out*/,
+                &useHalBufManager /*out*/, &cb /*out*/));
         ASSERT_NE(mSession, nullptr);
         ASSERT_FALSE(halStreams.empty());
 
@@ -2637,10 +2637,10 @@
         bool supportsPartialResults = false;
         bool useHalBufManager = false;
         int32_t partialResultCount = 0;
-        configurePreviewStream(name, mProvider, &previewThreshold, &mSession /*out*/,
-                               &previewStream /*out*/, &halStreams /*out*/,
-                               &supportsPartialResults /*out*/, &partialResultCount /*out*/,
-                               &useHalBufManager /*out*/, &cb /*out*/);
+        ASSERT_NO_FATAL_FAILURE(configurePreviewStream(
+                name, mProvider, &previewThreshold, &mSession /*out*/, &previewStream /*out*/,
+                &halStreams /*out*/, &supportsPartialResults /*out*/, &partialResultCount /*out*/,
+                &useHalBufManager /*out*/, &cb /*out*/));
 
         RequestTemplate reqTemplate = RequestTemplate::PREVIEW;
         ndk::ScopedAStatus ret = mSession->constructDefaultRequestSettings(reqTemplate, &settings);
@@ -2692,10 +2692,10 @@
         bool useHalBufManager = false;
         int32_t partialResultCount = 0;
 
-        configurePreviewStream(name, mProvider, &previewThreshold, &mSession /*out*/,
-                               &previewStream /*out*/, &halStreams /*out*/,
-                               &supportsPartialResults /*out*/, &partialResultCount /*out*/,
-                               &useHalBufManager /*out*/, &cb /*out*/);
+        ASSERT_NO_FATAL_FAILURE(configurePreviewStream(
+                name, mProvider, &previewThreshold, &mSession /*out*/, &previewStream /*out*/,
+                &halStreams /*out*/, &supportsPartialResults /*out*/, &partialResultCount /*out*/,
+                &useHalBufManager /*out*/, &cb /*out*/));
 
         ASSERT_NE(mSession, nullptr);
         ASSERT_NE(cb, nullptr);
@@ -2817,10 +2817,10 @@
         bool useHalBufManager = false;
 
         int32_t partialResultCount = 0;
-        configurePreviewStream(name, mProvider, &previewThreshold, &mSession /*out*/,
-                               &previewStream /*out*/, &halStreams /*out*/,
-                               &supportsPartialResults /*out*/, &partialResultCount /*out*/,
-                               &useHalBufManager /*out*/, &cb /*out*/);
+        ASSERT_NO_FATAL_FAILURE(configurePreviewStream(
+                name, mProvider, &previewThreshold, &mSession /*out*/, &previewStream /*out*/,
+                &halStreams /*out*/, &supportsPartialResults /*out*/, &partialResultCount /*out*/,
+                &useHalBufManager /*out*/, &cb /*out*/));
 
         ndk::ScopedAStatus returnStatus = mSession->flush();
         ASSERT_TRUE(returnStatus.isOk());
diff --git a/camera/provider/aidl/vts/camera_aidl_test.cpp b/camera/provider/aidl/vts/camera_aidl_test.cpp
index 75ad532..2bdd5e8 100644
--- a/camera/provider/aidl/vts/camera_aidl_test.cpp
+++ b/camera/provider/aidl/vts/camera_aidl_test.cpp
@@ -2296,14 +2296,10 @@
         bool supportsPartialResults = false;
         bool useHalBufManager = false;
         int32_t partialResultCount = 0;
-        configureSingleStream(name, mProvider, &streamThreshold, bufferUsage, reqTemplate,
-                              &session /*out*/, &testStream /*out*/, &halStreams /*out*/,
-                              &supportsPartialResults /*out*/, &partialResultCount /*out*/,
-                              &useHalBufManager /*out*/, &cb /*out*/);
-
-        ASSERT_NE(session, nullptr);
-        ASSERT_NE(cb, nullptr);
-        ASSERT_FALSE(halStreams.empty());
+        ASSERT_NO_FATAL_FAILURE(configureSingleStream(
+                name, mProvider, &streamThreshold, bufferUsage, reqTemplate, &session /*out*/,
+                &testStream /*out*/, &halStreams /*out*/, &supportsPartialResults /*out*/,
+                &partialResultCount /*out*/, &useHalBufManager /*out*/, &cb /*out*/));
 
         std::shared_ptr<ResultMetadataQueue> resultQueue;
         ::aidl::android::hardware::common::fmq::MQDescriptor<
@@ -2718,37 +2714,37 @@
     config.streams = streams;
     createStreamConfiguration(streams, StreamConfigurationMode::NORMAL_MODE, &config,
                               jpegBufferSize);
-    if (*session != nullptr) {
-        CameraMetadata sessionParams;
-        ret = (*session)->constructDefaultRequestSettings(reqTemplate, &sessionParams);
-        ASSERT_TRUE(ret.isOk());
-        config.sessionParams = sessionParams;
-        config.streamConfigCounter = (int32_t)streamConfigCounter;
 
-        bool supported = false;
-        ret = device->isStreamCombinationSupported(config, &supported);
-        ASSERT_TRUE(ret.isOk());
-        ASSERT_EQ(supported, true);
+    CameraMetadata sessionParams;
+    ret = (*session)->constructDefaultRequestSettings(reqTemplate, &sessionParams);
+    ASSERT_TRUE(ret.isOk());
+    config.sessionParams = sessionParams;
+    config.streamConfigCounter = (int32_t)streamConfigCounter;
 
-        std::vector<HalStream> halConfigs;
-        std::set<int32_t> halBufManagedStreamIds;
-        ret = configureStreams(*session, config, bufferManagerType, &halBufManagedStreamIds,
-                               &halConfigs);
-        ALOGI("configureStreams returns status: %d:%d", ret.getExceptionCode(),
-              ret.getServiceSpecificError());
-        ASSERT_TRUE(ret.isOk());
-        ASSERT_EQ(1u, halConfigs.size());
-        halStreams->clear();
-        halStreams->push_back(halConfigs[0]);
-        *useHalBufManager = halBufManagedStreamIds.size() != 0;
-        if (*useHalBufManager) {
-            std::vector<Stream> ss(1);
-            std::vector<HalStream> hs(1);
-            ss[0] = config.streams[0];
-            hs[0] = halConfigs[0];
-            (*cb)->setCurrentStreamConfig(ss, hs);
-        }
+    bool supported = false;
+    ret = device->isStreamCombinationSupported(config, &supported);
+    ASSERT_TRUE(ret.isOk());
+    ASSERT_EQ(supported, true);
+
+    std::vector<HalStream> halConfigs;
+    std::set<int32_t> halBufManagedStreamIds;
+    ret = configureStreams(*session, config, bufferManagerType, &halBufManagedStreamIds,
+                           &halConfigs);
+    ALOGI("configureStreams returns status: %d:%d", ret.getExceptionCode(),
+          ret.getServiceSpecificError());
+    ASSERT_TRUE(ret.isOk());
+    ASSERT_EQ(1u, halConfigs.size());
+    halStreams->clear();
+    halStreams->push_back(halConfigs[0]);
+    *useHalBufManager = halBufManagedStreamIds.size() != 0;
+    if (*useHalBufManager) {
+        std::vector<Stream> ss(1);
+        std::vector<HalStream> hs(1);
+        ss[0] = config.streams[0];
+        hs[0] = halConfigs[0];
+        (*cb)->setCurrentStreamConfig(ss, hs);
     }
+
     *previewStream = config.streams[0];
     ASSERT_TRUE(ret.isOk());
 }
@@ -2808,10 +2804,11 @@
         bool supportsPartialResults = false;
         bool useHalBufManager = false;
         int32_t partialResultCount = 0;
-        configureSingleStream(name, mProvider, &streamThreshold, GRALLOC1_CONSUMER_USAGE_HWCOMPOSER,
-                              RequestTemplate::PREVIEW, &session /*out*/, &testStream /*out*/,
-                              &halStreams /*out*/, &supportsPartialResults /*out*/,
-                              &partialResultCount /*out*/, &useHalBufManager /*out*/, &cb /*out*/);
+        ASSERT_NO_FATAL_FAILURE(configureSingleStream(
+                name, mProvider, &streamThreshold, GRALLOC1_CONSUMER_USAGE_HWCOMPOSER,
+                RequestTemplate::PREVIEW, &session /*out*/, &testStream /*out*/,
+                &halStreams /*out*/, &supportsPartialResults /*out*/, &partialResultCount /*out*/,
+                &useHalBufManager /*out*/, &cb /*out*/));
 
         ::aidl::android::hardware::common::fmq::MQDescriptor<
                 int8_t, aidl::android::hardware::common::fmq::SynchronizedReadWrite>
@@ -3570,10 +3567,10 @@
         Stream* previewStream, std::vector<HalStream>* halStreams, bool* supportsPartialResults,
         int32_t* partialResultCount, bool* useHalBufManager, std::shared_ptr<DeviceCb>* cb,
         uint32_t streamConfigCounter) {
-    configureSingleStream(name, provider, previewThreshold, GRALLOC1_CONSUMER_USAGE_HWCOMPOSER,
-                          RequestTemplate::PREVIEW, session, previewStream, halStreams,
-                          supportsPartialResults, partialResultCount, useHalBufManager, cb,
-                          streamConfigCounter);
+    ASSERT_NO_FATAL_FAILURE(configureSingleStream(
+            name, provider, previewThreshold, GRALLOC1_CONSUMER_USAGE_HWCOMPOSER,
+            RequestTemplate::PREVIEW, session, previewStream, halStreams, supportsPartialResults,
+            partialResultCount, useHalBufManager, cb, streamConfigCounter));
 }
 
 Status CameraAidlTest::isOfflineSessionSupported(const camera_metadata_t* staticMeta) {
diff --git a/compatibility_matrices/compatibility_matrix.202504.xml b/compatibility_matrices/compatibility_matrix.202504.xml
index 85702e2..7600594 100644
--- a/compatibility_matrices/compatibility_matrix.202504.xml
+++ b/compatibility_matrices/compatibility_matrix.202504.xml
@@ -531,6 +531,14 @@
             <instance>default</instance>
         </interface>
     </hal>
+    <hal format="aidl" exclusive-to="virtual-machine">
+        <name>android.hardware.security.see.authmgr</name>
+        <version>1</version>
+        <interface>
+            <name>IAuthMgrAuthorization</name>
+            <instance>default</instance>
+        </interface>
+    </hal>
     <hal format="aidl" updatable-via-apex="true">
         <name>android.hardware.security.secureclock</name>
         <version>1</version>
diff --git a/compatibility_matrices/compatibility_matrix.202604.xml b/compatibility_matrices/compatibility_matrix.202604.xml
index 46b04c9..ee3ba44 100644
--- a/compatibility_matrices/compatibility_matrix.202604.xml
+++ b/compatibility_matrices/compatibility_matrix.202604.xml
@@ -531,6 +531,14 @@
             <instance>default</instance>
         </interface>
     </hal>
+    <hal format="aidl" exclusive-to="virtual-machine">
+        <name>android.hardware.security.see.authmgr</name>
+        <version>1</version>
+        <interface>
+            <name>IAuthMgrAuthorization</name>
+            <instance>default</instance>
+        </interface>
+    </hal>
     <hal format="aidl" updatable-via-apex="true">
         <name>android.hardware.security.secureclock</name>
         <version>1</version>
diff --git a/gnss/common/utils/default/Utils.cpp b/gnss/common/utils/default/Utils.cpp
index 740bc59..c603ff8 100644
--- a/gnss/common/utils/default/Utils.cpp
+++ b/gnss/common/utils/default/Utils.cpp
@@ -147,10 +147,13 @@
     return gnssData;
 }
 
-GnssData Utils::getMockMeasurement(const bool enableCorrVecOutputs, const bool enableFullTracking) {
+namespace {
+GnssMeasurement getMockGnssMeasurement(int svid, GnssConstellationType constellationType,
+                                       float cN0DbHz, float basebandCN0DbHz,
+                                       double carrierFrequencyHz, bool enableCorrVecOutputs) {
     aidl::android::hardware::gnss::GnssSignalType signalType = {
-            .constellation = GnssConstellationType::GLONASS,
-            .carrierFrequencyHz = 1.59975e+09,
+            .constellation = constellationType,
+            .carrierFrequencyHz = carrierFrequencyHz,
             .codeType = aidl::android::hardware::gnss::GnssSignalType::CODE_TYPE_C,
     };
     GnssMeasurement measurement = {
@@ -161,23 +164,23 @@
                      GnssMeasurement::HAS_SATELLITE_ISB |
                      GnssMeasurement::HAS_SATELLITE_ISB_UNCERTAINTY |
                      GnssMeasurement::HAS_SATELLITE_PVT,
-            .svid = 13,
+            .svid = svid,
             .signalType = signalType,
+            .state = GnssMeasurement::STATE_CODE_LOCK | GnssMeasurement::STATE_BIT_SYNC |
+                     GnssMeasurement::STATE_SUBFRAME_SYNC | GnssMeasurement::STATE_TOW_DECODED |
+                     GnssMeasurement::STATE_GLO_STRING_SYNC |
+                     GnssMeasurement::STATE_GLO_TOD_DECODED,
             .receivedSvTimeInNs = 8195997131077,
             .receivedSvTimeUncertaintyInNs = 15,
-            .antennaCN0DbHz = 30.0,
-            .basebandCN0DbHz = 26.5,
-            .agcLevelDb = 2.3,
+            .antennaCN0DbHz = cN0DbHz,
+            .basebandCN0DbHz = basebandCN0DbHz,
             .pseudorangeRateMps = -484.13739013671875,
             .pseudorangeRateUncertaintyMps = 0.1037999987602233,
             .accumulatedDeltaRangeState = GnssMeasurement::ADR_STATE_VALID,
             .accumulatedDeltaRangeM = 1.52,
             .accumulatedDeltaRangeUncertaintyM = 2.43,
             .multipathIndicator = aidl::android::hardware::gnss::GnssMultipathIndicator::UNKNOWN,
-            .state = GnssMeasurement::STATE_CODE_LOCK | GnssMeasurement::STATE_BIT_SYNC |
-                     GnssMeasurement::STATE_SUBFRAME_SYNC | GnssMeasurement::STATE_TOW_DECODED |
-                     GnssMeasurement::STATE_GLO_STRING_SYNC |
-                     GnssMeasurement::STATE_GLO_TOD_DECODED,
+            .agcLevelDb = 2.3,
             .fullInterSignalBiasNs = 21.5,
             .fullInterSignalBiasUncertaintyNs = 792.0,
             .satelliteInterSignalBiasNs = 233.9,
@@ -199,35 +202,15 @@
                                              .satClkDriftMps = 0},
                             .ionoDelayMeters = 3.069949602639317e-08,
                             .tropoDelayMeters = 3.882265204404031,
-                            .ephemerisSource =
-                                    SatellitePvt::SatelliteEphemerisSource::SERVER_LONG_TERM,
                             .timeOfClockSeconds = 12345,
                             .issueOfDataClock = 143,
                             .timeOfEphemerisSeconds = 9876,
                             .issueOfDataEphemeris = 48,
+                            .ephemerisSource =
+                                    SatellitePvt::SatelliteEphemerisSource::SERVER_LONG_TERM,
                     },
             .correlationVectors = {}};
 
-    GnssClock clock = {.gnssClockFlags = GnssClock::HAS_FULL_BIAS | GnssClock::HAS_BIAS |
-                                         GnssClock::HAS_BIAS_UNCERTAINTY | GnssClock::HAS_DRIFT |
-                                         GnssClock::HAS_DRIFT_UNCERTAINTY,
-                       .timeNs = 2713545000000,
-                       .fullBiasNs = -1226701900521857520,
-                       .biasNs = 0.59689998626708984,
-                       .biasUncertaintyNs = 47514.989972114563,
-                       .driftNsps = -51.757811607455452,
-                       .driftUncertaintyNsps = 310.64968328491528,
-                       .hwClockDiscontinuityCount = 1,
-                       .referenceSignalTypeForIsb = signalType};
-
-    ElapsedRealtime timestamp = {
-            .flags = ElapsedRealtime::HAS_TIMESTAMP_NS | ElapsedRealtime::HAS_TIME_UNCERTAINTY_NS,
-            .timestampNs = ::android::elapsedRealtimeNano(),
-            // This is an hardcoded value indicating a 1ms of uncertainty between the two clocks.
-            // In an actual implementation provide an estimate of the synchronization uncertainty
-            // or don't set the field.
-            .timeUncertaintyNs = 1020400};
-
     if (enableCorrVecOutputs) {
         aidl::android::hardware::gnss::CorrelationVector correlationVector1 = {
                 .frequencyOffsetMps = 10,
@@ -242,6 +225,68 @@
         measurement.correlationVectors = {correlationVector1, correlationVector2};
         measurement.flags |= GnssMeasurement::HAS_CORRELATION_VECTOR;
     }
+    return measurement;
+}
+}  // namespace
+
+GnssData Utils::getMockMeasurement(const bool enableCorrVecOutputs, const bool enableFullTracking) {
+    std::vector<GnssMeasurement> measurements = {
+            // GPS
+            getMockGnssMeasurement(3, GnssConstellationType::GPS, 32.5, 27.5, kGpsL1FreqHz,
+                                   enableCorrVecOutputs),
+            getMockGnssMeasurement(5, GnssConstellationType::GPS, 27.0, 22.0, kGpsL1FreqHz,
+                                   enableCorrVecOutputs),
+            getMockGnssMeasurement(17, GnssConstellationType::GPS, 30.5, 25.5, kGpsL5FreqHz,
+                                   enableCorrVecOutputs),
+            getMockGnssMeasurement(26, GnssConstellationType::GPS, 24.1, 19.1, kGpsL5FreqHz,
+                                   enableCorrVecOutputs),
+            // GAL
+            getMockGnssMeasurement(2, GnssConstellationType::GALILEO, 33.5, 27.5, kGalE1FreqHz,
+                                   enableCorrVecOutputs),
+            getMockGnssMeasurement(4, GnssConstellationType::GALILEO, 28.0, 22.0, kGalE1FreqHz,
+                                   enableCorrVecOutputs),
+            getMockGnssMeasurement(10, GnssConstellationType::GALILEO, 35.5, 25.5, kGalE1FreqHz,
+                                   enableCorrVecOutputs),
+            getMockGnssMeasurement(29, GnssConstellationType::GALILEO, 34.1, 19.1, kGalE1FreqHz,
+                                   enableCorrVecOutputs),
+            // GLO
+            getMockGnssMeasurement(5, GnssConstellationType::GLONASS, 20.5, 15.5, kGloG1FreqHz,
+                                   enableCorrVecOutputs),
+            getMockGnssMeasurement(17, GnssConstellationType::GLONASS, 21.5, 16.5, kGloG1FreqHz,
+                                   enableCorrVecOutputs),
+            getMockGnssMeasurement(18, GnssConstellationType::GLONASS, 28.3, 25.3, kGloG1FreqHz,
+                                   enableCorrVecOutputs),
+            getMockGnssMeasurement(10, GnssConstellationType::GLONASS, 25.0, 20.0, kGloG1FreqHz,
+                                   enableCorrVecOutputs),
+            // IRNSS
+            getMockGnssMeasurement(3, GnssConstellationType::IRNSS, 22.0, 19.7, kIrnssL5FreqHz,
+                                   enableCorrVecOutputs),
+    };
+
+    GnssClock clock = {
+            .gnssClockFlags = GnssClock::HAS_FULL_BIAS | GnssClock::HAS_BIAS |
+                              GnssClock::HAS_BIAS_UNCERTAINTY | GnssClock::HAS_DRIFT |
+                              GnssClock::HAS_DRIFT_UNCERTAINTY,
+            .timeNs = 2713545000000,
+            .fullBiasNs = -1226701900521857520,
+            .biasNs = 0.59689998626708984,
+            .biasUncertaintyNs = 47514.989972114563,
+            .driftNsps = -51.757811607455452,
+            .driftUncertaintyNsps = 310.64968328491528,
+            .hwClockDiscontinuityCount = 1,
+            .referenceSignalTypeForIsb = {
+                    .constellation = GnssConstellationType::GLONASS,
+                    .carrierFrequencyHz = 1.59975e+09,
+                    .codeType = aidl::android::hardware::gnss::GnssSignalType::CODE_TYPE_C,
+            }};
+
+    ElapsedRealtime timestamp = {
+            .flags = ElapsedRealtime::HAS_TIMESTAMP_NS | ElapsedRealtime::HAS_TIME_UNCERTAINTY_NS,
+            .timestampNs = ::android::elapsedRealtimeNano(),
+            // This is an hardcoded value indicating a 1ms of uncertainty between the two clocks.
+            // In an actual implementation provide an estimate of the synchronization uncertainty
+            // or don't set the field.
+            .timeUncertaintyNs = 1020400};
 
     GnssAgc gnssAgc1 = {
             .agcLevelDb = 3.5,
@@ -255,7 +300,7 @@
             .carrierFrequencyHz = (int64_t)kGpsL1FreqHz,
     };
 
-    GnssData gnssData = {.measurements = {measurement},
+    GnssData gnssData = {.measurements = measurements,
                          .clock = clock,
                          .elapsedRealtime = timestamp,
                          .gnssAgcs = std::vector({gnssAgc1, gnssAgc2}),
diff --git a/graphics/composer/aidl/libhwc_aidl_test/ComposerClientWrapper.cpp b/graphics/composer/aidl/libhwc_aidl_test/ComposerClientWrapper.cpp
index 2252ce3..bfc4d2d 100644
--- a/graphics/composer/aidl/libhwc_aidl_test/ComposerClientWrapper.cpp
+++ b/graphics/composer/aidl/libhwc_aidl_test/ComposerClientWrapper.cpp
@@ -458,6 +458,11 @@
     return mComposerCallback->takeListOfRefreshRateChangedDebugData();
 }
 
+std::vector<std::pair<int64_t, common::DisplayHotplugEvent>>
+ComposerClientWrapper::getAndClearLatestHotplugs() {
+    return mComposerCallback->getAndClearLatestHotplugs();
+}
+
 int64_t ComposerClientWrapper::getInvalidDisplayId() {
     // returns an invalid display id (one that has not been registered to a
     // display. Currently assuming that a device will never have close to
diff --git a/graphics/composer/aidl/libhwc_aidl_test/GraphicsComposerCallback.cpp b/graphics/composer/aidl/libhwc_aidl_test/GraphicsComposerCallback.cpp
index ba16348..0daff56 100644
--- a/graphics/composer/aidl/libhwc_aidl_test/GraphicsComposerCallback.cpp
+++ b/graphics/composer/aidl/libhwc_aidl_test/GraphicsComposerCallback.cpp
@@ -100,6 +100,16 @@
     return mInvalidRefreshRateDebugEnabledCallbackCount;
 }
 
+std::vector<std::pair<int64_t, common::DisplayHotplugEvent>>
+GraphicsComposerCallback::getAndClearLatestHotplugs() {
+    std::vector<std::pair<int64_t, common::DisplayHotplugEvent>> ret;
+    {
+        std::scoped_lock lock(mMutex);
+        ret.swap(mLatestHotplugs);
+    }
+    return ret;
+}
+
 ::ndk::ScopedAStatus GraphicsComposerCallback::onHotplug(int64_t in_display, bool in_connected) {
     std::scoped_lock lock(mMutex);
 
@@ -196,6 +206,11 @@
 
 ::ndk::ScopedAStatus GraphicsComposerCallback::onHotplugEvent(int64_t in_display,
                                                               common::DisplayHotplugEvent event) {
+    {
+        std::scoped_lock lock(mMutex);
+        mLatestHotplugs.emplace_back(in_display, event);
+    }
+
     switch (event) {
         case common::DisplayHotplugEvent::CONNECTED:
             return onHotplug(in_display, true);
diff --git a/graphics/composer/aidl/libhwc_aidl_test/include/ComposerClientWrapper.h b/graphics/composer/aidl/libhwc_aidl_test/include/ComposerClientWrapper.h
index 5ba52bc..3d805c2 100644
--- a/graphics/composer/aidl/libhwc_aidl_test/include/ComposerClientWrapper.h
+++ b/graphics/composer/aidl/libhwc_aidl_test/include/ComposerClientWrapper.h
@@ -203,6 +203,8 @@
     std::pair<ScopedAStatus, std::vector<Luts>> getLuts(int64_t display,
                                                         const std::vector<Buffer>& buffers);
 
+    std::vector<std::pair<int64_t, common::DisplayHotplugEvent>> getAndClearLatestHotplugs();
+
     static constexpr int32_t kMaxFrameIntervalNs = 50000000;  // 20fps
     static constexpr int32_t kNoFrameIntervalNs = 0;
 
diff --git a/graphics/composer/aidl/libhwc_aidl_test/include/GraphicsComposerCallback.h b/graphics/composer/aidl/libhwc_aidl_test/include/GraphicsComposerCallback.h
index ff379b7..c769677 100644
--- a/graphics/composer/aidl/libhwc_aidl_test/include/GraphicsComposerCallback.h
+++ b/graphics/composer/aidl/libhwc_aidl_test/include/GraphicsComposerCallback.h
@@ -50,6 +50,8 @@
 
     int32_t getInvalidRefreshRateDebugEnabledCallbackCount() const;
 
+    std::vector<std::pair<int64_t, common::DisplayHotplugEvent>> getAndClearLatestHotplugs();
+
   private:
     virtual ::ndk::ScopedAStatus onHotplug(int64_t in_display, bool in_connected) override;
     virtual ::ndk::ScopedAStatus onRefresh(int64_t in_display) override;
@@ -71,6 +73,8 @@
     mutable std::mutex mMutex;
     // the set of all currently connected displays
     std::vector<int64_t> mDisplays GUARDED_BY(mMutex);
+    std::vector<std::pair<int64_t /*display id*/, common::DisplayHotplugEvent>> mLatestHotplugs
+            GUARDED_BY(mMutex);
     // true only when vsync is enabled
     bool mVsyncAllowed GUARDED_BY(mMutex) = true;
     // true only when RefreshRateChangedCallbackDebugEnabled is set to true.
diff --git a/radio/aidl/minradio/libminradio/Android.bp b/radio/aidl/minradio/libminradio/Android.bp
new file mode 100644
index 0000000..169df5e
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/Android.bp
@@ -0,0 +1,84 @@
+// 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",
+        "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..fda44c8
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/modem/RadioModem.h
@@ -0,0 +1,75 @@
+/*
+ * 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/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:
+    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/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..4e7c5ee
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/include/libminradio/sim/Filesystem.h
@@ -0,0 +1,74 @@
+/*
+ * 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 <set>
+#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);
+    std::set<int32_t> mUpdates 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);
+
+    std::set<int32_t> fetchAndClearUpdates();
+};
+
+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..f29cd39
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/modem/RadioModem.cpp
@@ -0,0 +1,200 @@
+/*
+ * 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 = 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/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..4be63f4
--- /dev/null
+++ b/radio/aidl/minradio/libminradio/sim/Filesystem.cpp
@@ -0,0 +1,100 @@
+/*
+ * 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
+    mUpdates.insert(path.fileId);
+}
+
+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
+    mUpdates.insert(path.fileId);
+}
+
+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::set<int32_t> Filesystem::fetchAndClearUpdates() {
+    std::unique_lock lck(mFilesGuard);
+    std::set<int32_t> result;
+    std::swap(result, mUpdates);
+    return result;
+}
+
+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
+}
diff --git a/secure_element/1.0/vts/functional/VtsHalSecureElementV1_0TargetTest.cpp b/secure_element/1.0/vts/functional/VtsHalSecureElementV1_0TargetTest.cpp
index 1623960..5df39ed 100644
--- a/secure_element/1.0/vts/functional/VtsHalSecureElementV1_0TargetTest.cpp
+++ b/secure_element/1.0/vts/functional/VtsHalSecureElementV1_0TargetTest.cpp
@@ -209,4 +209,15 @@
 INSTANTIATE_TEST_SUITE_P(
         PerInstance, SecureElementHidlTest,
         testing::ValuesIn(android::hardware::getAllHalInstanceNames(ISecureElement::descriptor)),
-        android::hardware::PrintInstanceNameToString);
\ No newline at end of file
+        android::hardware::PrintInstanceNameToString);
+
+int main(int argc, char** argv) {
+    ::testing::InitGoogleTest(&argc, argv);
+    std::system("svc nfc disable"); /* Turn off NFC */
+    sleep(5);
+    int status = RUN_ALL_TESTS();
+    LOG(INFO) << "Test result = " << status;
+    std::system("svc nfc enable"); /* Turn on NFC */
+    sleep(5);
+    return status;
+}
diff --git a/secure_element/1.1/vts/functional/VtsHalSecureElementV1_1TargetTest.cpp b/secure_element/1.1/vts/functional/VtsHalSecureElementV1_1TargetTest.cpp
index d7e4546..106ee29 100644
--- a/secure_element/1.1/vts/functional/VtsHalSecureElementV1_1TargetTest.cpp
+++ b/secure_element/1.1/vts/functional/VtsHalSecureElementV1_1TargetTest.cpp
@@ -94,3 +94,14 @@
         PerInstance, SecureElementHidlTest,
         testing::ValuesIn(android::hardware::getAllHalInstanceNames(ISecureElement::descriptor)),
         android::hardware::PrintInstanceNameToString);
+
+int main(int argc, char** argv) {
+    ::testing::InitGoogleTest(&argc, argv);
+    std::system("svc nfc disable"); /* Turn off NFC */
+    sleep(5);
+    int status = RUN_ALL_TESTS();
+    LOG(INFO) << "Test result = " << status;
+    std::system("svc nfc enable"); /* Turn on NFC */
+    sleep(5);
+    return status;
+}
diff --git a/secure_element/1.2/vts/functional/VtsHalSecureElementV1_2TargetTest.cpp b/secure_element/1.2/vts/functional/VtsHalSecureElementV1_2TargetTest.cpp
index 26b2ded..98c8a9c 100644
--- a/secure_element/1.2/vts/functional/VtsHalSecureElementV1_2TargetTest.cpp
+++ b/secure_element/1.2/vts/functional/VtsHalSecureElementV1_2TargetTest.cpp
@@ -108,3 +108,14 @@
         PerInstance, SecureElementHidlTest,
         testing::ValuesIn(android::hardware::getAllHalInstanceNames(ISecureElement::descriptor)),
         android::hardware::PrintInstanceNameToString);
+
+int main(int argc, char** argv) {
+    ::testing::InitGoogleTest(&argc, argv);
+    std::system("svc nfc disable"); /* Turn off NFC */
+    sleep(5);
+    int status = RUN_ALL_TESTS();
+    LOG(INFO) << "Test result = " << status;
+    std::system("svc nfc enable"); /* Turn on NFC */
+    sleep(5);
+    return status;
+}
diff --git a/secure_element/aidl/vts/VtsHalSecureElementTargetTest.cpp b/secure_element/aidl/vts/VtsHalSecureElementTargetTest.cpp
index 9678da4..da69b37 100644
--- a/secure_element/aidl/vts/VtsHalSecureElementTargetTest.cpp
+++ b/secure_element/aidl/vts/VtsHalSecureElementTargetTest.cpp
@@ -320,5 +320,10 @@
     ::testing::InitGoogleTest(&argc, argv);
     ABinderProcess_setThreadPoolMaxThreadCount(1);
     ABinderProcess_startThreadPool();
-    return RUN_ALL_TESTS();
+    std::system("/system/bin/svc nfc disable"); /* Turn off NFC */
+    sleep(5);
+    int status = RUN_ALL_TESTS();
+    std::system("/system/bin/svc nfc enable"); /* Turn on NFC */
+    sleep(5);
+    return status;
 }
diff --git a/security/keymint/aidl/Android.bp b/security/keymint/aidl/Android.bp
index 5236e90..195e47b 100644
--- a/security/keymint/aidl/Android.bp
+++ b/security/keymint/aidl/Android.bp
@@ -52,7 +52,7 @@
         },
 
     ],
-
+    min_sdk_version: "35",
 }
 
 // An aidl_interface_defaults that includes the latest KeyMint AIDL interface.
diff --git a/security/rkp/aidl/Android.bp b/security/rkp/aidl/Android.bp
index adc63f6..df8a0ef 100644
--- a/security/rkp/aidl/Android.bp
+++ b/security/rkp/aidl/Android.bp
@@ -35,6 +35,7 @@
                 "//apex_available:platform",
                 "com.android.virt",
             ],
+            min_sdk_version: "35",
         },
     },
     versions_with_info: [
diff --git a/security/secureclock/aidl/Android.bp b/security/secureclock/aidl/Android.bp
index d7e7b43..1d4ec58 100644
--- a/security/secureclock/aidl/Android.bp
+++ b/security/secureclock/aidl/Android.bp
@@ -28,4 +28,5 @@
         },
     },
     versions: ["1"],
+    min_sdk_version: "35",
 }
diff --git a/security/see/authmgr/aidl/README.md b/security/see/authmgr/aidl/README.md
index 97b2b1d..d7bb5e4 100644
--- a/security/see/authmgr/aidl/README.md
+++ b/security/see/authmgr/aidl/README.md
@@ -16,6 +16,9 @@
 requirements that are specific to Android release versions.
 
 ### Android 16
-If implementing `IAuthMgrAuthorization` in Android 16 only one AuthMgr Backend is
+- If implementing `IAuthMgrAuthorization` in Android 16 only one AuthMgr Backend is
 supported and dynamic service discovery is not supported. The AuthMgr Backend
-service must be exposed on secure partition ID 0x8001 over VSOCK port 1.
\ No newline at end of file
+service must be exposed on secure partition ID 0x8001 over VSOCK port 1.
+
+- AuthMgr Front Ends must implement the "android.16" profile as described in the
+[Android Profile for DICE](https://pigweed.googlesource.com/open-dice/+/HEAD/docs/android.md#versions)
\ No newline at end of file
diff --git a/security/see/authmgr/aidl/default/Android.bp b/security/see/authmgr/aidl/default/Android.bp
new file mode 100644
index 0000000..35fce32
--- /dev/null
+++ b/security/see/authmgr/aidl/default/Android.bp
@@ -0,0 +1,20 @@
+//
+// Copyright (C) 2025 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.
+
+vintf_fragment {
+    name: "android.hardware.security.see.authmgr.xml",
+    src: "android.hardware.security.see.authmgr.xml",
+    vendor: true,
+}
diff --git a/security/see/authmgr/aidl/default/android.hardware.security.see.authmgr.xml b/security/see/authmgr/aidl/default/android.hardware.security.see.authmgr.xml
new file mode 100644
index 0000000..17fcc21
--- /dev/null
+++ b/security/see/authmgr/aidl/default/android.hardware.security.see.authmgr.xml
@@ -0,0 +1,10 @@
+<manifest version="1.0" type="device">
+    <hal format="aidl" exclusive-to="virtual-machine">
+        <name>android.hardware.security.see.authmgr</name>
+        <version>1</version>
+        <interface>
+            <name>IAuthMgrAuthorization</name>
+            <instance>default</instance>
+        </interface>
+    </hal>
+</manifest>
diff --git a/security/see/hwcrypto/aidl/vts/functional/Android.bp b/security/see/hwcrypto/aidl/vts/functional/Android.bp
index eb2eba1..fc63878 100644
--- a/security/see/hwcrypto/aidl/vts/functional/Android.bp
+++ b/security/see/hwcrypto/aidl/vts/functional/Android.bp
@@ -53,9 +53,11 @@
 rust_test {
     name: "VtsAidlHwCryptoTests",
     srcs: ["hwcryptokey_tests.rs"],
+    test_config: "AndroidKeyOperations.xml",
     require_root: true,
     defaults: [
         "hw_crypto_hal_aidl_rust_defaults",
+        "rdroidtest.defaults",
     ],
     rustlibs: [
         "libhwcryptohal_vts_test",
@@ -69,9 +71,11 @@
 rust_test {
     name: "VtsAidlHwCryptoOperationsTests",
     srcs: ["hwcrypto_operations_tests.rs"],
+    test_config: "AndroidTestOperations.xml",
     require_root: true,
     defaults: [
         "hw_crypto_hal_aidl_rust_defaults",
+        "rdroidtest.defaults",
     ],
     rustlibs: [
         "libhwcryptohal_vts_test",
diff --git a/security/see/hwcrypto/aidl/vts/functional/AndroidKeyOperations.xml b/security/see/hwcrypto/aidl/vts/functional/AndroidKeyOperations.xml
new file mode 100644
index 0000000..57229d7
--- /dev/null
+++ b/security/see/hwcrypto/aidl/vts/functional/AndroidKeyOperations.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 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.
+-->
+<configuration description="Config for HwCrypto HAL operations VTS tests.">
+  <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+  <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+    <option name="push-file" key="VtsAidlHwCryptoTests" value="/data/local/tmp/VtsAidlHwCryptoTests" />
+  </target_preparer>
+
+  <test class="com.android.tradefed.testtype.rust.RustBinaryTest" >
+    <option name="test-device-path" value="/data/local/tmp" />
+    <option name="module-name" value="VtsAidlHwCryptoTests" />
+    <!-- Rust tests are run in parallel by default. Run these ones
+         single-threaded. -->
+    <option name="native-test-flag" value="--test-threads=1" />
+  </test>
+</configuration>
\ No newline at end of file
diff --git a/security/see/hwcrypto/aidl/vts/functional/AndroidTestOperations.xml b/security/see/hwcrypto/aidl/vts/functional/AndroidTestOperations.xml
new file mode 100644
index 0000000..f069b3b
--- /dev/null
+++ b/security/see/hwcrypto/aidl/vts/functional/AndroidTestOperations.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 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.
+-->
+<configuration description="Config for HwCrypto HAL device key VTS tests.">
+  <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+  <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+    <option name="push-file" key="VtsAidlHwCryptoOperationsTests" value="/data/local/tmp/VtsAidlHwCryptoOperationsTests" />
+  </target_preparer>
+
+  <test class="com.android.tradefed.testtype.rust.RustBinaryTest" >
+    <option name="test-device-path" value="/data/local/tmp" />
+    <option name="module-name" value="VtsAidlHwCryptoOperationsTests" />
+    <!-- Rust tests are run in parallel by default. Run these ones
+         single-threaded. -->
+    <option name="native-test-flag" value="--test-threads=1" />
+  </test>
+</configuration>
\ No newline at end of file
diff --git a/security/see/hwcrypto/aidl/vts/functional/hwcrypto_operations_tests.rs b/security/see/hwcrypto/aidl/vts/functional/hwcrypto_operations_tests.rs
index 521fb73..69a34e3 100644
--- a/security/see/hwcrypto/aidl/vts/functional/hwcrypto_operations_tests.rs
+++ b/security/see/hwcrypto/aidl/vts/functional/hwcrypto_operations_tests.rs
@@ -27,8 +27,10 @@
     KeyPolicy::KeyPolicy,CryptoOperation::CryptoOperation,CryptoOperationSet::CryptoOperationSet,
     OperationParameters::OperationParameters, PatternParameters::PatternParameters,
 };
+use rdroidtest::{ignore_if, rdroidtest};
 
-#[test]
+#[rdroidtest]
+#[ignore_if(hwcryptohal_vts_test::ignore_test())]
 fn test_hwcrypto_key_operations_connection() {
     let hw_crypto_key = hwcryptohal_vts_test::get_hwcryptokey()
         .expect("Couldn't get back a hwcryptokey binder object");
@@ -36,7 +38,8 @@
     assert!(hw_crypto_operations.is_ok(), "Couldn't get back a hwcrypto operations binder object");
 }
 
-#[test]
+#[rdroidtest]
+#[ignore_if(hwcryptohal_vts_test::ignore_test())]
 fn test_hwcrypto_key_operations_simple_aes_test() {
     let hw_crypto_key = hwcryptohal_vts_test::get_hwcryptokey()
         .expect("Couldn't get back a hwcryptokey binder object");
@@ -123,7 +126,8 @@
     assert_eq!(decrypted_msg, "string to be encrypted", "couldn't retrieve original message");
 }
 
-#[test]
+#[rdroidtest]
+#[ignore_if(hwcryptohal_vts_test::ignore_test())]
 fn test_hwcrypto_key_operations_simple_hmac_test() {
     let hw_crypto_key = hwcryptohal_vts_test::get_hwcryptokey()
         .expect("Couldn't get back a hwcryptokey binder object");
@@ -183,7 +187,8 @@
     assert_eq!(mac, mac2, "got a different mac");
 }
 
-#[test]
+#[rdroidtest]
+#[ignore_if(hwcryptohal_vts_test::ignore_test())]
 fn test_hwcrypto_key_operations_aes_simple_cbcs_test_non_block_multiple() {
     let hw_crypto_key = hwcryptohal_vts_test::get_hwcryptokey()
         .expect("Couldn't get back a hwcryptokey binder object");
@@ -280,7 +285,8 @@
     );
 }
 
-#[test]
+#[rdroidtest]
+#[ignore_if(hwcryptohal_vts_test::ignore_test())]
 fn test_hwcrypto_key_operations_aes_simple_all_encrypted_cbcs_test() {
     let hw_crypto_key = hwcryptohal_vts_test::get_hwcryptokey()
         .expect("Couldn't get back a hwcryptokey binder object");
@@ -402,7 +408,8 @@
     );
 }
 
-#[test]
+#[rdroidtest]
+#[ignore_if(hwcryptohal_vts_test::ignore_test())]
 fn check_cbcs_wrong_key_types() {
     let hw_crypto_key = hwcryptohal_vts_test::get_hwcryptokey()
         .expect("Couldn't get back a hwcryptokey binder object");
@@ -473,7 +480,8 @@
     assert!(process_result.is_err(), "Should not be able to use cbcs mode with this key type");
 }
 
-#[test]
+#[rdroidtest]
+#[ignore_if(hwcryptohal_vts_test::ignore_test())]
 fn aes_simple_cbcs_test() {
     let hw_crypto_key = hwcryptohal_vts_test::get_hwcryptokey()
         .expect("Couldn't get back a hwcryptokey binder object");
@@ -584,3 +592,5 @@
         "couldn't retrieve original message"
     );
 }
+
+rdroidtest::test_main!();
diff --git a/security/see/hwcrypto/aidl/vts/functional/hwcryptokey_tests.rs b/security/see/hwcrypto/aidl/vts/functional/hwcryptokey_tests.rs
index fcce839..8b4d924 100644
--- a/security/see/hwcrypto/aidl/vts/functional/hwcryptokey_tests.rs
+++ b/security/see/hwcrypto/aidl/vts/functional/hwcryptokey_tests.rs
@@ -26,14 +26,17 @@
 };
 use android_hardware_security_see_hwcrypto::aidl::android::hardware::security::see::hwcrypto::KeyPolicy::KeyPolicy;
 use hwcryptohal_common;
+use rdroidtest::{ignore_if, rdroidtest};
 
-#[test]
+#[rdroidtest]
+#[ignore_if(hwcryptohal_vts_test::ignore_test())]
 fn test_hwcrypto_key_connection() {
     let hw_crypto_key = hwcryptohal_vts_test::get_hwcryptokey();
     assert!(hw_crypto_key.is_ok(), "Couldn't get back a hwcryptokey binder object");
 }
 
-#[test]
+#[rdroidtest]
+#[ignore_if(hwcryptohal_vts_test::ignore_test())]
 fn test_hwcrypto_key_get_current_dice_policy() {
     let hw_crypto_key = hwcryptohal_vts_test::get_hwcryptokey()
         .expect("Couldn't get back a hwcryptokey binder object");
@@ -41,7 +44,8 @@
     assert!(!dice_policy.is_empty(), "received empty dice policy");
 }
 
-#[test]
+#[rdroidtest]
+#[ignore_if(hwcryptohal_vts_test::ignore_test())]
 fn test_hwcrypto_get_keyslot_data() {
     let hw_crypto_key = hwcryptohal_vts_test::get_hwcryptokey()
         .expect("Couldn't get back a hwcryptokey binder object");
@@ -55,7 +59,8 @@
     );
 }
 
-#[test]
+#[rdroidtest]
+#[ignore_if(hwcryptohal_vts_test::ignore_test())]
 fn test_hwcrypto_import_clear_key() {
     let hw_crypto_key = hwcryptohal_vts_test::get_hwcryptokey()
         .expect("Couldn't get back a hwcryptokey binder object");
@@ -83,7 +88,8 @@
     assert!(key.is_err(), "imported keys should be of type PORTABLE");
 }
 
-#[test]
+#[rdroidtest]
+#[ignore_if(hwcryptohal_vts_test::ignore_test())]
 fn test_hwcrypto_token_export_import() {
     // This test is not representative of the complete flow because here the exporter and importer
     // are the same client, which is not something we would usually do
@@ -107,7 +113,8 @@
     // TODO: Use operations to verify that the keys match
 }
 
-#[test]
+#[rdroidtest]
+#[ignore_if(hwcryptohal_vts_test::ignore_test())]
 fn test_hwcrypto_android_invalid_calls() {
     let hw_crypto_key = hwcryptohal_vts_test::get_hwcryptokey()
         .expect("Couldn't get back a hwcryptokey binder object");
@@ -163,3 +170,5 @@
         "wrong error type received"
     );
 }
+
+rdroidtest::test_main!();
diff --git a/security/see/hwcrypto/aidl/vts/functional/lib.rs b/security/see/hwcrypto/aidl/vts/functional/lib.rs
index 465dde7..43676f6 100644
--- a/security/see/hwcrypto/aidl/vts/functional/lib.rs
+++ b/security/see/hwcrypto/aidl/vts/functional/lib.rs
@@ -18,11 +18,26 @@
 //! It provides the base clases necessaries to write HwCrypto VTS tests
 
 use anyhow::Result;
-use android_hardware_security_see_hwcrypto::aidl::android::hardware::security::see::hwcrypto::IHwCryptoKey::BpHwCryptoKey;
 use android_hardware_security_see_hwcrypto::aidl::android::hardware::security::see::hwcrypto::IHwCryptoKey::IHwCryptoKey;
 
+pub const HWCRYPTO_SERVICE: &str = "android.hardware.security.see.hwcrypto.IHwCryptoKey";
+
 /// Get a HwCryptoKey binder service object using the service manager
 pub fn get_hwcryptokey() -> Result<binder::Strong<dyn IHwCryptoKey>, binder::Status> {
-    let interface_name = <BpHwCryptoKey as IHwCryptoKey>::get_descriptor().to_owned() + "/default";
+    let interface_name = HWCRYPTO_SERVICE.to_owned() + "/default";
     Ok(binder::get_interface(&interface_name)?)
 }
+
+pub fn get_supported_instances() -> Vec<(String, String)> {
+    // Determine which instances are available.
+    binder::get_declared_instances(HWCRYPTO_SERVICE)
+        .unwrap_or_default()
+        .into_iter()
+        .map(|v| (v.clone(), v))
+        .collect()
+}
+
+pub fn ignore_test() -> bool {
+    let instances = get_supported_instances();
+    instances.len() == 0
+}
diff --git a/security/see/storage/default/Android.bp b/security/see/storage/default/Android.bp
index 7ea7739..33d93bb 100644
--- a/security/see/storage/default/Android.bp
+++ b/security/see/storage/default/Android.bp
@@ -13,9 +13,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-prebuilt_etc {
+vintf_fragment {
     name: "android.hardware.security.see.storage-service.trusty.xml",
-    sub_dir: "vintf",
-    vendor: true,
     src: "android.hardware.security.see.storage-service.trusty.xml",
+    vendor: true,
 }
diff --git a/sensors/aidl/multihal/android.hardware.sensors-service-multihal.rc b/sensors/aidl/multihal/android.hardware.sensors-service-multihal.rc
index 5aecc54..d1acd6f 100644
--- a/sensors/aidl/multihal/android.hardware.sensors-service-multihal.rc
+++ b/sensors/aidl/multihal/android.hardware.sensors-service-multihal.rc
@@ -1,3 +1,6 @@
+on boot
+    setprop vendor.sensors.dynamic_sensor_op_timeout_ms 1600
+
 service vendor.sensors-hal-multihal /vendor/bin/hw/android.hardware.sensors-service.multihal
     class hal
     user system
diff --git a/wifi/aidl/default/aidl_struct_util.cpp b/wifi/aidl/default/aidl_struct_util.cpp
index 8bc9d1a..87e6d95 100644
--- a/wifi/aidl/default/aidl_struct_util.cpp
+++ b/wifi/aidl/default/aidl_struct_util.cpp
@@ -66,6 +66,8 @@
             return IWifiChip::FeatureSetMask::SET_VOIP_MODE;
         case WIFI_FEATURE_MLO_SAP:
             return IWifiChip::FeatureSetMask::MLO_SAP;
+        case WIFI_FEATURE_MULTIPLE_MLD_ON_SAP:
+            return IWifiChip::FeatureSetMask::MULTIPLE_MLD_ON_SAP;
     };
     CHECK(false) << "Unknown legacy feature: " << feature;
     return {};
@@ -122,7 +124,8 @@
                                       WIFI_FEATURE_P2P_RAND_MAC,
                                       WIFI_FEATURE_AFC_CHANNEL,
                                       WIFI_FEATURE_SET_VOIP_MODE,
-                                      WIFI_FEATURE_MLO_SAP};
+                                      WIFI_FEATURE_MLO_SAP,
+                                      WIFI_FEATURE_MULTIPLE_MLD_ON_SAP};
     for (const auto feature : features) {
         if (feature & legacy_feature_set) {
             *aidl_feature_set |= static_cast<uint32_t>(convertLegacyChipFeatureToAidl(feature));
diff --git a/wifi/legacy_headers/include/hardware_legacy/wifi_hal.h b/wifi/legacy_headers/include/hardware_legacy/wifi_hal.h
index dbcc152..4cabbe4 100644
--- a/wifi/legacy_headers/include/hardware_legacy/wifi_hal.h
+++ b/wifi/legacy_headers/include/hardware_legacy/wifi_hal.h
@@ -499,6 +499,8 @@
 #define WIFI_FEATURE_SET_VOIP_MODE          (uint64_t)0x1000000000 // Support Voip mode setting
 #define WIFI_FEATURE_CACHED_SCAN_RESULTS    (uint64_t)0x2000000000 // Support cached scan result report
 #define WIFI_FEATURE_MLO_SAP (uint64_t)0x4000000000                // Support MLO SoftAp
+#define WIFI_FEATURE_MULTIPLE_MLD_ON_SAP \
+    (uint64_t)0x8000000000  // Support Multiple MLD SoftAp (Bridged Dual 11be SoftAp)
 // Add more features here
 
 #define IS_MASK_SET(mask, flags)        (((flags) & (mask)) == (mask))