Merge "Add VTS for VUR." into main
diff --git a/audio/aidl/default/Android.bp b/audio/aidl/default/Android.bp
index 41fc80b..844f1e9 100644
--- a/audio/aidl/default/Android.bp
+++ b/audio/aidl/default/Android.bp
@@ -249,7 +249,7 @@
         "EffectFactory.cpp",
         "EffectMain.cpp",
     ],
-    installable: false, //installed in apex com.android.hardware.audio.effect
+    installable: false, //installed in apex com.android.hardware.audio
 }
 
 cc_library_headers {
@@ -271,9 +271,3 @@
     sub_dir: "vintf",
     installable: false,
 }
-
-prebuilt_etc {
-    name: "audio_effects_config.xml",
-    src: "audio_effects_config.xml",
-    installable: false,
-}
diff --git a/audio/aidl/default/apex/com.android.hardware.audio/Android.bp b/audio/aidl/default/apex/com.android.hardware.audio/Android.bp
index 2ece7a1..ee7e46e 100644
--- a/audio/aidl/default/apex/com.android.hardware.audio/Android.bp
+++ b/audio/aidl/default/apex/com.android.hardware.audio/Android.bp
@@ -46,6 +46,5 @@
     prebuilts: [
         "android.hardware.audio.service-aidl.example.rc",
         "android.hardware.audio.service-aidl.xml",
-        "audio_effects_config.xml",
     ],
 }
diff --git a/audio/aidl/vts/Android.bp b/audio/aidl/vts/Android.bp
index 5218fdd..d219fa4 100644
--- a/audio/aidl/vts/Android.bp
+++ b/audio/aidl/vts/Android.bp
@@ -26,6 +26,7 @@
         "android.hardware.common.fmq-V1-ndk",
         "libaudioaidlcommon",
         "libaidlcommonsupport",
+        "libpffft",
     ],
     header_libs: [
         "libaudioaidl_headers",
@@ -36,6 +37,7 @@
         "-Wextra",
         "-Werror",
         "-Wthread-safety",
+        "-Wno-error=unused-parameter",
     ],
     test_config_template: "VtsHalAudioTargetTestTemplate.xml",
     test_suites: [
diff --git a/audio/aidl/vts/EffectHelper.h b/audio/aidl/vts/EffectHelper.h
index 0be4e50..82a07fd 100644
--- a/audio/aidl/vts/EffectHelper.h
+++ b/audio/aidl/vts/EffectHelper.h
@@ -37,6 +37,7 @@
 
 #include "EffectFactoryHelper.h"
 #include "TestUtils.h"
+#include "pffft.hpp"
 
 using namespace android;
 using aidl::android::hardware::audio::effect::CommandId;
@@ -329,4 +330,45 @@
         ASSERT_NO_FATAL_FAILURE(command(mEffect, CommandId::RESET));
         ASSERT_NO_FATAL_FAILURE(expectState(mEffect, State::IDLE));
     }
+
+    // Find FFT bin indices for testFrequencies and get bin center frequencies
+    void roundToFreqCenteredToFftBin(std::vector<int>& testFrequencies,
+                                     std::vector<int>& binOffsets, const float kBinWidth) {
+        for (size_t i = 0; i < testFrequencies.size(); i++) {
+            binOffsets[i] = std::round(testFrequencies[i] / kBinWidth);
+            testFrequencies[i] = std::round(binOffsets[i] * kBinWidth);
+        }
+    }
+
+    // Generate multitone input between -1 to +1 using testFrequencies
+    void generateMultiTone(const std::vector<int>& testFrequencies, std::vector<float>& input,
+                           const int samplingFrequency) {
+        for (size_t i = 0; i < input.size(); i++) {
+            input[i] = 0;
+
+            for (size_t j = 0; j < testFrequencies.size(); j++) {
+                input[i] += sin(2 * M_PI * testFrequencies[j] * i / samplingFrequency);
+            }
+            input[i] /= testFrequencies.size();
+        }
+    }
+
+    // Use FFT transform to convert the buffer to frequency domain
+    // Compute its magnitude at binOffsets
+    std::vector<float> calculateMagnitude(const std::vector<float>& buffer,
+                                          const std::vector<int>& binOffsets, const int nPointFFT) {
+        std::vector<float> fftInput(nPointFFT);
+        PFFFT_Setup* inputHandle = pffft_new_setup(nPointFFT, PFFFT_REAL);
+        pffft_transform_ordered(inputHandle, buffer.data(), fftInput.data(), nullptr,
+                                PFFFT_FORWARD);
+        pffft_destroy_setup(inputHandle);
+        std::vector<float> bufferMag(binOffsets.size());
+        for (size_t i = 0; i < binOffsets.size(); i++) {
+            size_t k = binOffsets[i];
+            bufferMag[i] = sqrt((fftInput[k * 2] * fftInput[k * 2]) +
+                                (fftInput[k * 2 + 1] * fftInput[k * 2 + 1]));
+        }
+
+        return bufferMag;
+    }
 };
diff --git a/audio/aidl/vts/VtsHalVolumeTargetTest.cpp b/audio/aidl/vts/VtsHalVolumeTargetTest.cpp
index aa2c05f..059d6ab 100644
--- a/audio/aidl/vts/VtsHalVolumeTargetTest.cpp
+++ b/audio/aidl/vts/VtsHalVolumeTargetTest.cpp
@@ -21,6 +21,7 @@
 
 using namespace android;
 
+using aidl::android::hardware::audio::common::getChannelCount;
 using aidl::android::hardware::audio::effect::Descriptor;
 using aidl::android::hardware::audio::effect::getEffectTypeUuidVolume;
 using aidl::android::hardware::audio::effect::IEffect;
@@ -29,6 +30,80 @@
 using aidl::android::hardware::audio::effect::Volume;
 using android::hardware::audio::common::testing::detail::TestExecutionTracer;
 
+class VolumeControlHelper : public EffectHelper {
+  public:
+    void SetUpVolumeControl() {
+        ASSERT_NE(nullptr, mFactory);
+        ASSERT_NO_FATAL_FAILURE(create(mFactory, mEffect, mDescriptor));
+        initFrameCount();
+        Parameter::Specific specific = getDefaultParamSpecific();
+        Parameter::Common common = EffectHelper::createParamCommon(
+                0 /* session */, 1 /* ioHandle */, kSamplingFrequency /* iSampleRate */,
+                kSamplingFrequency /* oSampleRate */, mInputFrameCount /* iFrameCount */,
+                mInputFrameCount /* oFrameCount */);
+        ASSERT_NO_FATAL_FAILURE(open(mEffect, common, specific, &mOpenEffectReturn, EX_NONE));
+        ASSERT_NE(nullptr, mEffect);
+    }
+
+    void TearDownVolumeControl() {
+        ASSERT_NO_FATAL_FAILURE(close(mEffect));
+        ASSERT_NO_FATAL_FAILURE(destroy(mFactory, mEffect));
+        mOpenEffectReturn = IEffect::OpenEffectReturn{};
+    }
+
+    Parameter::Specific getDefaultParamSpecific() {
+        Volume vol = Volume::make<Volume::levelDb>(kMinLevel);
+        Parameter::Specific specific = Parameter::Specific::make<Parameter::Specific::volume>(vol);
+        return specific;
+    }
+
+    Parameter createVolumeParam(int param, Volume::Tag volTag) {
+        return Parameter::make<Parameter::specific>(
+                Parameter::Specific::make<Parameter::Specific::volume>(
+                        (volTag == Volume::mute) ? Volume::make<Volume::mute>(param)
+                                                 : Volume::make<Volume::levelDb>(param)));
+    }
+
+    void initFrameCount() {
+        int channelCount = getChannelCount(
+                AudioChannelLayout::make<AudioChannelLayout::layoutMask>(kDefaultChannelLayout));
+        mInputFrameCount = kBufferSize / channelCount;
+        mOutputFrameCount = kBufferSize / channelCount;
+    }
+
+    bool isLevelValid(int level) {
+        auto vol = Volume::make<Volume::levelDb>(level);
+        return isParameterValid<Volume, Range::volume>(vol, mDescriptor);
+    }
+
+    void setAndVerifyParameters(Volume::Tag volTag, int param, binder_exception_t expected) {
+        auto expectedParam = createVolumeParam(param, volTag);
+        EXPECT_STATUS(expected, mEffect->setParameter(expectedParam)) << expectedParam.toString();
+
+        if (expected == EX_NONE) {
+            Volume::Id volId = Volume::Id::make<Volume::Id::commonTag>(volTag);
+
+            auto id = Parameter::Id::make<Parameter::Id::volumeTag>(volId);
+            // get parameter
+            Parameter getParam;
+            // if set success, then get should match
+            EXPECT_STATUS(expected, mEffect->getParameter(id, &getParam));
+            EXPECT_EQ(expectedParam, getParam) << "\nexpectedParam:" << expectedParam.toString()
+                                               << "\ngetParam:" << getParam.toString();
+        }
+    }
+
+    static constexpr int kSamplingFrequency = 44100;
+    static constexpr int kDurationMilliSec = 2000;
+    static constexpr int kBufferSize = kSamplingFrequency * kDurationMilliSec / 1000;
+    static constexpr int kMinLevel = -96;
+    static constexpr int kDefaultChannelLayout = AudioChannelLayout::LAYOUT_STEREO;
+    long mInputFrameCount, mOutputFrameCount;
+    std::shared_ptr<IFactory> mFactory;
+    std::shared_ptr<IEffect> mEffect;
+    IEffect::OpenEffectReturn mOpenEffectReturn;
+    Descriptor mDescriptor;
+};
 /**
  * Here we focus on specific parameter checking, general IEffect interfaces testing performed in
  * VtsAudioEffectTargetTest.
@@ -37,7 +112,8 @@
 using VolumeParamTestParam =
         std::tuple<std::pair<std::shared_ptr<IFactory>, Descriptor>, int, bool>;
 
-class VolumeParamTest : public ::testing::TestWithParam<VolumeParamTestParam>, public EffectHelper {
+class VolumeParamTest : public ::testing::TestWithParam<VolumeParamTestParam>,
+                        public VolumeControlHelper {
   public:
     VolumeParamTest()
         : mParamLevel(std::get<PARAM_LEVEL>(GetParam())),
@@ -45,94 +121,167 @@
         std::tie(mFactory, mDescriptor) = std::get<PARAM_INSTANCE_NAME>(GetParam());
     }
 
-    void SetUp() override {
-        ASSERT_NE(nullptr, mFactory);
-        ASSERT_NO_FATAL_FAILURE(create(mFactory, mEffect, mDescriptor));
+    void SetUp() override { ASSERT_NO_FATAL_FAILURE(SetUpVolumeControl()); }
+    void TearDown() override { TearDownVolumeControl(); }
 
-        Parameter::Specific specific = getDefaultParamSpecific();
-        Parameter::Common common = EffectHelper::createParamCommon(
-                0 /* session */, 1 /* ioHandle */, 44100 /* iSampleRate */, 44100 /* oSampleRate */,
-                kInputFrameCount /* iFrameCount */, kOutputFrameCount /* oFrameCount */);
-        IEffect::OpenEffectReturn ret;
-        ASSERT_NO_FATAL_FAILURE(open(mEffect, common, specific, &ret, EX_NONE));
-        ASSERT_NE(nullptr, mEffect);
-    }
-    void TearDown() override {
-        ASSERT_NO_FATAL_FAILURE(close(mEffect));
-        ASSERT_NO_FATAL_FAILURE(destroy(mFactory, mEffect));
-    }
-
-    Parameter::Specific getDefaultParamSpecific() {
-        Volume vol = Volume::make<Volume::levelDb>(-9600);
-        Parameter::Specific specific = Parameter::Specific::make<Parameter::Specific::volume>(vol);
-        return specific;
-    }
-
-    static const long kInputFrameCount = 0x100, kOutputFrameCount = 0x100;
-    std::shared_ptr<IFactory> mFactory;
-    std::shared_ptr<IEffect> mEffect;
-    Descriptor mDescriptor;
     int mParamLevel = 0;
     bool mParamMute = false;
-
-    void SetAndGetParameters() {
-        for (auto& it : mTags) {
-            auto& tag = it.first;
-            auto& vol = it.second;
-
-            // validate parameter
-            Descriptor desc;
-            ASSERT_STATUS(EX_NONE, mEffect->getDescriptor(&desc));
-            const bool valid = isParameterValid<Volume, Range::volume>(it.second, desc);
-            const binder_exception_t expected = valid ? EX_NONE : EX_ILLEGAL_ARGUMENT;
-
-            // set parameter
-            Parameter expectParam;
-            Parameter::Specific specific;
-            specific.set<Parameter::Specific::volume>(vol);
-            expectParam.set<Parameter::specific>(specific);
-            EXPECT_STATUS(expected, mEffect->setParameter(expectParam)) << expectParam.toString();
-
-            // only get if parameter is in range and set success
-            if (expected == EX_NONE) {
-                Parameter getParam;
-                Parameter::Id id;
-                Volume::Id volId;
-                volId.set<Volume::Id::commonTag>(tag);
-                id.set<Parameter::Id::volumeTag>(volId);
-                EXPECT_STATUS(EX_NONE, mEffect->getParameter(id, &getParam));
-
-                EXPECT_EQ(expectParam, getParam) << "\nexpect:" << expectParam.toString()
-                                                 << "\ngetParam:" << getParam.toString();
-            }
-        }
-    }
-
-    void addLevelParam(int level) {
-        Volume vol;
-        vol.set<Volume::levelDb>(level);
-        mTags.push_back({Volume::levelDb, vol});
-    }
-
-    void addMuteParam(bool mute) {
-        Volume vol;
-        vol.set<Volume::mute>(mute);
-        mTags.push_back({Volume::mute, vol});
-    }
-
-  private:
-    std::vector<std::pair<Volume::Tag, Volume>> mTags;
-    void CleanUp() { mTags.clear(); }
 };
 
-TEST_P(VolumeParamTest, SetAndGetLevel) {
-    EXPECT_NO_FATAL_FAILURE(addLevelParam(mParamLevel));
-    SetAndGetParameters();
+TEST_P(VolumeParamTest, SetAndGetParams) {
+    ASSERT_NO_FATAL_FAILURE(
+            setAndVerifyParameters(Volume::levelDb, mParamLevel,
+                                   isLevelValid(mParamLevel) ? EX_NONE : EX_ILLEGAL_ARGUMENT));
+    ASSERT_NO_FATAL_FAILURE(setAndVerifyParameters(Volume::mute, mParamMute, EX_NONE));
 }
 
-TEST_P(VolumeParamTest, SetAndGetMute) {
-    EXPECT_NO_FATAL_FAILURE(addMuteParam(mParamMute));
-    SetAndGetParameters();
+using VolumeDataTestParam = std::pair<std::shared_ptr<IFactory>, Descriptor>;
+
+class VolumeDataTest : public ::testing::TestWithParam<VolumeDataTestParam>,
+                       public VolumeControlHelper {
+  public:
+    VolumeDataTest() {
+        std::tie(mFactory, mDescriptor) = GetParam();
+        mInput.resize(kBufferSize);
+        mInputMag.resize(mTestFrequencies.size());
+        mBinOffsets.resize(mTestFrequencies.size());
+        roundToFreqCenteredToFftBin(mTestFrequencies, mBinOffsets, kBinWidth);
+        generateMultiTone(mTestFrequencies, mInput, kSamplingFrequency);
+        mInputMag = calculateMagnitude(mInput, mBinOffsets, kNPointFFT);
+    }
+
+    std::vector<int> calculatePercentageDiff(const std::vector<float>& outputMag) {
+        std::vector<int> percentages(mTestFrequencies.size());
+
+        for (size_t i = 0; i < mInputMag.size(); i++) {
+            float diff = mInputMag[i] - outputMag[i];
+            percentages[i] = std::round(diff / mInputMag[i] * 100);
+        }
+        return percentages;
+    }
+
+    // Convert Decibel value to Percentage
+    int percentageDb(float level) { return std::round((1 - (pow(10, level / 20))) * 100); }
+
+    void SetUp() override { ASSERT_NO_FATAL_FAILURE(SetUpVolumeControl()); }
+    void TearDown() override { TearDownVolumeControl(); }
+
+    static constexpr int kMaxAudioSample = 1;
+    static constexpr int kTransitionDuration = 300;
+    static constexpr int kNPointFFT = 32768;
+    static constexpr float kBinWidth = (float)kSamplingFrequency / kNPointFFT;
+    static constexpr size_t offset = kSamplingFrequency * kTransitionDuration / 1000;
+    static constexpr float kBaseLevel = 0;
+    std::vector<int> mTestFrequencies = {100, 1000};
+    std::vector<float> mInput;
+    std::vector<float> mInputMag;
+    std::vector<int> mBinOffsets;
+};
+
+TEST_P(VolumeDataTest, ApplyLevelMuteUnmute) {
+    std::vector<float> output(kBufferSize);
+    std::vector<int> diffs(mTestFrequencies.size());
+    std::vector<float> outputMag(mTestFrequencies.size());
+
+    if (!isLevelValid(kBaseLevel)) {
+        GTEST_SKIP() << "Volume Level not supported, skipping the test\n";
+    }
+
+    // Apply Volume Level
+
+    ASSERT_NO_FATAL_FAILURE(setAndVerifyParameters(Volume::levelDb, kBaseLevel, EX_NONE));
+    ASSERT_NO_FATAL_FAILURE(processAndWriteToOutput(mInput, output, mEffect, &mOpenEffectReturn));
+
+    outputMag = calculateMagnitude(output, mBinOffsets, kNPointFFT);
+    diffs = calculatePercentageDiff(outputMag);
+
+    for (size_t i = 0; i < diffs.size(); i++) {
+        ASSERT_EQ(diffs[i], percentageDb(kBaseLevel));
+    }
+
+    // Apply Mute
+
+    ASSERT_NO_FATAL_FAILURE(setAndVerifyParameters(Volume::mute, true /*mute*/, EX_NONE));
+    ASSERT_NO_FATAL_FAILURE(processAndWriteToOutput(mInput, output, mEffect, &mOpenEffectReturn));
+
+    std::vector<float> subOutputMute(output.begin() + offset, output.end());
+    outputMag = calculateMagnitude(subOutputMute, mBinOffsets, kNPointFFT);
+    diffs = calculatePercentageDiff(outputMag);
+
+    for (size_t i = 0; i < diffs.size(); i++) {
+        ASSERT_EQ(diffs[i], percentageDb(kMinLevel /*Mute*/));
+    }
+
+    // Verifying Fade out
+    outputMag = calculateMagnitude(output, mBinOffsets, kNPointFFT);
+    diffs = calculatePercentageDiff(outputMag);
+
+    for (size_t i = 0; i < diffs.size(); i++) {
+        ASSERT_LT(diffs[i], percentageDb(kMinLevel /*Mute*/));
+    }
+
+    // Apply Unmute
+
+    ASSERT_NO_FATAL_FAILURE(setAndVerifyParameters(Volume::mute, false /*unmute*/, EX_NONE));
+    ASSERT_NO_FATAL_FAILURE(processAndWriteToOutput(mInput, output, mEffect, &mOpenEffectReturn));
+
+    std::vector<float> subOutputUnmute(output.begin() + offset, output.end());
+
+    outputMag = calculateMagnitude(subOutputUnmute, mBinOffsets, kNPointFFT);
+    diffs = calculatePercentageDiff(outputMag);
+
+    for (size_t i = 0; i < diffs.size(); i++) {
+        ASSERT_EQ(diffs[i], percentageDb(kBaseLevel));
+    }
+
+    // Verifying Fade in
+    outputMag = calculateMagnitude(output, mBinOffsets, kNPointFFT);
+    diffs = calculatePercentageDiff(outputMag);
+
+    for (size_t i = 0; i < diffs.size(); i++) {
+        ASSERT_GT(diffs[i], percentageDb(kBaseLevel));
+    }
+}
+
+TEST_P(VolumeDataTest, DecreasingLevels) {
+    std::vector<int> decreasingLevels = {-24, -48, -96};
+    std::vector<float> baseOutput(kBufferSize);
+    std::vector<int> baseDiffs(mTestFrequencies.size());
+    std::vector<float> outputMag(mTestFrequencies.size());
+
+    if (!isLevelValid(kBaseLevel)) {
+        GTEST_SKIP() << "Volume Level not supported, skipping the test\n";
+    }
+
+    ASSERT_NO_FATAL_FAILURE(setAndVerifyParameters(Volume::levelDb, kBaseLevel, EX_NONE));
+    ASSERT_NO_FATAL_FAILURE(
+            processAndWriteToOutput(mInput, baseOutput, mEffect, &mOpenEffectReturn));
+
+    outputMag = calculateMagnitude(baseOutput, mBinOffsets, kNPointFFT);
+    baseDiffs = calculatePercentageDiff(outputMag);
+
+    for (int level : decreasingLevels) {
+        std::vector<float> output(kBufferSize);
+        std::vector<int> diffs(mTestFrequencies.size());
+
+        // Skipping the further steps for unnsupported level values
+        if (!isLevelValid(level)) {
+            continue;
+        }
+        ASSERT_NO_FATAL_FAILURE(setAndVerifyParameters(Volume::levelDb, level, EX_NONE));
+        ASSERT_NO_FATAL_FAILURE(
+                processAndWriteToOutput(mInput, output, mEffect, &mOpenEffectReturn));
+
+        outputMag = calculateMagnitude(output, mBinOffsets, kNPointFFT);
+        diffs = calculatePercentageDiff(outputMag);
+
+        // Decrease in volume level results in greater magnitude difference
+        for (size_t i = 0; i < diffs.size(); i++) {
+            ASSERT_GT(diffs[i], baseDiffs[i]);
+        }
+
+        baseDiffs = diffs;
+    }
 }
 
 std::vector<std::pair<std::shared_ptr<IFactory>, Descriptor>> kDescPair;
@@ -157,6 +306,20 @@
 
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(VolumeParamTest);
 
+INSTANTIATE_TEST_SUITE_P(VolumeTest, VolumeDataTest,
+                         testing::ValuesIn(EffectFactoryHelper::getAllEffectDescriptors(
+                                 IFactory::descriptor, getEffectTypeUuidVolume())),
+                         [](const testing::TestParamInfo<VolumeDataTest::ParamType>& 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(VolumeDataTest);
+
 int main(int argc, char** argv) {
     ::testing::InitGoogleTest(&argc, argv);
     ::testing::UnitTest::GetInstance()->listeners().Append(new TestExecutionTracer());
diff --git a/automotive/vehicle/aidl/emu_metadata/android.hardware.automotive.vehicle-types-meta.json b/automotive/vehicle/aidl/emu_metadata/android.hardware.automotive.vehicle-types-meta.json
index df0f51c..c812326 100644
--- a/automotive/vehicle/aidl/emu_metadata/android.hardware.automotive.vehicle-types-meta.json
+++ b/automotive/vehicle/aidl/emu_metadata/android.hardware.automotive.vehicle-types-meta.json
@@ -1089,7 +1089,7 @@
                 "data_enum": "TrailerState"
             },
             {
-                "name": "Vehicle’s curb weight",
+                "name": "VEHICLE_CURB_WEIGHT",
                 "value": 289410886
             },
             {
diff --git a/automotive/vehicle/aidl/generated_lib/java/UnitsForVehicleProperty.java b/automotive/vehicle/aidl/generated_lib/java/UnitsForVehicleProperty.java
new file mode 100644
index 0000000..b30c8e6
--- /dev/null
+++ b/automotive/vehicle/aidl/generated_lib/java/UnitsForVehicleProperty.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+/**
+ * DO NOT EDIT MANUALLY!!!
+ *
+ * Generated by tools/generate_annotation_enums.py.
+ */
+
+// clang-format off
+
+package android.hardware.automotive.vehicle;
+
+import java.util.Map;
+
+public final class UnitsForVehicleProperty {
+
+    public static final Map<Integer, Integer> values = Map.ofEntries(
+        Map.entry(VehicleProperty.INFO_MODEL_YEAR, VehicleUnit.YEAR),
+        Map.entry(VehicleProperty.INFO_FUEL_CAPACITY, VehicleUnit.MILLILITER),
+        Map.entry(VehicleProperty.INFO_EV_BATTERY_CAPACITY, VehicleUnit.WATT_HOUR),
+        Map.entry(VehicleProperty.INFO_EXTERIOR_DIMENSIONS, VehicleUnit.MILLIMETER),
+        Map.entry(VehicleProperty.PERF_ODOMETER, VehicleUnit.KILOMETER),
+        Map.entry(VehicleProperty.PERF_VEHICLE_SPEED, VehicleUnit.METER_PER_SEC),
+        Map.entry(VehicleProperty.PERF_VEHICLE_SPEED_DISPLAY, VehicleUnit.METER_PER_SEC),
+        Map.entry(VehicleProperty.PERF_STEERING_ANGLE, VehicleUnit.DEGREES),
+        Map.entry(VehicleProperty.PERF_REAR_STEERING_ANGLE, VehicleUnit.DEGREES),
+        Map.entry(VehicleProperty.ENGINE_COOLANT_TEMP, VehicleUnit.CELSIUS),
+        Map.entry(VehicleProperty.ENGINE_OIL_TEMP, VehicleUnit.CELSIUS),
+        Map.entry(VehicleProperty.ENGINE_RPM, VehicleUnit.RPM),
+        Map.entry(VehicleProperty.FUEL_LEVEL, VehicleUnit.MILLILITER),
+        Map.entry(VehicleProperty.EV_BATTERY_LEVEL, VehicleUnit.WATT_HOUR),
+        Map.entry(VehicleProperty.EV_CURRENT_BATTERY_CAPACITY, VehicleUnit.WATT_HOUR),
+        Map.entry(VehicleProperty.EV_BATTERY_INSTANTANEOUS_CHARGE_RATE, VehicleUnit.MILLIWATTS),
+        Map.entry(VehicleProperty.RANGE_REMAINING, VehicleUnit.METER),
+        Map.entry(VehicleProperty.EV_BATTERY_AVERAGE_TEMPERATURE, VehicleUnit.CELSIUS),
+        Map.entry(VehicleProperty.TIRE_PRESSURE, VehicleUnit.KILOPASCAL),
+        Map.entry(VehicleProperty.CRITICALLY_LOW_TIRE_PRESSURE, VehicleUnit.KILOPASCAL),
+        Map.entry(VehicleProperty.HVAC_TEMPERATURE_CURRENT, VehicleUnit.CELSIUS),
+        Map.entry(VehicleProperty.HVAC_TEMPERATURE_SET, VehicleUnit.CELSIUS),
+        Map.entry(VehicleProperty.EXTERNAL_CAR_TIME, VehicleUnit.MILLI_SECS),
+        Map.entry(VehicleProperty.ANDROID_EPOCH_TIME, VehicleUnit.MILLI_SECS),
+        Map.entry(VehicleProperty.ENV_OUTSIDE_TEMPERATURE, VehicleUnit.CELSIUS),
+        Map.entry(VehicleProperty.WINDSHIELD_WIPERS_PERIOD, VehicleUnit.MILLI_SECS),
+        Map.entry(VehicleProperty.EV_CHARGE_CURRENT_DRAW_LIMIT, VehicleUnit.AMPERE),
+        Map.entry(VehicleProperty.EV_CHARGE_TIME_REMAINING, VehicleUnit.SECS),
+        Map.entry(VehicleProperty.CRUISE_CONTROL_TARGET_SPEED, VehicleUnit.METER_PER_SEC),
+        Map.entry(VehicleProperty.ADAPTIVE_CRUISE_CONTROL_TARGET_TIME_GAP, VehicleUnit.MILLI_SECS),
+        Map.entry(VehicleProperty.ADAPTIVE_CRUISE_CONTROL_LEAD_VEHICLE_MEASURED_DISTANCE, VehicleUnit.MILLIMETER)
+    );
+
+}
diff --git a/automotive/vehicle/aidl/impl/utils/common/include/VehicleUtils.h b/automotive/vehicle/aidl/impl/utils/common/include/VehicleUtils.h
index 523cac5..aca725d 100644
--- a/automotive/vehicle/aidl/impl/utils/common/include/VehicleUtils.h
+++ b/automotive/vehicle/aidl/impl/utils/common/include/VehicleUtils.h
@@ -333,6 +333,23 @@
             static_cast<aidl::android::hardware::automotive::vehicle::VehicleProperty>(propId));
 }
 
+template <typename T>
+void roundToNearestResolution(std::vector<T>& arrayToSanitize, float resolution) {
+    if (resolution == 0) {
+        return;
+    }
+    for (size_t i = 0; i < arrayToSanitize.size(); i++) {
+        arrayToSanitize[i] = (T)((std::round(arrayToSanitize[i] / resolution)) * resolution);
+    }
+}
+
+inline void sanitizeByResolution(aidl::android::hardware::automotive::vehicle::RawPropValues* value,
+                                 float resolution) {
+    roundToNearestResolution(value->int32Values, resolution);
+    roundToNearestResolution(value->floatValues, resolution);
+    roundToNearestResolution(value->int64Values, resolution);
+}
+
 }  // namespace vehicle
 }  // namespace automotive
 }  // namespace hardware
diff --git a/automotive/vehicle/aidl/impl/vhal/include/SubscriptionManager.h b/automotive/vehicle/aidl/impl/vhal/include/SubscriptionManager.h
index 5053c96..2f16fca 100644
--- a/automotive/vehicle/aidl/impl/vhal/include/SubscriptionManager.h
+++ b/automotive/vehicle/aidl/impl/vhal/include/SubscriptionManager.h
@@ -25,6 +25,8 @@
 #include <android-base/result.h>
 #include <android-base/thread_annotations.h>
 
+#include <cmath>
+#include <limits>
 #include <mutex>
 #include <optional>
 #include <unordered_map>
@@ -39,6 +41,7 @@
 // A structure to represent subscription config for one subscription client.
 struct SubConfig {
     float sampleRateHz;
+    float resolution;
     bool enableVur;
 };
 
@@ -47,14 +50,19 @@
   public:
     using ClientIdType = const AIBinder*;
 
-    void addClient(const ClientIdType& clientId, float sampleRateHz, bool enableVur);
+    void addClient(const ClientIdType& clientId, const SubConfig& subConfig);
     void removeClient(const ClientIdType& clientId);
     float getMaxSampleRateHz() const;
+    float getMinRequiredResolution() const;
     bool isVurEnabled() const;
-    bool isVurEnabledForClient(const ClientIdType& clientId);
+    bool isVurEnabledForClient(const ClientIdType& clientId) const;
+    float getResolutionForClient(const ClientIdType& clientId) const;
 
   private:
     float mMaxSampleRateHz = 0.;
+    // Baseline for resolution is maximum possible float. We want to sanitize to the highest
+    // requested resolution, which is the smallest float value for resolution.
+    float mMinRequiredResolution = std::numeric_limits<float>::max();
     bool mEnableVur;
     std::unordered_map<ClientIdType, SubConfig> mConfigByClient;
 
@@ -117,6 +125,9 @@
     // Checks whether the sample rate is valid.
     static bool checkSampleRateHz(float sampleRateHz);
 
+    // Checks whether the resolution is valid.
+    static bool checkResolution(float resolution);
+
   private:
     // Friend class for testing.
     friend class DefaultVehicleHalTest;
@@ -153,8 +164,8 @@
 
     VhalResult<void> addContinuousSubscriberLocked(const ClientIdType& clientId,
                                                    const PropIdAreaId& propIdAreaId,
-                                                   float sampleRateHz, bool enableVur)
-            REQUIRES(mLock);
+                                                   float sampleRateHz, float resolution,
+                                                   bool enableVur) REQUIRES(mLock);
     VhalResult<void> addOnChangeSubscriberLocked(const PropIdAreaId& propIdAreaId) REQUIRES(mLock);
     // Removes the subscription client for the continuous [propId, areaId].
     VhalResult<void> removeContinuousSubscriberLocked(const ClientIdType& clientId,
diff --git a/automotive/vehicle/aidl/impl/vhal/src/DefaultVehicleHal.cpp b/automotive/vehicle/aidl/impl/vhal/src/DefaultVehicleHal.cpp
index cc5edcc..a29861f 100644
--- a/automotive/vehicle/aidl/impl/vhal/src/DefaultVehicleHal.cpp
+++ b/automotive/vehicle/aidl/impl/vhal/src/DefaultVehicleHal.cpp
@@ -722,6 +722,10 @@
                 return StatusError(StatusCode::INVALID_ARG)
                        << "invalid sample rate: " << sampleRateHz << " HZ";
             }
+            if (!SubscriptionManager::checkResolution(option.resolution)) {
+                return StatusError(StatusCode::INVALID_ARG)
+                       << "invalid resolution: " << option.resolution;
+            }
         }
     }
     return {};
diff --git a/automotive/vehicle/aidl/impl/vhal/src/SubscriptionManager.cpp b/automotive/vehicle/aidl/impl/vhal/src/SubscriptionManager.cpp
index 29d81a7..f1106ee 100644
--- a/automotive/vehicle/aidl/impl/vhal/src/SubscriptionManager.cpp
+++ b/automotive/vehicle/aidl/impl/vhal/src/SubscriptionManager.cpp
@@ -43,11 +43,12 @@
 constexpr float ONE_SECOND_IN_NANOS = 1'000'000'000.;
 
 SubscribeOptions newSubscribeOptions(int32_t propId, int32_t areaId, float sampleRateHz,
-                                     bool enableVur) {
+                                     float resolution, bool enableVur) {
     SubscribeOptions subscribedOptions;
     subscribedOptions.propId = propId;
     subscribedOptions.areaIds = {areaId};
     subscribedOptions.sampleRate = sampleRateHz;
+    subscribedOptions.resolution = resolution;
     subscribedOptions.enableVariableUpdateRate = enableVur;
 
     return subscribedOptions;
@@ -81,8 +82,18 @@
     return intervalNanos;
 }
 
+bool SubscriptionManager::checkResolution(float resolution) {
+    if (resolution == 0) {
+        return true;
+    }
+
+    float log = std::log10(resolution);
+    return log == (int)log;
+}
+
 void ContSubConfigs::refreshCombinedConfig() {
     float maxSampleRateHz = 0.;
+    float minRequiredResolution = std::numeric_limits<float>::max();
     bool enableVur = true;
     // This is not called frequently so a brute-focre is okay. More efficient way exists but this
     // is simpler.
@@ -90,6 +101,9 @@
         if (subConfig.sampleRateHz > maxSampleRateHz) {
             maxSampleRateHz = subConfig.sampleRateHz;
         }
+        if (subConfig.resolution < minRequiredResolution) {
+            minRequiredResolution = subConfig.resolution;
+        }
         if (!subConfig.enableVur) {
             // If one client does not enable variable update rate, we cannot enable variable update
             // rate in IVehicleHardware.
@@ -97,14 +111,12 @@
         }
     }
     mMaxSampleRateHz = maxSampleRateHz;
+    mMinRequiredResolution = minRequiredResolution;
     mEnableVur = enableVur;
 }
 
-void ContSubConfigs::addClient(const ClientIdType& clientId, float sampleRateHz, bool enableVur) {
-    mConfigByClient[clientId] = {
-            .sampleRateHz = sampleRateHz,
-            .enableVur = enableVur,
-    };
+void ContSubConfigs::addClient(const ClientIdType& clientId, const SubConfig& subConfig) {
+    mConfigByClient[clientId] = subConfig;
     refreshCombinedConfig();
 }
 
@@ -117,12 +129,26 @@
     return mMaxSampleRateHz;
 }
 
+float ContSubConfigs::getMinRequiredResolution() const {
+    return mMinRequiredResolution;
+}
+
 bool ContSubConfigs::isVurEnabled() const {
     return mEnableVur;
 }
 
-bool ContSubConfigs::isVurEnabledForClient(const ClientIdType& clientId) {
-    return mConfigByClient[clientId].enableVur;
+bool ContSubConfigs::isVurEnabledForClient(const ClientIdType& clientId) const {
+    if (mConfigByClient.find(clientId) == mConfigByClient.end()) {
+        return false;
+    }
+    return mConfigByClient.at(clientId).enableVur;
+}
+
+float ContSubConfigs::getResolutionForClient(const ClientIdType& clientId) const {
+    if (mConfigByClient.find(clientId) == mConfigByClient.end()) {
+        return 0.0f;
+    }
+    return mConfigByClient.at(clientId).resolution;
 }
 
 VhalResult<void> SubscriptionManager::addOnChangeSubscriberLocked(
@@ -135,7 +161,8 @@
     int32_t propId = propIdAreaId.propId;
     int32_t areaId = propIdAreaId.areaId;
     if (auto status = mVehicleHardware->subscribe(
-                newSubscribeOptions(propId, areaId, /*updateRateHz=*/0, /*enableVur*/ false));
+                newSubscribeOptions(propId, areaId, /*updateRateHz=*/0, /*resolution*/ 0.0f,
+                                    /*enableVur*/ false));
         status != StatusCode::OK) {
         return StatusError(status)
                << StringPrintf("failed subscribe for prop: %s, areaId: %" PRId32,
@@ -146,10 +173,15 @@
 
 VhalResult<void> SubscriptionManager::addContinuousSubscriberLocked(
         const ClientIdType& clientId, const PropIdAreaId& propIdAreaId, float sampleRateHz,
-        bool enableVur) {
+        float resolution, bool enableVur) {
     // Make a copy so that we don't modify 'mContSubConfigsByPropIdArea' on failure cases.
     ContSubConfigs newConfig = mContSubConfigsByPropIdArea[propIdAreaId];
-    newConfig.addClient(clientId, sampleRateHz, enableVur);
+    SubConfig subConfig = {
+            .sampleRateHz = sampleRateHz,
+            .resolution = resolution,
+            .enableVur = enableVur,
+    };
+    newConfig.addClient(clientId, subConfig);
     return updateContSubConfigsLocked(propIdAreaId, newConfig);
 }
 
@@ -183,7 +215,10 @@
     const auto& oldConfig = mContSubConfigsByPropIdArea[propIdAreaId];
     float newRateHz = newConfig.getMaxSampleRateHz();
     float oldRateHz = oldConfig.getMaxSampleRateHz();
-    if (newRateHz == oldRateHz && newConfig.isVurEnabled() == oldConfig.isVurEnabled()) {
+    float newResolution = newConfig.getMinRequiredResolution();
+    float oldResolution = oldConfig.getMinRequiredResolution();
+    if (newRateHz == oldRateHz && newResolution == oldResolution &&
+        newConfig.isVurEnabled() == oldConfig.isVurEnabled()) {
         mContSubConfigsByPropIdArea[propIdAreaId] = newConfig;
         return {};
     }
@@ -199,8 +234,8 @@
         }
     }
     if (newRateHz != 0) {
-        if (auto status = mVehicleHardware->subscribe(
-                    newSubscribeOptions(propId, areaId, newRateHz, newConfig.isVurEnabled()));
+        if (auto status = mVehicleHardware->subscribe(newSubscribeOptions(
+                    propId, areaId, newRateHz, newResolution, newConfig.isVurEnabled()));
             status != StatusCode::OK) {
             return StatusError(status) << StringPrintf(
                            "failed subscribe for prop: %s, areaId"
@@ -231,6 +266,11 @@
             if (auto result = getIntervalNanos(sampleRateHz); !result.ok()) {
                 return StatusError(StatusCode::INVALID_ARG) << result.error().message();
             }
+            if (!checkResolution(option.resolution)) {
+                return StatusError(StatusCode::INVALID_ARG) << StringPrintf(
+                               "SubscribeOptions.resolution %f is not an integer power of 10",
+                               option.resolution);
+            }
         }
 
         if (option.areaIds.empty()) {
@@ -253,6 +293,7 @@
             VhalResult<void> result;
             if (isContinuousProperty) {
                 result = addContinuousSubscriberLocked(clientId, propIdAreaId, option.sampleRate,
+                                                       option.resolution,
                                                        option.enableVariableUpdateRate);
             } else {
                 result = addOnChangeSubscriberLocked(propIdAreaId);
@@ -393,15 +434,19 @@
 
         for (const auto& [client, callback] : mClientsByPropIdAreaId[propIdAreaId]) {
             auto& subConfigs = mContSubConfigsByPropIdArea[propIdAreaId];
+            // Clients must be sent different VehiclePropValues with different levels of granularity
+            // as requested by the client using resolution.
+            VehiclePropValue newValue = value;
+            sanitizeByResolution(&(newValue.value), subConfigs.getResolutionForClient(client));
             // If client wants VUR (and VUR is supported as checked in DefaultVehicleHal), it is
             // possible that VUR is not enabled in IVehicleHardware because another client does not
             // enable VUR. We will implement VUR filtering here for the client that enables it.
             if (subConfigs.isVurEnabledForClient(client) && !subConfigs.isVurEnabled()) {
-                if (isValueUpdatedLocked(callback, value)) {
-                    clients[callback].push_back(value);
+                if (isValueUpdatedLocked(callback, newValue)) {
+                    clients[callback].push_back(newValue);
                 }
             } else {
-                clients[callback].push_back(value);
+                clients[callback].push_back(newValue);
             }
         }
     }
diff --git a/automotive/vehicle/aidl/impl/vhal/test/DefaultVehicleHalTest.cpp b/automotive/vehicle/aidl/impl/vhal/test/DefaultVehicleHalTest.cpp
index bb82108..11a8fc7 100644
--- a/automotive/vehicle/aidl/impl/vhal/test/DefaultVehicleHalTest.cpp
+++ b/automotive/vehicle/aidl/impl/vhal/test/DefaultVehicleHalTest.cpp
@@ -234,6 +234,14 @@
                             },
             },
             {
+                    .name = "invalid_resolution",
+                    .option =
+                            {
+                                    .propId = GLOBAL_CONTINUOUS_PROP,
+                                    .resolution = 2.0,
+                            },
+            },
+            {
                     .name = "static_property",
                     .option =
                             {
diff --git a/automotive/vehicle/aidl/impl/vhal/test/SubscriptionManagerTest.cpp b/automotive/vehicle/aidl/impl/vhal/test/SubscriptionManagerTest.cpp
index aa5f003..f377202 100644
--- a/automotive/vehicle/aidl/impl/vhal/test/SubscriptionManagerTest.cpp
+++ b/automotive/vehicle/aidl/impl/vhal/test/SubscriptionManagerTest.cpp
@@ -520,6 +520,14 @@
     ASSERT_FALSE(SubscriptionManager::checkSampleRateHz(0));
 }
 
+TEST_F(SubscriptionManagerTest, testCheckResolutionValid) {
+    ASSERT_TRUE(SubscriptionManager::checkResolution(1.0));
+}
+
+TEST_F(SubscriptionManagerTest, testCheckResolutionInvalid) {
+    ASSERT_FALSE(SubscriptionManager::checkResolution(2.0));
+}
+
 TEST_F(SubscriptionManagerTest, testSubscribe_enableVur) {
     std::vector<SubscribeOptions> options = {{
             .propId = 0,
@@ -641,6 +649,102 @@
             << "Must filter out property events if VUR is enabled";
 }
 
+TEST_F(SubscriptionManagerTest, testSubscribe_enableVur_filterUnchangedEvents_withResolution) {
+    SpAIBinder binder1 = ndk::SharedRefBase::make<PropertyCallback>()->asBinder();
+    std::shared_ptr<IVehicleCallback> client1 = IVehicleCallback::fromBinder(binder1);
+    SpAIBinder binder2 = ndk::SharedRefBase::make<PropertyCallback>()->asBinder();
+    std::shared_ptr<IVehicleCallback> client2 = IVehicleCallback::fromBinder(binder2);
+    SubscribeOptions client1Option = {
+            .propId = 0,
+            .areaIds = {0},
+            .sampleRate = 10.0,
+            .resolution = 0.01,
+            .enableVariableUpdateRate = false,
+    };
+    auto result = getManager()->subscribe(client1, {client1Option}, true);
+    ASSERT_TRUE(result.ok()) << "failed to subscribe: " << result.error().message();
+
+    ASSERT_THAT(getHardware()->getSubscribeOptions(), UnorderedElementsAre(client1Option));
+
+    getHardware()->clearSubscribeOptions();
+    SubscribeOptions client2Option = {
+            .propId = 0,
+            .areaIds = {0, 1},
+            .sampleRate = 20.0,
+            .resolution = 0.1,
+            .enableVariableUpdateRate = true,
+    };
+
+    result = getManager()->subscribe(client2, {client2Option}, true);
+    ASSERT_TRUE(result.ok()) << "failed to subscribe: " << result.error().message();
+
+    ASSERT_THAT(getHardware()->getSubscribeOptions(),
+                UnorderedElementsAre(
+                        SubscribeOptions{
+                                .propId = 0,
+                                .areaIds = {0},
+                                .sampleRate = 20.0,
+                                .resolution = 0.01,
+                                // This is enabled for client2, but disabled for client1.
+                                .enableVariableUpdateRate = false,
+                        },
+                        SubscribeOptions{
+                                .propId = 0,
+                                .areaIds = {1},
+                                .sampleRate = 20.0,
+                                .resolution = 0.1,
+                                .enableVariableUpdateRate = true,
+                        }));
+
+    std::vector<VehiclePropValue> propertyEvents = {{
+                                                            .prop = 0,
+                                                            .areaId = 0,
+                                                            .value = {.floatValues = {1.0}},
+                                                            .timestamp = 1,
+                                                    },
+                                                    {
+                                                            .prop = 0,
+                                                            .areaId = 1,
+                                                            .value = {.floatValues = {1.0}},
+                                                            .timestamp = 1,
+                                                    }};
+    auto clients =
+            getManager()->getSubscribedClients(std::vector<VehiclePropValue>(propertyEvents));
+
+    ASSERT_THAT(clients[client1], UnorderedElementsAre(propertyEvents[0]));
+    ASSERT_THAT(clients[client2], UnorderedElementsAre(propertyEvents[0], propertyEvents[1]));
+
+    clients = getManager()->getSubscribedClients({{
+            .prop = 0,
+            .areaId = 0,
+            .value = {.floatValues = {1.01}},
+            .timestamp = 2,
+    }});
+
+    ASSERT_FALSE(clients.find(client1) == clients.end())
+            << "Must not filter out property events if VUR is not enabled";
+    ASSERT_TRUE(clients.find(client2) == clients.end())
+            << "Must filter out property events if VUR is enabled and change is too small";
+    ASSERT_TRUE(abs(clients[client1][0].value.floatValues[0] - 1.01) < 0.0000001)
+            << "Expected property value == 1.01, instead got "
+            << clients[client1][0].value.floatValues[0];
+
+    clients = getManager()->getSubscribedClients({{
+            .prop = 0,
+            .areaId = 1,
+            .value = {.floatValues = {1.06}},
+            .timestamp = 3,
+    }});
+
+    ASSERT_TRUE(clients.find(client1) == clients.end())
+            << "Must not get property events for an areaId that the client hasn't subscribed to";
+    ASSERT_FALSE(clients.find(client2) == clients.end())
+            << "Must get property events significant changes";
+    ASSERT_TRUE(abs(clients[client2][0].value.floatValues[0] - 1.1) < 0.0000001)
+            << "Expected property value == 1.1, instead got "
+            << clients[client2][0].value.floatValues[0];
+}
+
 TEST_F(SubscriptionManagerTest, testSubscribe_enableVur_mustNotFilterStatusChange) {
     SpAIBinder binder1 = ndk::SharedRefBase::make<PropertyCallback>()->asBinder();
     std::shared_ptr<IVehicleCallback> client1 = IVehicleCallback::fromBinder(binder1);
diff --git a/automotive/vehicle/aidl_property/android/hardware/automotive/vehicle/VehicleProperty.aidl b/automotive/vehicle/aidl_property/android/hardware/automotive/vehicle/VehicleProperty.aidl
index 812b9b9..6f5c0c1 100644
--- a/automotive/vehicle/aidl_property/android/hardware/automotive/vehicle/VehicleProperty.aidl
+++ b/automotive/vehicle/aidl_property/android/hardware/automotive/vehicle/VehicleProperty.aidl
@@ -79,7 +79,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.STATIC
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:YEAR
+     * @unit VehicleUnit.YEAR
      * @version 2
      */
     INFO_MODEL_YEAR = 0x0103 + 0x10000000 + 0x01000000
@@ -89,7 +89,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.STATIC
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:MILLILITER
+     * @unit VehicleUnit.MILLILITER
      * @version 2
      */
     INFO_FUEL_CAPACITY = 0x0104 + 0x10000000 + 0x01000000
@@ -124,7 +124,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.STATIC
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:WH
+     * @unit VehicleUnit.WATT_HOUR
      * @version 2
      */
     INFO_EV_BATTERY_CAPACITY = 0x0106 + 0x10000000 + 0x01000000
@@ -184,7 +184,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.STATIC
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:MILLIMETER
+     * @unit VehicleUnit.MILLIMETER
      * @version 2
      */
     INFO_EXTERIOR_DIMENSIONS = 0x010B + 0x10000000 + 0x01000000
@@ -210,7 +210,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:KILOMETER
+     * @unit VehicleUnit.KILOMETER
      * @version 2
      */
     PERF_ODOMETER = 0x0204 + 0x10000000 + 0x01000000
@@ -226,7 +226,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:METER_PER_SEC
+     * @unit VehicleUnit.METER_PER_SEC
      * @version 2
      */
     PERF_VEHICLE_SPEED = 0x0207 + 0x10000000 + 0x01000000
@@ -239,7 +239,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:METER_PER_SEC
+     * @unit VehicleUnit.METER_PER_SEC
      * @version 2
      */
     PERF_VEHICLE_SPEED_DISPLAY = 0x0208 + 0x10000000 + 0x01000000
@@ -251,7 +251,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:DEGREES
+     * @unit VehicleUnit.DEGREES
      * @version 2
      */
     PERF_STEERING_ANGLE = 0x0209 + 0x10000000 + 0x01000000
@@ -263,7 +263,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:DEGREES
+     * @unit VehicleUnit.DEGREES
      * @version 2
      */
     PERF_REAR_STEERING_ANGLE = 0x0210 + 0x10000000 + 0x01000000
@@ -273,7 +273,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:CELSIUS
+     * @unit VehicleUnit.CELSIUS
      * @version 2
      */
     ENGINE_COOLANT_TEMP = 0x0301 + 0x10000000 + 0x01000000
@@ -293,7 +293,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:CELSIUS
+     * @unit VehicleUnit.CELSIUS
      * @version 2
      */
     ENGINE_OIL_TEMP = 0x0304 + 0x10000000 + 0x01000000
@@ -303,7 +303,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:RPM
+     * @unit VehicleUnit.RPM
      * @version 2
      */
     ENGINE_RPM = 0x0305 + 0x10000000 + 0x01000000
@@ -356,7 +356,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:MILLILITER
+     * @unit VehicleUnit.MILLILITER
      * @version 2
      */
     FUEL_LEVEL = 0x0307 + 0x10000000 + 0x01000000
@@ -383,7 +383,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:WH
+     * @unit VehicleUnit.WATT_HOUR
      * @version 2
      */
     EV_BATTERY_LEVEL = 0x0309 + 0x10000000 + 0x01000000
@@ -398,7 +398,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.ON_CHANGE
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:WH
+     * @unit VehicleUnit.WATT_HOUR
      * @version 2
      */
     EV_CURRENT_BATTERY_CAPACITY =
@@ -433,7 +433,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:MW
+     * @unit VehicleUnit.MILLIWATTS
      * @version 2
      */
     EV_BATTERY_INSTANTANEOUS_CHARGE_RATE = 0x030C + 0x10000000 + 0x01000000
@@ -452,7 +452,7 @@
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ_WRITE
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:METER
+     * @unit VehicleUnit.METER
      * @version 2
      */
     RANGE_REMAINING = 0x0308 + 0x10000000 + 0x01000000
@@ -466,7 +466,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:CELSIUS
+     * @unit VehicleUnit.CELSIUS
      * @version 3
      */
     EV_BATTERY_AVERAGE_TEMPERATURE =
@@ -494,7 +494,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:KILOPASCAL
+     * @unit VehicleUnit.KILOPASCAL
      * @version 2
      */
     TIRE_PRESSURE = 0x0309 + 0x10000000 + 0x07000000
@@ -510,7 +510,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.STATIC
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:KILOPASCAL
+     * @unit VehicleUnit.KILOPASCAL
      * @version 2
      */
     CRITICALLY_LOW_TIRE_PRESSURE = 0x030A + 0x10000000 + 0x07000000
@@ -862,7 +862,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.ON_CHANGE
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:CELSIUS
+     * @unit VehicleUnit.CELSIUS
      * @version 2
      */
     HVAC_TEMPERATURE_CURRENT = 0x0502 + 0x10000000 + 0x05000000
@@ -894,7 +894,7 @@
      * @change_mode VehiclePropertyChangeMode.ON_CHANGE
      * @access VehiclePropertyAccess.READ_WRITE
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:CELSIUS
+     * @unit VehicleUnit.CELSIUS
      * @version 2
      */
     HVAC_TEMPERATURE_SET = 0x0503 + 0x10000000 + 0x05000000
@@ -1130,7 +1130,7 @@
      *              configArray[1] = FAHRENHEIT
      *
      * This parameter MAY be used for displaying any HVAC temperature in the system.
-     * Values must be one of VehicleUnit::CELSIUS or VehicleUnit::FAHRENHEIT
+     * Values must be one of VehicleUnit.CELSIUS or VehicleUnit.FAHRENHEIT
      * Note that internally, all temperatures are represented in floating point Celsius.
      *
      * If updating HVAC_TEMPERATURE_DISPLAY_UNITS affects the values of other *_DISPLAY_UNITS
@@ -1287,7 +1287,7 @@
      *
      *      floatValues[0] = the requested value that an application wants to set a temperature to.
      *      floatValues[1] = the unit for floatValues[0]. It should be one of
-     *                       {VehicleUnit:CELSIUS, VehicleUnit:FAHRENHEIT}.
+     *                       {VehicleUnit.CELSIUS, VehicleUnit.FAHRENHEIT}.
      *      floatValues[2] = the value OEMs suggested in CELSIUS. This value is not included
      *                       in the request.
      *      floatValues[3] = the value OEMs suggested in FAHRENHEIT. This value is not included
@@ -1300,18 +1300,18 @@
      * For example, when a user uses the voice assistant to set HVAC temperature to 66.2 in
      * Fahrenheit.
      * First, an application will set this property with the value
-     * [66.2, (float)VehicleUnit:FAHRENHEIT,0,0].
+     * [66.2, (float)VehicleUnit.FAHRENHEIT,0,0].
      * If OEMs suggest to set 19.0 in Celsius or 66.5 in Fahrenheit for user's request, then VHAL
      * must generate a callback with property value
-     * [66.2, (float)VehicleUnit:FAHRENHEIT, 19.0, 66.5]. After the voice assistant gets the
+     * [66.2, (float)VehicleUnit.FAHRENHEIT, 19.0, 66.5]. After the voice assistant gets the
      * callback, it will inform the user and set HVAC temperature to the suggested value.
      *
      * Another example, an application receives 21 Celsius as the current temperature value by
      * querying HVC_TEMPERATURE_SET. But the application wants to know what value is displayed on
      * the car's UI in Fahrenheit.
-     * For this, the application sets the property to [21, (float)VehicleUnit:CELSIUS, 0, 0]. If
+     * For this, the application sets the property to [21, (float)VehicleUnit.CELSIUS, 0, 0]. If
      * the suggested value by the OEM for 21 Celsius is 70 Fahrenheit, then VHAL must generate a
-     * callback with property value [21, (float)VehicleUnit:CELSIUS, 21.0, 70.0].
+     * callback with property value [21, (float)VehicleUnit.CELSIUS, 21.0, 70.0].
      * In this case, the application can know that the value is 70.0 Fahrenheit in the car’s UI.
      *
      * @change_mode VehiclePropertyChangeMode.ON_CHANGE
@@ -1504,7 +1504,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.ON_CHANGE
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:MILLI_SECS
+     * @unit VehicleUnit.MILLI_SECS
      * @version 2
      */
     EXTERNAL_CAR_TIME = 0x0608 + 0x10000000 // VehiclePropertyGroup:SYSTEM
@@ -1534,7 +1534,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.ON_CHANGE
      * @access VehiclePropertyAccess.WRITE
-     * @unit VehicleUnit:MILLI_SECS
+     * @unit VehicleUnit.MILLI_SECS
      * @version 2
      */
     ANDROID_EPOCH_TIME = 0x0606 + 0x10000000 + 0x01000000
@@ -1559,7 +1559,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:CELSIUS
+     * @unit VehicleUnit.CELSIUS
      * @version 2
      */
     ENV_OUTSIDE_TEMPERATURE = 0x0703 + 0x10000000 + 0x01000000
@@ -3212,7 +3212,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.ON_CHANGE
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:MILLI_SECS
+     * @unit VehicleUnit.MILLI_SECS
      * @version 2
      */
     WINDSHIELD_WIPERS_PERIOD =
@@ -4789,7 +4789,7 @@
      * @change_mode VehiclePropertyChangeMode.ON_CHANGE
      * @access VehiclePropertyAccess.READ_WRITE
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:AMPERE
+     * @unit VehicleUnit.AMPERE
      * @version 2
      */
     EV_CHARGE_CURRENT_DRAW_LIMIT = 0x0F3F + 0x10000000 + 0x01000000
@@ -4854,7 +4854,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:SECS
+     * @unit VehicleUnit.SECS
      * @version 2
      */
     EV_CHARGE_TIME_REMAINING = 0x0F43 + 0x10000000 + 0x01000000
@@ -4888,7 +4888,7 @@
             + 0x00400000, // VehiclePropertyGroup:SYSTEM,VehicleArea:GLOBAL,VehiclePropertyType:INT32
 
     /**
-     * Vehicle’s curb weight
+     * Vehicle’s curb weight in kilograms.
      *
      * Returns the vehicle's curb weight in kilograms. Curb weight is
      * the total weight of the vehicle with standard equipment and all
@@ -4905,10 +4905,8 @@
      *
      * @change_mode VehiclePropertyChangeMode.STATIC
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:KILOGRAM
      * @version 2
      */
-
     VEHICLE_CURB_WEIGHT = 0x0F46 + 0x10000000 + 0x01000000
             + 0x00400000, // VehiclePropertyGroup:SYSTEM,VehicleArea:GLOBAL,VehiclePropertyType:INT32
 
@@ -5567,7 +5565,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.ON_CHANGE
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:METER_PER_SEC
+     * @unit VehicleUnit.METER_PER_SEC
      * @version 2
      */
     CRUISE_CONTROL_TARGET_SPEED =
@@ -5599,7 +5597,7 @@
      * @change_mode VehiclePropertyChangeMode.ON_CHANGE
      * @access VehiclePropertyAccess.READ_WRITE
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:MILLI_SECS
+     * @unit VehicleUnit.MILLI_SECS
      * @version 2
      */
     ADAPTIVE_CRUISE_CONTROL_TARGET_TIME_GAP =
@@ -5630,7 +5628,7 @@
      *
      * @change_mode VehiclePropertyChangeMode.CONTINUOUS
      * @access VehiclePropertyAccess.READ
-     * @unit VehicleUnit:MILLIMETER
+     * @unit VehicleUnit.MILLIMETER
      * @version 2
      */
     ADAPTIVE_CRUISE_CONTROL_LEAD_VEHICLE_MEASURED_DISTANCE =
diff --git a/automotive/vehicle/tools/generate_annotation_enums.py b/automotive/vehicle/tools/generate_annotation_enums.py
index 93d408e..f279767 100755
--- a/automotive/vehicle/tools/generate_annotation_enums.py
+++ b/automotive/vehicle/tools/generate_annotation_enums.py
@@ -18,7 +18,8 @@
 
    Need ANDROID_BUILD_TOP environmental variable to be set. This script will update
    ChangeModeForVehicleProperty.h and AccessForVehicleProperty.h under generated_lib/cpp and
-   ChangeModeForVehicleProperty.java, AccessForVehicleProperty.java, EnumForVehicleProperty.java under generated_lib/java.
+   ChangeModeForVehicleProperty.java, AccessForVehicleProperty.java, EnumForVehicleProperty.java
+   UnitsForVehicleProperty.java under generated_lib/java.
 
    Usage:
    $ python generate_annotation_enums.py
@@ -42,6 +43,8 @@
     'AccessForVehicleProperty.java')
 ENUM_JAVA_FILE_PATH = ('hardware/interfaces/automotive/vehicle/aidl/generated_lib/java/' +
                          'EnumForVehicleProperty.java')
+UNITS_JAVA_FILE_PATH = ('hardware/interfaces/automotive/vehicle/aidl/generated_lib/java/' +
+                       'UnitsForVehicleProperty.java')
 VERSION_CPP_FILE_PATH = ('hardware/interfaces/automotive/vehicle/aidl/generated_lib/cpp/' +
     'VersionForVehicleProperty.h')
 SCRIPT_PATH = 'hardware/interfaces/automotive/vehicle/tools/generate_annotation_enums.py'
@@ -175,6 +178,15 @@
     public static final Map<Integer, List<Class<?>>> values = Map.ofEntries(
 """
 
+UNITS_JAVA_HEADER = """package android.hardware.automotive.vehicle;
+
+import java.util.Map;
+
+public final class UnitsForVehicleProperty {
+
+    public static final Map<Integer, Integer> values = Map.ofEntries(
+"""
+
 
 class PropertyConfig:
     """Represents one VHAL property definition in VehicleProperty.aidl."""
@@ -316,6 +328,12 @@
                     continue;
                 if not cpp:
                     annotation = "List.of(" + ', '.join([class_name + ".class" for class_name in config.enum_types]) + ")"
+            elif field == 'unit_type':
+                if not config.unit_type:
+                    continue
+                if not cpp:
+                    annotation = config.unit_type
+
             elif field == 'version':
                 if cpp:
                     annotation = config.version
@@ -499,6 +517,12 @@
     enum_types.setJavaFooter(JAVA_FOOTER)
     generated_files.append(enum_types)
 
+    unit_type = GeneratedFile('unit_type')
+    unit_type.setJavaFilePath(os.path.join(android_top, UNITS_JAVA_FILE_PATH))
+    unit_type.setJavaHeader(UNITS_JAVA_HEADER)
+    unit_type.setJavaFooter(JAVA_FOOTER)
+    generated_files.append(unit_type)
+
     version = GeneratedFile('version')
     version.setCppFilePath(os.path.join(android_top, VERSION_CPP_FILE_PATH))
     version.setCppHeader(VERSION_CPP_HEADER)
diff --git a/gnss/1.1/vts/functional/gnss_hal_test.cpp b/gnss/1.1/vts/functional/gnss_hal_test.cpp
index 6663a19..5ec9806 100644
--- a/gnss/1.1/vts/functional/gnss_hal_test.cpp
+++ b/gnss/1.1/vts/functional/gnss_hal_test.cpp
@@ -168,8 +168,7 @@
     manager->listManifestByInterface(
             "android.hardware.gnss@1.1::IGnss",
             [&hasGnssHalVersion_1_1](const hidl_vec<hidl_string>& registered) {
-                ASSERT_EQ(1, registered.size());
-                hasGnssHalVersion_1_1 = true;
+                hasGnssHalVersion_1_1 = registered.size() != 0;
             });
 
     bool hasGnssHalVersion_2_0 = false;