Merge "Introduce internal APIs between CarTelemetryService and ScriptExecutor service." into sc-dev
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/config.xml b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
index 52739bf..f675d4e 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
@@ -146,4 +146,7 @@
     <!-- The name of the package that will hold the system cluster service role. -->
     <string name="config_systemAutomotiveCluster" translatable="false">android.car.cluster</string>
 
+    <!-- Whether this device is supporting the microphone toggle -->
+    <bool name="config_supportsMicToggle">true</bool>
+
 </resources>
diff --git a/cpp/telemetry/ARCHITECTURE.md b/cpp/telemetry/ARCHITECTURE.md
index ae60c6b..75dd194 100644
--- a/cpp/telemetry/ARCHITECTURE.md
+++ b/cpp/telemetry/ARCHITECTURE.md
@@ -10,9 +10,13 @@
 
 ## Structure
 
-- aidl/            - AIDL declerations
-- products/        - AAOS Telemetry product, it's included in car_base.mk
-- sepolicy         - SELinux policies
-- src/             - Source code
-- *.rc             - rc file to start services
-- *.xml            - VINTF manifest (TODO: needed?)
+```
+aidl/                    - Internal AIDL declerations, for public AIDLs, please see
+                           //frameworks/hardware/interfaces/automotive/telemetry
+products/                - AAOS Telemetry product, it's included in car_base.mk
+sepolicy                 - SELinux policies
+src/                     - Source code
+   TelemetryServer.h     - The main class.
+*.rc                     - rc file to start services
+*.xml                    - VINTF manifest (TODO: needed?)
+```
diff --git a/cpp/telemetry/Android.bp b/cpp/telemetry/Android.bp
index 45e1358..4e6c1c0 100644
--- a/cpp/telemetry/Android.bp
+++ b/cpp/telemetry/Android.bp
@@ -24,10 +24,11 @@
         "-Wno-unused-parameter",
     ],
     shared_libs: [
-        "android.frameworks.automotive.telemetry-V1-cpp",
+        "android.automotive.telemetry.internal-ndk_platform",
+        "android.frameworks.automotive.telemetry-V1-ndk_platform",
         "libbase",
+        "libbinder_ndk",
         "liblog",
-        "libbinder",
         "libutils",
     ],
     product_variables: {
@@ -46,9 +47,11 @@
     ],
     srcs: [
         "src/CarTelemetryImpl.cpp",
+        "src/CarTelemetryInternalImpl.cpp",
         "src/RingBuffer.cpp",
+        "src/TelemetryServer.cpp",
     ],
-    // Allow dependencies to use header files.
+    // Allow dependents to use the header files.
     export_include_dirs: [
         "src",
     ],
@@ -62,12 +65,14 @@
     test_suites: ["general-tests"],
     srcs: [
         "tests/CarTelemetryImplTest.cpp",
+        "tests/CarTelemetryInternalImplTest.cpp",
         "tests/RingBufferTest.cpp",
     ],
     // Statically link only in tests, for portability reason.
     static_libs: [
         "android.automotive.telemetryd@1.0-impl",
-        "android.frameworks.automotive.telemetry-V1-cpp",
+        "android.automotive.telemetry.internal-ndk_platform",
+        "android.frameworks.automotive.telemetry-V1-ndk_platform",
         "libgmock",
         "libgtest",
     ],
diff --git a/cpp/telemetry/README.md b/cpp/telemetry/README.md
index e1f277b..a13116d 100644
--- a/cpp/telemetry/README.md
+++ b/cpp/telemetry/README.md
@@ -1,3 +1,19 @@
 # Automotive Telemetry Service
 
 A structured log collection service for CarTelemetryService. See ARCHITECTURE.md to learn internals.
+
+## Useful Commands
+
+**Dump service information**
+
+`adb shell dumpsys android.automotive.telemetry.internal.ICarTelemetryInternal/default`
+
+**Starting emulator**
+
+`aae emulator run -selinux permissive -writable-system`
+
+**Running tests**
+
+`atest cartelemetryd_impl_test:CarTelemetryInternalImplTest#TestSetListenerReturnsOk`
+
+`atest cartelemetryd_impl_test`
diff --git a/cpp/telemetry/aidl/Android.bp b/cpp/telemetry/aidl/Android.bp
new file mode 100644
index 0000000..b4cd9c7
--- /dev/null
+++ b/cpp/telemetry/aidl/Android.bp
@@ -0,0 +1,35 @@
+// 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.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aidl_interface {
+    name: "android.automotive.telemetry.internal",
+    unstable: true,
+    vendor_available: false,
+    srcs: [
+        "android/automotive/telemetry/internal/*.aidl",
+    ],
+    backend: {
+        ndk: {
+            enabled: true,
+        },
+        java: {
+            platform_apis: true,
+            enabled: true,
+        },
+    }
+}
diff --git a/cpp/telemetry/aidl/android/automotive/telemetry/internal/CarDataInternal.aidl b/cpp/telemetry/aidl/android/automotive/telemetry/internal/CarDataInternal.aidl
new file mode 100644
index 0000000..bf31169
--- /dev/null
+++ b/cpp/telemetry/aidl/android/automotive/telemetry/internal/CarDataInternal.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+package android.automotive.telemetry.internal;
+
+/**
+ * Wrapper for {@code android.frameworks.automotive.telemetry.CarData}.
+ */
+parcelable CarDataInternal {
+  /**
+   * Must be a valid id. Scripts subscribe to data using this id.
+   */
+  int id;
+
+  /**
+   * Content corresponding to the schema defined by the id.
+   */
+  byte[] content;
+}
diff --git a/cpp/telemetry/aidl/android/automotive/telemetry/internal/ICarDataListener.aidl b/cpp/telemetry/aidl/android/automotive/telemetry/internal/ICarDataListener.aidl
new file mode 100644
index 0000000..48ab6f9
--- /dev/null
+++ b/cpp/telemetry/aidl/android/automotive/telemetry/internal/ICarDataListener.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+package android.automotive.telemetry.internal;
+
+import android.automotive.telemetry.internal.CarDataInternal;
+
+/**
+ * Listener for {@code ICarTelemetryInternal#registerListener}.
+ */
+oneway interface ICarDataListener {
+  /**
+   * Called by ICarTelemetry when the data are available to be consumed. ICarTelemetry removes
+   * the delivered data when the callback succeeds.
+   *
+   * <p>If the collected data is too large, it will send only chunk of the data, and the callback
+   * will be fired again.
+   *
+   * @param dataList the pushed data.
+   */
+  void onCarDataReceived(in CarDataInternal[] dataList);
+}
diff --git a/cpp/telemetry/aidl/android/automotive/telemetry/internal/ICarTelemetryInternal.aidl b/cpp/telemetry/aidl/android/automotive/telemetry/internal/ICarTelemetryInternal.aidl
new file mode 100644
index 0000000..b8938ff
--- /dev/null
+++ b/cpp/telemetry/aidl/android/automotive/telemetry/internal/ICarTelemetryInternal.aidl
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+package android.automotive.telemetry.internal;
+
+import android.automotive.telemetry.internal.ICarDataListener;
+
+/**
+ * An internal API provided by cartelemetryd for receiving the collected data.
+ */
+interface ICarTelemetryInternal {
+  /**
+   * Sets a listener for CarData. If there are existing CarData in the buffer, the daemon will
+   * start pushing them to the listener. There can be only a single registered listener at a time.
+   *
+   * @param listener the only listener.
+   * @throws IllegalStateException if someone is already registered or the listener is dead.
+   */
+  void setListener(in ICarDataListener listener);
+
+  /**
+   * Clears the listener if exists. Silently ignores if there is no listener.
+   */
+  void clearListener();
+}
diff --git a/cpp/telemetry/sampleclient/README.md b/cpp/telemetry/sampleclient/README.md
index 8e8f3cc..ebbaf16 100644
--- a/cpp/telemetry/sampleclient/README.md
+++ b/cpp/telemetry/sampleclient/README.md
@@ -2,11 +2,27 @@
 
 This is a sample vendor service that sends `CarData` to car telemetry service.
 
+## Running
+
+**1. Quick mode - under root**
+
+```
+m -j android.automotive.telemetryd-sampleclient
+
+adb remount  # make sure run "adb disable-verity" before remounting
+adb push $ANDROID_PRODUCT_OUT/vendor/bin/android.automotive.telemetryd-sampleclient /system/bin/
+
+adb shell /system/bin/android.automotive.telemetryd-sampleclient
+
+# Then check logcat and dumpsys to verify the results.
+```
+
+**2. Under vendor**
+
 To include it in the final image, add
 `PRODUCT_PACKAGES += android.automotive.telemetryd-sampleclient` to
 `//packages/services/Car/cpp/telemetry/products/telemetry.mk` (or other suitable mk file).
 
-Example:
 ```
 # this goes to products/telemetry.mk
 
diff --git a/cpp/telemetry/sampleinternalclient/Android.bp b/cpp/telemetry/sampleinternalclient/Android.bp
new file mode 100644
index 0000000..d04ac09
--- /dev/null
+++ b/cpp/telemetry/sampleinternalclient/Android.bp
@@ -0,0 +1,36 @@
+// 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.
+
+// Sample client for ICarTelemetry service.
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_binary {
+    name: "android.automotive.telemetryd-sampleinternalclient",
+    srcs: [
+        "main.cpp",
+    ],
+    cflags: [
+        "-Werror",
+        "-Wall",
+        "-Wno-unused-parameter",
+    ],
+    shared_libs: [
+        "android.automotive.telemetry.internal-ndk_platform",
+        "libbase",
+        "libbinder_ndk",
+        "libutils",
+    ],
+}
diff --git a/cpp/telemetry/sampleinternalclient/main.cpp b/cpp/telemetry/sampleinternalclient/main.cpp
new file mode 100644
index 0000000..06ca549
--- /dev/null
+++ b/cpp/telemetry/sampleinternalclient/main.cpp
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+// This is a sample reader client for ICarTelemetryInternal.
+// TODO(b/186017953): remove this client when CarTelemetryService is implemented.
+//
+// adb remount  # make sure run "adb disable-verity" before remounting
+// adb push $ANDROID_PRODUCT_OUT/system/bin/android.automotive.telemetryd-sampleinternalclient
+// /system/bin/
+//
+// adb shell /system/bin/android.automotive.telemetryd-sampleinternalclient
+
+#define LOG_TAG "cartelemetryd_sampleint"
+
+#include <aidl/android/automotive/telemetry/internal/BnCarDataListener.h>
+#include <aidl/android/automotive/telemetry/internal/CarDataInternal.h>
+#include <aidl/android/automotive/telemetry/internal/ICarTelemetryInternal.h>
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
+
+using ::aidl::android::automotive::telemetry::internal::BnCarDataListener;
+using ::aidl::android::automotive::telemetry::internal::CarDataInternal;
+using ::aidl::android::automotive::telemetry::internal::ICarDataListener;
+using ::aidl::android::automotive::telemetry::internal::ICarTelemetryInternal;
+using ::android::base::StringPrintf;
+
+class CarDataListenerImpl : public BnCarDataListener {
+public:
+    ::ndk::ScopedAStatus onCarDataReceived(
+            const std::vector<CarDataInternal>& in_dataList) override;
+};
+
+::ndk::ScopedAStatus CarDataListenerImpl::onCarDataReceived(
+        const std::vector<CarDataInternal>& dataList) {
+    LOG(INFO) << "Received data size = " << dataList.size();
+    for (const auto data : dataList) {
+        LOG(INFO) << "data.id = " << data.id;
+    }
+    return ::ndk::ScopedAStatus::ok();
+}
+
+int main(int argc, char* argv[]) {
+    // The name of the service is described in
+    // https://source.android.com/devices/architecture/aidl/aidl-hals#instance-names
+    const std::string instance = StringPrintf("%s/default", ICarTelemetryInternal::descriptor);
+    LOG(INFO) << "Obtaining: " << instance;
+    std::shared_ptr<ICarTelemetryInternal> service = ICarTelemetryInternal::fromBinder(
+            ndk::SpAIBinder(AServiceManager_getService(instance.c_str())));
+    if (!service) {
+        LOG(FATAL) << "ICarTelemetryInternal service not found, may be still initializing?";
+    }
+
+    LOG(INFO) << "Setting the listener";
+    std::shared_ptr<CarDataListenerImpl> listener = ndk::SharedRefBase::make<CarDataListenerImpl>();
+    auto status = service->setListener(listener);
+    if (!status.isOk()) {
+        LOG(FATAL) << "Failed to set the listener";
+    }
+
+    ::ABinderProcess_startThreadPool();
+    ::ABinderProcess_joinThreadPool();
+    return 1;  // not reachable
+}
diff --git a/cpp/telemetry/sepolicy/private/service_contexts b/cpp/telemetry/sepolicy/private/service_contexts
index 24d7df3..f26b5f8 100644
--- a/cpp/telemetry/sepolicy/private/service_contexts
+++ b/cpp/telemetry/sepolicy/private/service_contexts
@@ -1 +1,2 @@
 android.frameworks.automotive.telemetry.ICarTelemetry/default  u:object_r:cartelemetryd_service:s0
+android.automotive.telemetry.internal.ICarTelemetryInternal/default  u:object_r:cartelemetryd_service:s0
diff --git a/cpp/telemetry/src/BufferedCarData.h b/cpp/telemetry/src/BufferedCarData.h
index a15a25f..0c6ff4d 100644
--- a/cpp/telemetry/src/BufferedCarData.h
+++ b/cpp/telemetry/src/BufferedCarData.h
@@ -17,26 +17,25 @@
 #ifndef CPP_TELEMETRY_SRC_BUFFEREDCARDATA_H_
 #define CPP_TELEMETRY_SRC_BUFFEREDCARDATA_H_
 
-#include <android/frameworks/automotive/telemetry/CarData.h>
+#include <stdint.h>
+
+#include <tuple>
+#include <vector>
 
 namespace android {
 namespace automotive {
 namespace telemetry {
 
+// Internally stored `CarData` with some extras.
 struct BufferedCarData {
-    BufferedCarData(const android::frameworks::automotive::telemetry::CarData& data, int32_t uid) :
-          mId(data.id),
-          mContent(std::move(data.content)),
-          mLogUid(uid) {}
-
-    // Visible for testing.
-    BufferedCarData(int32_t id, const std::vector<uint8_t>& content, int32_t uid) :
-          mId(id),
-          mContent(std::move(content)),
-          mLogUid(uid) {}
+    BufferedCarData(BufferedCarData&& other) = default;
+    BufferedCarData(const BufferedCarData&) = default;
+    BufferedCarData& operator=(BufferedCarData&& other) = default;
+    BufferedCarData& operator=(const BufferedCarData&) = default;
 
     inline bool operator==(const BufferedCarData& rhs) const {
-        return std::tie(mId, mContent, mLogUid) == std::tie(rhs.mId, rhs.mContent, rhs.mLogUid);
+        return std::tie(mId, mContent, mPublisherUid) ==
+                std::tie(rhs.mId, rhs.mContent, rhs.mPublisherUid);
     }
 
     // Returns the size of the stored data. Note that it's not the exact size of the struct.
@@ -45,8 +44,8 @@
     const int32_t mId;
     const std::vector<uint8_t> mContent;
 
-    // The uid of the logging client (defaults to -1).
-    const int32_t mLogUid;
+    // The uid of the logging client.
+    const uid_t mPublisherUid;
 };
 
 }  // namespace telemetry
diff --git a/cpp/telemetry/src/CarTelemetryImpl.cpp b/cpp/telemetry/src/CarTelemetryImpl.cpp
index bbf7ac5..34a4e60 100644
--- a/cpp/telemetry/src/CarTelemetryImpl.cpp
+++ b/cpp/telemetry/src/CarTelemetryImpl.cpp
@@ -18,9 +18,8 @@
 
 #include "BufferedCarData.h"
 
-#include <android-base/logging.h>
-#include <android/frameworks/automotive/telemetry/CarData.h>
-#include <binder/IPCThreadState.h>
+#include <aidl/android/frameworks/automotive/telemetry/CarData.h>
+#include <android/binder_ibinder.h>
 
 #include <stdio.h>
 
@@ -30,27 +29,21 @@
 namespace automotive {
 namespace telemetry {
 
-using ::android::binder::Status;
-using ::android::frameworks::automotive::telemetry::CarData;
+using ::aidl::android::frameworks::automotive::telemetry::CarData;
 
 CarTelemetryImpl::CarTelemetryImpl(RingBuffer* buffer) : mRingBuffer(buffer) {}
 
 // TODO(b/174608802): Add 10kb size check for the `dataList`, see the AIDL for the limits
-Status CarTelemetryImpl::write(const std::vector<CarData>& dataList) {
-    uid_t uid = IPCThreadState::self()->getCallingUid();
-    // NOTE: CarData here will be coped to BufferedCarData, as we don't know what Binder will do
-    //       with the current allocated CarData.
-    for (auto& carData : dataList) {
-        mRingBuffer->push(BufferedCarData(carData, uid));
+ndk::ScopedAStatus CarTelemetryImpl::write(const std::vector<CarData>& dataList) {
+    uid_t publisherUid = ::AIBinder_getCallingUid();
+    for (auto&& data : dataList) {
+        mRingBuffer->push({.mId = data.id,
+                           .mContent = std::move(data.content),
+                           .mPublisherUid = publisherUid});
     }
-    return Status::ok();
+    return ndk::ScopedAStatus::ok();
 }
 
-status_t CarTelemetryImpl::dump(int fd, const android::Vector<android::String16>& args) {
-    dprintf(fd, "CarTelemetryImpl:\n");
-    mRingBuffer->dump(fd, /* indent= */ 2);
-    return android::OK;
-}
 }  // namespace telemetry
 }  // namespace automotive
 }  // namespace android
diff --git a/cpp/telemetry/src/CarTelemetryImpl.h b/cpp/telemetry/src/CarTelemetryImpl.h
index 173a472..a3bb6c1 100644
--- a/cpp/telemetry/src/CarTelemetryImpl.h
+++ b/cpp/telemetry/src/CarTelemetryImpl.h
@@ -17,14 +17,13 @@
 #ifndef CPP_TELEMETRY_SRC_CARTELEMETRYIMPL_H_
 #define CPP_TELEMETRY_SRC_CARTELEMETRYIMPL_H_
 
-#include <android/frameworks/automotive/telemetry/BnCarTelemetry.h>
-#include <android/frameworks/automotive/telemetry/CarData.h>
+#include "RingBuffer.h"
+
+#include <aidl/android/frameworks/automotive/telemetry/BnCarTelemetry.h>
+#include <aidl/android/frameworks/automotive/telemetry/CarData.h>
 #include <utils/String16.h>
 #include <utils/Vector.h>
 
-#include <RingBuffer.h>
-
-#include <memory>
 #include <vector>
 
 namespace android {
@@ -32,17 +31,15 @@
 namespace telemetry {
 
 // Implementation of android.frameworks.automotive.telemetry.ICarTelemetry.
-class CarTelemetryImpl : public android::frameworks::automotive::telemetry::BnCarTelemetry {
+class CarTelemetryImpl : public aidl::android::frameworks::automotive::telemetry::BnCarTelemetry {
 public:
     // Doesn't own `buffer`.
     explicit CarTelemetryImpl(RingBuffer* buffer);
 
-    android::binder::Status write(
-            const std::vector<android::frameworks::automotive::telemetry::CarData>& dataList)
+    ndk::ScopedAStatus write(
+            const std::vector<aidl::android::frameworks::automotive::telemetry::CarData>& dataList)
             override;
 
-    status_t dump(int fd, const android::Vector<android::String16>& args) override;
-
 private:
     RingBuffer* mRingBuffer;  // not owned
 };
diff --git a/cpp/telemetry/src/CarTelemetryInternalImpl.cpp b/cpp/telemetry/src/CarTelemetryInternalImpl.cpp
new file mode 100644
index 0000000..7a3b141
--- /dev/null
+++ b/cpp/telemetry/src/CarTelemetryInternalImpl.cpp
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+
+#include "CarTelemetryInternalImpl.h"
+
+#include <aidl/android/automotive/telemetry/internal/BnCarDataListener.h>
+#include <aidl/android/automotive/telemetry/internal/CarDataInternal.h>
+#include <aidl/android/automotive/telemetry/internal/ICarDataListener.h>
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+
+namespace android {
+namespace automotive {
+namespace telemetry {
+
+using ::aidl::android::automotive::telemetry::internal::BnCarDataListener;
+using ::aidl::android::automotive::telemetry::internal::CarDataInternal;
+using ::aidl::android::automotive::telemetry::internal::ICarDataListener;
+using ::android::base::StringPrintf;
+
+CarTelemetryInternalImpl::CarTelemetryInternalImpl(RingBuffer* buffer) :
+      mRingBuffer(buffer),
+      mBinderDeathRecipient(
+              ::AIBinder_DeathRecipient_new(CarTelemetryInternalImpl::listenerBinderDied)) {}
+
+ndk::ScopedAStatus CarTelemetryInternalImpl::setListener(
+        const std::shared_ptr<ICarDataListener>& listener) {
+    const std::scoped_lock<std::mutex> lock(mMutex);
+
+    if (mCarDataListener != nullptr) {
+        return ndk::ScopedAStatus::fromExceptionCodeWithMessage(::EX_ILLEGAL_STATE,
+                                                                "ICarDataListener is already set.");
+    }
+
+    // If passed a local binder, AIBinder_linkToDeath will do nothing and return
+    // STATUS_INVALID_OPERATION. We ignore this case because we only use local binders in tests
+    // where this is not an error.
+    if (listener->isRemote()) {
+        auto status = ndk::ScopedAStatus::fromStatus(
+                ::AIBinder_linkToDeath(listener->asBinder().get(), mBinderDeathRecipient.get(),
+                                       this));
+        if (!status.isOk()) {
+            return ndk::ScopedAStatus::fromExceptionCodeWithMessage(::EX_ILLEGAL_STATE,
+                                                                    status.getMessage());
+        }
+    }
+
+    mCarDataListener = listener;
+    return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus CarTelemetryInternalImpl::clearListener() {
+    const std::scoped_lock<std::mutex> lock(mMutex);
+    if (mCarDataListener == nullptr) {
+        LOG(INFO) << __func__ << ": No ICarDataListener, ignoring the call";
+        return ndk::ScopedAStatus::ok();
+    }
+    auto status = ndk::ScopedAStatus::fromStatus(
+            ::AIBinder_unlinkToDeath(mCarDataListener->asBinder().get(),
+                                     mBinderDeathRecipient.get(), this));
+    if (!status.isOk()) {
+        LOG(WARNING) << __func__
+                     << ": unlinkToDeath failed, continuing anyway: " << status.getMessage();
+    }
+    mCarDataListener = nullptr;
+    return ndk::ScopedAStatus::ok();
+}
+
+binder_status_t CarTelemetryInternalImpl::dump(int fd, const char** args, uint32_t numArgs) {
+    dprintf(fd, "ICarTelemetryInternal:\n");
+    mRingBuffer->dump(fd);
+    return ::STATUS_OK;
+}
+
+// Removes the listener if its binder dies.
+void CarTelemetryInternalImpl::listenerBinderDiedImpl() {
+    LOG(WARNING) << "A ICarDataListener died, removing the listener.";
+    const std::scoped_lock<std::mutex> lock(mMutex);
+    mCarDataListener = nullptr;
+}
+
+void CarTelemetryInternalImpl::listenerBinderDied(void* cookie) {
+    auto thiz = static_cast<CarTelemetryInternalImpl*>(cookie);
+    thiz->listenerBinderDiedImpl();
+}
+
+}  // namespace telemetry
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/telemetry/src/CarTelemetryInternalImpl.h b/cpp/telemetry/src/CarTelemetryInternalImpl.h
new file mode 100644
index 0000000..12ad5cd
--- /dev/null
+++ b/cpp/telemetry/src/CarTelemetryInternalImpl.h
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+#ifndef CPP_TELEMETRY_SRC_CARTELEMETRYINTERNALIMPL_H_
+#define CPP_TELEMETRY_SRC_CARTELEMETRYINTERNALIMPL_H_
+
+#include "RingBuffer.h"
+
+#include <aidl/android/automotive/telemetry/internal/BnCarTelemetryInternal.h>
+#include <aidl/android/automotive/telemetry/internal/CarDataInternal.h>
+#include <aidl/android/automotive/telemetry/internal/ICarDataListener.h>
+#include <android/binder_status.h>
+#include <utils/Mutex.h>
+#include <utils/String16.h>
+#include <utils/Vector.h>
+
+namespace android {
+namespace automotive {
+namespace telemetry {
+
+// Implementation of android.automotive.telemetry.ICarTelemetryInternal.
+class CarTelemetryInternalImpl :
+      public aidl::android::automotive::telemetry::internal::BnCarTelemetryInternal {
+public:
+    // Doesn't own `buffer`.
+    explicit CarTelemetryInternalImpl(RingBuffer* buffer);
+
+    ndk::ScopedAStatus setListener(
+            const std::shared_ptr<aidl::android::automotive::telemetry::internal::ICarDataListener>&
+                    listener) override;
+
+    ndk::ScopedAStatus clearListener() override;
+
+    binder_status_t dump(int fd, const char** args, uint32_t numArgs) override;
+
+private:
+    // Death recipient callback that is called when ICarDataListener dies.
+    // The cookie is a pointer to a CarTelemetryInternalImpl object.
+    static void listenerBinderDied(void* cookie);
+
+    void listenerBinderDiedImpl();
+
+    RingBuffer* mRingBuffer;  // not owned
+    ndk::ScopedAIBinder_DeathRecipient mBinderDeathRecipient;
+    std::mutex mMutex;  // a mutex for the whole instance
+
+    std::shared_ptr<aidl::android::automotive::telemetry::internal::ICarDataListener>
+            mCarDataListener GUARDED_BY(mMutex);
+};
+
+}  // namespace telemetry
+}  // namespace automotive
+}  // namespace android
+
+#endif  // CPP_TELEMETRY_SRC_CARTELEMETRYINTERNALIMPL_H_
diff --git a/cpp/telemetry/src/RingBuffer.cpp b/cpp/telemetry/src/RingBuffer.cpp
index 658d9d3..36de3f8 100644
--- a/cpp/telemetry/src/RingBuffer.cpp
+++ b/cpp/telemetry/src/RingBuffer.cpp
@@ -17,7 +17,6 @@
 #include "RingBuffer.h"
 
 #include <android-base/logging.h>
-#include <android/frameworks/automotive/telemetry/CarData.h>
 
 #include <inttypes.h>  // for PRIu64 and friends
 
@@ -27,53 +26,35 @@
 namespace automotive {
 namespace telemetry {
 
-// Do now allow buffering more than this amount of data. It's to make sure we won't get
-// 200 thousands of small CarData.
-const int kMaxNumberOfItems = 5000;
-
-RingBuffer::RingBuffer(int32_t limit) : mSizeLimitBytes(limit) {}
+RingBuffer::RingBuffer(int32_t limit) : mSizeLimit(limit) {}
 
 void RingBuffer::push(BufferedCarData&& data) {
-    int32_t dataSizeBytes = data.contentSizeInBytes();
-    if (dataSizeBytes > mSizeLimitBytes) {
-        LOG(WARNING) << "CarData(id=" << data.mId << ") size (" << dataSizeBytes
-                     << "b) is larger than " << mSizeLimitBytes << "b, dropping it.";
-        return;
-    }
-    mCurrentSizeBytes += dataSizeBytes;
+    const std::scoped_lock<std::mutex> lock(mMutex);
     mList.push_back(std::move(data));
-    while (mCurrentSizeBytes > mSizeLimitBytes || mList.size() > kMaxNumberOfItems) {
-        mCurrentSizeBytes -= mList.front().contentSizeInBytes();
+    while (mList.size() > mSizeLimit) {
         mList.pop_front();
         mTotalDroppedDataCount += 1;
     }
 }
 
-std::vector<BufferedCarData> RingBuffer::popAllDataForId(int32_t id) {
-    LOG(VERBOSE) << "popAllDataForId id=" << id;
-    std::vector<BufferedCarData> result;
-    for (auto it = mList.begin(); it != mList.end();) {
-        if (it->mId == id) {
-            mCurrentSizeBytes -= (*it).contentSizeInBytes();
-            result.push_back(std::move(*it));
-            it = mList.erase(it);
-        } else {
-            ++it;
-        }
-    }
+BufferedCarData RingBuffer::popFront() {
+    const std::scoped_lock<std::mutex> lock(mMutex);
+    auto result = std::move(mList.front());
+    mList.pop_front();
     return result;
 }
 
-void RingBuffer::dump(int fd, int indent) const {
-    dprintf(fd, "%*sRingBuffer:\n", indent, "");
-    dprintf(fd, "%*s  mSizeLimitBytes=%d\n", indent, "", mSizeLimitBytes);
-    dprintf(fd, "%*s  mCurrentSizeBytes=%d\n", indent, "", mCurrentSizeBytes);
-    dprintf(fd, "%*s  mList.size=%zu\n", indent, "", mList.size());
-    dprintf(fd, "%*s  mTotalDroppedDataCount=%" PRIu64 "\n", indent, "", mTotalDroppedDataCount);
+void RingBuffer::dump(int fd) const {
+    const std::scoped_lock<std::mutex> lock(mMutex);
+    dprintf(fd, "RingBuffer:\n");
+    dprintf(fd, "  mSizeLimit=%d\n", mSizeLimit);
+    dprintf(fd, "  mList.size=%zu\n", mList.size());
+    dprintf(fd, "  mTotalDroppedDataCount=%" PRIu64 "\n", mTotalDroppedDataCount);
 }
 
-int32_t RingBuffer::currentSizeBytes() const {
-    return mCurrentSizeBytes;
+int32_t RingBuffer::size() const {
+    const std::scoped_lock<std::mutex> lock(mMutex);
+    return mList.size();
 }
 
 }  // namespace telemetry
diff --git a/cpp/telemetry/src/RingBuffer.h b/cpp/telemetry/src/RingBuffer.h
index 8978f32..07ce709 100644
--- a/cpp/telemetry/src/RingBuffer.h
+++ b/cpp/telemetry/src/RingBuffer.h
@@ -17,37 +17,48 @@
 #ifndef CPP_TELEMETRY_SRC_RINGBUFFER_H_
 #define CPP_TELEMETRY_SRC_RINGBUFFER_H_
 
-#include <BufferedCarData.h>
+#include "BufferedCarData.h"
 
 #include <list>
+#include <mutex>
 
 namespace android {
 namespace automotive {
 namespace telemetry {
 
+// A ring buffer that holds BufferedCarData. It drops old data if it's full.
+// Thread-safe.
 class RingBuffer {
 public:
-    // RingBuffer limits `currentSizeBytes()` to the given param `sizeLimitBytes`.
-    explicit RingBuffer(int32_t sizeLimitBytes);
+    // RingBuffer limits the number of elements in the buffer to the given param `sizeLimit`.
+    // Doesn't pre-allocate the memory.
+    explicit RingBuffer(int32_t sizeLimit);
+
+    // Not copyable or movable
+    RingBuffer(const RingBuffer&) = delete;
+    RingBuffer& operator=(const RingBuffer&) = delete;
+    RingBuffer(RingBuffer&&) = delete;
+    RingBuffer& operator=(RingBuffer&&) = delete;
 
     // Pushes the data to the buffer. If the buffer is full, it removes the oldest data.
     // Supports moving the data to the RingBuffer.
     void push(BufferedCarData&& data);
 
-    // Returns all the CarData with the given `id` and removes them from the buffer.
-    // Complexity is O(n), as this method is expected to be called infrequently.
-    std::vector<BufferedCarData> popAllDataForId(int32_t id);
+    // Returns the oldest element from the ring buffer and removes it from the buffer.
+    BufferedCarData popFront();
 
     // Dumps the current state for dumpsys.
-    void dump(int fd, int indent) const;
+    void dump(int fd) const;
 
-    // Returns the total size of CarData content in the buffer.
-    int32_t currentSizeBytes() const;
+    // Returns the number of elements in the buffer.
+    int32_t size() const;
 
 private:
-    const int32_t mSizeLimitBytes;
-    int32_t mCurrentSizeBytes;
+    mutable std::mutex mMutex;  // a mutex for the whole instance
 
+    const int32_t mSizeLimit;
+
+    // TODO(b/174608802): Improve dropped CarData handling, see ag/13818937 for details.
     int64_t mTotalDroppedDataCount;
 
     // Linked list that holds all the data and allows deleting old data when the buffer is full.
diff --git a/cpp/telemetry/src/TelemetryServer.cpp b/cpp/telemetry/src/TelemetryServer.cpp
new file mode 100644
index 0000000..54cd3c4
--- /dev/null
+++ b/cpp/telemetry/src/TelemetryServer.cpp
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+#include "TelemetryServer.h"
+
+#include "CarTelemetryImpl.h"
+#include "RingBuffer.h"
+
+#include <android-base/chrono_utils.h>
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+#include <android/binder_interface_utils.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
+
+#include <inttypes.h>  // for PRIu64 and friends
+
+#include <memory>
+#include <thread>  // NOLINT(build/c++11)
+
+namespace android {
+namespace automotive {
+namespace telemetry {
+
+using ::android::automotive::telemetry::RingBuffer;
+
+constexpr const char kCarTelemetryServiceName[] =
+        "android.frameworks.automotive.telemetry.ICarTelemetry/default";
+constexpr const char kCarTelemetryInternalServiceName[] =
+        "android.automotive.telemetry.internal.ICarTelemetryInternal/default";
+
+// TODO(b/183444070): make it configurable using sysprop
+// CarData count limit in the RingBuffer. In worst case it will use kMaxBufferSize * 10Kb memory,
+// which is ~ 1MB.
+const int kMaxBufferSize = 100;
+
+TelemetryServer::TelemetryServer() : mRingBuffer(kMaxBufferSize) {}
+
+void TelemetryServer::registerServices() {
+    std::shared_ptr<CarTelemetryImpl> telemetry =
+            ndk::SharedRefBase::make<CarTelemetryImpl>(&mRingBuffer);
+    std::shared_ptr<CarTelemetryInternalImpl> telemetryInternal =
+            ndk::SharedRefBase::make<CarTelemetryInternalImpl>(&mRingBuffer);
+
+    // Wait for the service manager before starting ICarTelemetry service.
+    while (android::base::GetProperty("init.svc.servicemanager", "") != "running") {
+        // Poll frequent enough so the writer clients can connect to the service during boot.
+        std::this_thread::sleep_for(250ms);
+    }
+
+    LOG(VERBOSE) << "Registering " << kCarTelemetryServiceName;
+    binder_exception_t exception =
+            ::AServiceManager_addService(telemetry->asBinder().get(), kCarTelemetryServiceName);
+    if (exception != ::EX_NONE) {
+        LOG(FATAL) << "Unable to register " << kCarTelemetryServiceName
+                   << ", exception=" << exception;
+    }
+
+    LOG(VERBOSE) << "Registering " << kCarTelemetryInternalServiceName;
+    exception = ::AServiceManager_addService(telemetryInternal->asBinder().get(),
+                                             kCarTelemetryInternalServiceName);
+    if (exception != ::EX_NONE) {
+        LOG(FATAL) << "Unable to register " << kCarTelemetryInternalServiceName
+                   << ", exception=" << exception;
+    }
+}
+
+void TelemetryServer::startAndJoinThreadPool() {
+    ::ABinderProcess_startThreadPool();  // Starts the default 15 binder threads.
+    ::ABinderProcess_joinThreadPool();
+}
+
+}  // namespace telemetry
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/telemetry/src/TelemetryServer.h b/cpp/telemetry/src/TelemetryServer.h
new file mode 100644
index 0000000..0b400a1
--- /dev/null
+++ b/cpp/telemetry/src/TelemetryServer.h
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+#ifndef CPP_TELEMETRY_SRC_TELEMETRYSERVER_H_
+#define CPP_TELEMETRY_SRC_TELEMETRYSERVER_H_
+
+#include "CarTelemetryImpl.h"
+#include "CarTelemetryInternalImpl.h"
+
+#include <utils/Errors.h>
+
+namespace android {
+namespace automotive {
+namespace telemetry {
+
+class TelemetryServer {
+public:
+    TelemetryServer();
+
+    // Registers all the implemented AIDL services. Waits until `servicemanager` is available.
+    // Aborts the process if fails.
+    void registerServices();
+
+    // Blocks the thread.
+    void startAndJoinThreadPool();
+
+private:
+    RingBuffer mRingBuffer;
+};
+
+}  // namespace telemetry
+}  // namespace automotive
+}  // namespace android
+
+#endif  // CPP_TELEMETRY_SRC_TELEMETRYSERVER_H_
diff --git a/cpp/telemetry/src/main.cpp b/cpp/telemetry/src/main.cpp
index fcec0ee..1bd0dde 100644
--- a/cpp/telemetry/src/main.cpp
+++ b/cpp/telemetry/src/main.cpp
@@ -14,52 +14,23 @@
  * limitations under the License.
  */
 
-#include "CarTelemetryImpl.h"
-#include "RingBuffer.h"
+#include "TelemetryServer.h"
 
-#include <android-base/chrono_utils.h>
 #include <android-base/logging.h>
-#include <android-base/properties.h>
-#include <binder/IPCThreadState.h>
-#include <binder/IServiceManager.h>
-#include <binder/ProcessState.h>
 
-#include <thread>  // NOLINT(build/c++11)
-
-using ::android::String16;
-using ::android::automotive::telemetry::CarTelemetryImpl;
-using ::android::automotive::telemetry::RingBuffer;
-
-constexpr const char kCarTelemetryServiceName[] =
-        "android.frameworks.automotive.telemetry.ICarTelemetry/default";
-// Total CarData content size limit in the RingBuffer. 2MB max memory for buffer is good for now.
-const int kMaxBufferSizeKilobytes = 2048;
+using ::android::automotive::telemetry::TelemetryServer;
 
 // TODO(b/174608802): handle SIGQUIT/SIGTERM
 
 int main(void) {
     LOG(INFO) << "Starting cartelemetryd";
 
-    RingBuffer buffer(kMaxBufferSizeKilobytes * 1024);
+    TelemetryServer server;
 
-    android::sp<CarTelemetryImpl> telemetry = new CarTelemetryImpl(&buffer);
-
-    // Wait for the service manager before starting ICarTelemetry service.
-    while (android::base::GetProperty("init.svc.servicemanager", "") != "running") {
-        // Poll frequent enough so the writer clients can connect to the service during boot.
-        std::this_thread::sleep_for(250ms);
-    }
-
-    LOG(VERBOSE) << "Registering " << kCarTelemetryServiceName;
-    auto status = android::defaultServiceManager()->addService(String16(kCarTelemetryServiceName),
-                                                               telemetry);
-    if (status != android::OK) {
-        LOG(ERROR) << "Unable to register " << kCarTelemetryServiceName << ", status=" << status;
-        return 1;
-    }
+    // Register AIDL services. Aborts the server if fails.
+    server.registerServices();
 
     LOG(VERBOSE) << "Service is created, joining the threadpool";
-    android::ProcessState::self()->startThreadPool();  // Starts default 15 binder threads.
-    android::IPCThreadState::self()->joinThreadPool();
+    server.startAndJoinThreadPool();
     return 1;  // never reaches
 }
diff --git a/cpp/telemetry/tests/CarTelemetryImplTest.cpp b/cpp/telemetry/tests/CarTelemetryImplTest.cpp
index f0af963..0286477 100644
--- a/cpp/telemetry/tests/CarTelemetryImplTest.cpp
+++ b/cpp/telemetry/tests/CarTelemetryImplTest.cpp
@@ -17,8 +17,8 @@
 #include "CarTelemetryImpl.h"
 #include "RingBuffer.h"
 
-#include <android/frameworks/automotive/telemetry/CarData.h>
-#include <android/frameworks/automotive/telemetry/ICarTelemetry.h>
+#include <aidl/android/frameworks/automotive/telemetry/CarData.h>
+#include <aidl/android/frameworks/automotive/telemetry/ICarTelemetry.h>
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
@@ -29,13 +29,12 @@
 namespace android {
 namespace automotive {
 namespace telemetry {
-namespace {
 
-using android::frameworks::automotive::telemetry::CarData;
-using android::frameworks::automotive::telemetry::ICarTelemetry;
-using testing::ContainerEq;
+using ::aidl::android::frameworks::automotive::telemetry::CarData;
+using ::aidl::android::frameworks::automotive::telemetry::ICarTelemetry;
+using ::testing::ContainerEq;
 
-const size_t kMaxBufferSizeBytes = 1024;
+const size_t kMaxBufferSize = 5;
 
 CarData buildCarData(int id, const std::vector<uint8_t>& content) {
     CarData msg;
@@ -44,56 +43,57 @@
     return msg;
 }
 
+BufferedCarData buildBufferedCarData(const CarData& data, uid_t publisherUid) {
+    return {.mId = data.id, .mContent = std::move(data.content), .mPublisherUid = publisherUid};
+}
+
 class CarTelemetryImplTest : public ::testing::Test {
 protected:
     CarTelemetryImplTest() :
-          mBuffer(RingBuffer(kMaxBufferSizeBytes)),
-          mTelemetry(std::make_unique<CarTelemetryImpl>(&mBuffer)) {}
+          mBuffer(RingBuffer(kMaxBufferSize)),
+          mTelemetry(ndk::SharedRefBase::make<CarTelemetryImpl>(&mBuffer)) {}
 
     RingBuffer mBuffer;
-    std::unique_ptr<ICarTelemetry> mTelemetry;
+    std::shared_ptr<ICarTelemetry> mTelemetry;
 };
 
-TEST_F(CarTelemetryImplTest, TestWriteReturnsOkStatus) {
+TEST_F(CarTelemetryImplTest, WriteReturnsOkStatus) {
     CarData msg = buildCarData(101, {1, 0, 1, 0});
 
     auto status = mTelemetry->write({msg});
 
-    EXPECT_TRUE(status.isOk()) << status;
+    EXPECT_TRUE(status.isOk()) << status.getMessage();
 }
 
-TEST_F(CarTelemetryImplTest, TestWriteAddsCarDataToRingBuffer) {
+TEST_F(CarTelemetryImplTest, WriteAddsCarDataToRingBuffer) {
     CarData msg = buildCarData(101, {1, 0, 1, 0});
 
     mTelemetry->write({msg});
 
-    std::vector<BufferedCarData> result = mBuffer.popAllDataForId(101);
-    std::vector<BufferedCarData> expected = {BufferedCarData(msg, getuid())};
-    EXPECT_THAT(result, ContainerEq(expected));
+    EXPECT_EQ(mBuffer.popFront(), buildBufferedCarData(msg, getuid()));
 }
 
-TEST_F(CarTelemetryImplTest, TestWriteBuffersOnlyLimitedAmount) {
-    RingBuffer buffer(15);  // bytes
-    CarTelemetryImpl telemetry(&buffer);
+TEST_F(CarTelemetryImplTest, WriteBuffersOnlyLimitedAmount) {
+    RingBuffer buffer(/* sizeLimit= */ 3);
+    auto telemetry = ndk::SharedRefBase::make<CarTelemetryImpl>(&buffer);
 
-    CarData msg101_2 = buildCarData(101, {1, 0});        // 2 bytes
-    CarData msg101_4 = buildCarData(101, {1, 0, 1, 0});  // 4 bytes
-    CarData msg201_3 = buildCarData(201, {3, 3, 3});     // 3 bytes
+    CarData msg101_2 = buildCarData(101, {1, 0});
+    CarData msg101_4 = buildCarData(101, {1, 0, 1, 0});
+    CarData msg201_3 = buildCarData(201, {3, 3, 3});
 
-    telemetry.write({msg101_2, msg101_4, msg101_4, msg201_3, msg201_3});
+    // Inserting 5 elements
+    telemetry->write({msg101_2, msg101_4, msg101_4, msg201_3});
+    telemetry->write({msg201_3});
 
-    // Size without the first msg101_2, because ushing the last msg201_3 will force RingBuffer to
-    // drop the earliest msg101_2.
-    EXPECT_EQ(buffer.currentSizeBytes(), 14);
-    std::vector<BufferedCarData> result = buffer.popAllDataForId(101);
-    std::vector<BufferedCarData> expected = {BufferedCarData(msg101_4, getuid()),
-                                             BufferedCarData(msg101_4, getuid())};
+    EXPECT_EQ(buffer.size(), 3);
+    std::vector<BufferedCarData> result = {buffer.popFront(), buffer.popFront(), buffer.popFront()};
+    std::vector<BufferedCarData> expected = {buildBufferedCarData(msg101_4, getuid()),
+                                             buildBufferedCarData(msg201_3, getuid()),
+                                             buildBufferedCarData(msg201_3, getuid())};
     EXPECT_THAT(result, ContainerEq(expected));
-    // Fetching 2x msg101_4 will decrease the size of the RingBuffer
-    EXPECT_EQ(buffer.currentSizeBytes(), 6);
+    EXPECT_EQ(buffer.size(), 0);
 }
 
-}  // namespace
 }  // namespace telemetry
 }  // namespace automotive
 }  // namespace android
diff --git a/cpp/telemetry/tests/CarTelemetryInternalImplTest.cpp b/cpp/telemetry/tests/CarTelemetryInternalImplTest.cpp
new file mode 100644
index 0000000..22838cd
--- /dev/null
+++ b/cpp/telemetry/tests/CarTelemetryInternalImplTest.cpp
@@ -0,0 +1,85 @@
+/*
+ * Copyright 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.
+ */
+
+#include "CarTelemetryInternalImpl.h"
+#include "RingBuffer.h"
+
+#include <aidl/android/automotive/telemetry/internal/BnCarDataListener.h>
+#include <aidl/android/automotive/telemetry/internal/CarDataInternal.h>
+#include <aidl/android/automotive/telemetry/internal/ICarTelemetryInternal.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <unistd.h>
+
+#include <memory>
+
+namespace android {
+namespace automotive {
+namespace telemetry {
+
+using ::aidl::android::automotive::telemetry::internal::BnCarDataListener;
+using ::aidl::android::automotive::telemetry::internal::CarDataInternal;
+using ::aidl::android::automotive::telemetry::internal::ICarTelemetryInternal;
+using ::ndk::ScopedAStatus;
+
+const size_t kMaxBufferSize = 5;
+
+class MockCarDataListener : public BnCarDataListener {
+public:
+    MOCK_METHOD(ScopedAStatus, onCarDataReceived, (const std::vector<CarDataInternal>& dataList),
+                (override));
+};
+
+// The main test class.
+class CarTelemetryInternalImplTest : public ::testing::Test {
+protected:
+    CarTelemetryInternalImplTest() :
+          mBuffer(RingBuffer(kMaxBufferSize)),
+          mTelemetryInternal(ndk::SharedRefBase::make<CarTelemetryInternalImpl>(&mBuffer)),
+          mMockCarDataListener(ndk::SharedRefBase::make<MockCarDataListener>()) {}
+
+    RingBuffer mBuffer;
+    std::shared_ptr<ICarTelemetryInternal> mTelemetryInternal;
+    std::shared_ptr<MockCarDataListener> mMockCarDataListener;
+};
+
+TEST_F(CarTelemetryInternalImplTest, SetListenerReturnsOk) {
+    auto status = mTelemetryInternal->setListener(mMockCarDataListener);
+
+    EXPECT_TRUE(status.isOk()) << status.getMessage();
+}
+
+TEST_F(CarTelemetryInternalImplTest, SetListenerFailsWhenAlreadySubscribed) {
+    mTelemetryInternal->setListener(mMockCarDataListener);
+
+    auto status = mTelemetryInternal->setListener(ndk::SharedRefBase::make<MockCarDataListener>());
+
+    EXPECT_EQ(status.getExceptionCode(), ::EX_ILLEGAL_STATE) << status.getMessage();
+}
+
+TEST_F(CarTelemetryInternalImplTest, ClearListenerWorks) {
+    mTelemetryInternal->setListener(mMockCarDataListener);
+
+    mTelemetryInternal->clearListener();
+    auto status = mTelemetryInternal->setListener(mMockCarDataListener);
+
+    EXPECT_TRUE(status.isOk()) << status.getMessage();
+}
+
+}  // namespace telemetry
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/telemetry/tests/RingBufferTest.cpp b/cpp/telemetry/tests/RingBufferTest.cpp
index 077705e..c5b5bc7 100644
--- a/cpp/telemetry/tests/RingBufferTest.cpp
+++ b/cpp/telemetry/tests/RingBufferTest.cpp
@@ -26,43 +26,33 @@
 namespace android {
 namespace automotive {
 namespace telemetry {
-namespace {
 
 using testing::ContainerEq;
 
 BufferedCarData buildBufferedCarData(int32_t id, const std::vector<uint8_t>& content) {
-    return BufferedCarData(id, content, /* uid= */ 0);
+    return {.mId = id, .mContent = content, .mPublisherUid = 0};
 }
 
-TEST(RingBufferTest, TestPopAllDataForIdReturnsCorrectResults) {
-    RingBuffer buffer(10);  // bytes
-    buffer.push(buildBufferedCarData(101, {7}));
+TEST(RingBufferTest, PopFrontReturnsCorrectResults) {
+    RingBuffer buffer(/* sizeLimit= */ 10);
     buffer.push(buildBufferedCarData(101, {7}));
     buffer.push(buildBufferedCarData(102, {7}));
+
+    BufferedCarData result = buffer.popFront();
+
+    EXPECT_EQ(result, buildBufferedCarData(101, {7}));
+}
+
+TEST(RingBufferTest, PopFrontRemovesFromBuffer) {
+    RingBuffer buffer(/* sizeLimit= */ 10);
     buffer.push(buildBufferedCarData(101, {7}));
+    buffer.push(buildBufferedCarData(102, {7, 8}));
 
-    std::vector<BufferedCarData> result = buffer.popAllDataForId(101);
+    buffer.popFront();
 
-    std::vector<BufferedCarData> expected = {buildBufferedCarData(101, {7}),
-                                             buildBufferedCarData(101, {7}),
-                                             buildBufferedCarData(101, {7})};
-    EXPECT_THAT(result, ContainerEq(expected));
+    EXPECT_EQ(buffer.size(), 1);  // only ID=102 left
 }
 
-TEST(RingBufferTest, TestPopAllDataForIdRemovesFromBuffer) {
-    RingBuffer buffer(10);                              // bytes
-    buffer.push(buildBufferedCarData(101, {7}));        // 1 byte
-    buffer.push(buildBufferedCarData(102, {7, 8}));     // 2 byte
-    buffer.push(buildBufferedCarData(103, {7, 8, 9}));  // 3 bytes
-
-    buffer.popAllDataForId(101);  // also removes CarData with the given ID
-
-    EXPECT_EQ(buffer.popAllDataForId(101).size(), 0);
-    EXPECT_EQ(buffer.popAllDataForId(102).size(), 1);
-    EXPECT_EQ(buffer.currentSizeBytes(), 3);  // bytes, because only ID=103 left.
-}
-
-}  // namespace
 }  // namespace telemetry
 }  // namespace automotive
 }  // namespace android
diff --git a/service/src/com/android/car/audio/CarAudioContext.java b/service/src/com/android/car/audio/CarAudioContext.java
index 5c76e17..c93eca3 100644
--- a/service/src/com/android/car/audio/CarAudioContext.java
+++ b/service/src/com/android/car/audio/CarAudioContext.java
@@ -254,6 +254,10 @@
         return uniqueContexts;
     }
 
+    static boolean isCriticalAudioContext(@CarAudioContext.AudioContext int audioContext) {
+        return CarAudioContext.EMERGENCY == audioContext || CarAudioContext.SAFETY == audioContext;
+    }
+
     static String toString(@AudioContext int audioContext) {
         String name = CONTEXT_NAMES.get(audioContext);
         if (name != null) {
diff --git a/service/src/com/android/car/audio/CarAudioFocus.java b/service/src/com/android/car/audio/CarAudioFocus.java
index 5b5ac10..0b10e2b 100644
--- a/service/src/com/android/car/audio/CarAudioFocus.java
+++ b/service/src/com/android/car/audio/CarAudioFocus.java
@@ -15,6 +15,8 @@
  */
 package com.android.car.audio;
 
+import static com.android.car.audio.CarAudioContext.isCriticalAudioContext;
+
 import android.content.pm.PackageManager;
 import android.media.AudioAttributes;
 import android.media.AudioFocusInfo;
@@ -26,7 +28,6 @@
 import android.util.Slog;
 
 import com.android.car.CarLog;
-import com.android.car.audio.CarAudioContext.AudioContext;
 import com.android.internal.annotations.GuardedBy;
 
 import java.util.ArrayList;
@@ -131,10 +132,6 @@
         }
     }
 
-    private boolean isCriticalAudioContext(@AudioContext int audioContext) {
-        return CarAudioContext.EMERGENCY == audioContext || CarAudioContext.SAFETY == audioContext;
-    }
-
     // This sends a focus loss message to the targeted requester.
     private void sendFocusLossLocked(AudioFocusInfo loser, int lossType) {
         int result = mAudioManager.dispatchAudioFocusChange(loser, lossType,
diff --git a/service/src/com/android/car/audio/CarAudioPowerListener.java b/service/src/com/android/car/audio/CarAudioPowerListener.java
index eed53cb..0ee885c 100644
--- a/service/src/com/android/car/audio/CarAudioPowerListener.java
+++ b/service/src/com/android/car/audio/CarAudioPowerListener.java
@@ -78,7 +78,7 @@
     void startListeningForPolicyChanges() {
         if (mCarPowerManagementService == null) {
             Slog.w(TAG, "Cannot find CarPowerManagementService");
-            mCarAudioService.enableAudio();
+            mCarAudioService.setAudioEnabled(/* isAudioEnabled= */ true);
             return;
         }
 
@@ -100,7 +100,7 @@
 
         if (policy == null) {
             Slog.w(TAG, "Policy is null. Defaulting to enabled");
-            mCarAudioService.enableAudio();
+            mCarAudioService.setAudioEnabled(/* isAudioEnabled= */ true);
             return;
         }
 
@@ -112,11 +112,6 @@
     @GuardedBy("mLock")
     private void updateAudioPowerStateLocked(CarPowerPolicy policy) {
         mIsAudioEnabled = policy.isComponentEnabled(AUDIO);
-
-        if (mIsAudioEnabled) {
-            mCarAudioService.enableAudio();
-        } else {
-            mCarAudioService.disableAudio();
-        }
+        mCarAudioService.setAudioEnabled(mIsAudioEnabled);
     }
 }
diff --git a/service/src/com/android/car/audio/CarAudioService.java b/service/src/com/android/car/audio/CarAudioService.java
index 08e648d..1e81c94 100644
--- a/service/src/com/android/car/audio/CarAudioService.java
+++ b/service/src/com/android/car/audio/CarAudioService.java
@@ -1243,22 +1243,16 @@
         return getCarAudioZone(zoneId).getInputAudioDevices();
     }
 
-    void disableAudio() {
-        // TODO(b/176258537) mute everything
+    void setAudioEnabled(boolean isAudioEnabled) {
         if (Slogf.isLoggable(CarLog.TAG_AUDIO, Log.DEBUG)) {
-            Slogf.d(CarLog.TAG_AUDIO, "Disabling audio");
+            Slogf.d(CarLog.TAG_AUDIO, "Setting isAudioEnabled to %b", isAudioEnabled);
         }
 
-        mFocusHandler.setRestrictFocus(/* isFocusRestricted= */ true);
-    }
-
-    void enableAudio() {
-        // TODO(b/176258537) unmute appropriate things
-        if (Slogf.isLoggable(CarLog.TAG_AUDIO, Log.DEBUG)) {
-            Slogf.d(CarLog.TAG_AUDIO, "Enabling audio");
+        mFocusHandler.setRestrictFocus(/* isFocusRestricted= */ !isAudioEnabled);
+        if (mUseCarVolumeGroupMuting) {
+            mCarVolumeGroupMuting.setRestrictMuting(/* isMutingRestricted= */ !isAudioEnabled);
         }
-
-        mFocusHandler.setRestrictFocus(/* isFocusRestricted= */ false);
+        // TODO(b/176258537) if not using group volume, then set master mute accordingly
     }
 
     private void enforcePermission(String permissionName) {
diff --git a/service/src/com/android/car/audio/CarAudioZone.java b/service/src/com/android/car/audio/CarAudioZone.java
index 33d5d40..de540d2 100644
--- a/service/src/com/android/car/audio/CarAudioZone.java
+++ b/service/src/com/android/car/audio/CarAudioZone.java
@@ -115,7 +115,8 @@
      *
      * Note that it is fine that there are devices which do not appear in any group. Those devices
      * may be reserved for other purposes.
-     * Step value validation is done in {@link CarVolumeGroup#bind(int, CarAudioDeviceInfo)}
+     * Step value validation is done in
+     * {@link CarVolumeGroup.Builder#setDeviceInfoForContext(int, CarAudioDeviceInfo)}
      */
     boolean validateVolumeGroups() {
         Set<Integer> contexts = new HashSet<>();
diff --git a/service/src/com/android/car/audio/CarAudioZonesHelper.java b/service/src/com/android/car/audio/CarAudioZonesHelper.java
index cf85356..7956ea2 100644
--- a/service/src/com/android/car/audio/CarAudioZonesHelper.java
+++ b/service/src/com/android/car/audio/CarAudioZonesHelper.java
@@ -119,9 +119,10 @@
         }
     }
 
-    static void bindNonLegacyContexts(CarVolumeGroup group, CarAudioDeviceInfo info) {
+    static void setNonLegacyContexts(CarVolumeGroup.Builder groupBuilder,
+            CarAudioDeviceInfo info) {
         for (@AudioContext int audioContext : NON_LEGACY_CONTEXTS) {
-            group.bind(audioContext, info);
+            groupBuilder.setDeviceInfoForContext(audioContext, info);
         }
     }
 
@@ -401,19 +402,20 @@
 
     private CarVolumeGroup parseVolumeGroup(XmlPullParser parser, int zoneId, int groupId)
             throws XmlPullParserException, IOException {
-        CarVolumeGroup group =
-                new CarVolumeGroup(zoneId, groupId, mCarAudioSettings, mUseCarVolumeGroupMute);
+        CarVolumeGroup.Builder groupBuilder =
+                new CarVolumeGroup.Builder(zoneId, groupId, mCarAudioSettings,
+                        mUseCarVolumeGroupMute);
         while (parser.next() != XmlPullParser.END_TAG) {
             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
             if (TAG_AUDIO_DEVICE.equals(parser.getName())) {
                 String address = parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS);
                 validateOutputDeviceExist(address);
-                parseVolumeGroupContexts(parser, group, address);
+                parseVolumeGroupContexts(parser, groupBuilder, address);
             } else {
                 skip(parser);
             }
         }
-        return group;
+        return groupBuilder.build();
     }
 
     private void validateOutputDeviceExist(String address) {
@@ -425,7 +427,7 @@
     }
 
     private void parseVolumeGroupContexts(
-            XmlPullParser parser, CarVolumeGroup group, String address)
+            XmlPullParser parser, CarVolumeGroup.Builder groupBuilder, String address)
             throws XmlPullParserException, IOException {
         while (parser.next() != XmlPullParser.END_TAG) {
             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
@@ -434,11 +436,11 @@
                         parser.getAttributeValue(NAMESPACE, ATTR_CONTEXT_NAME));
                 validateCarAudioContextSupport(carAudioContext);
                 CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address);
-                group.bind(carAudioContext, info);
+                groupBuilder.setDeviceInfoForContext(carAudioContext, info);
 
                 // If V1, default new contexts to same device as DEFAULT_AUDIO_USAGE
                 if (isVersionOne() && carAudioContext == CarAudioService.DEFAULT_AUDIO_CONTEXT) {
-                    bindNonLegacyContexts(group, info);
+                    setNonLegacyContexts(groupBuilder, info);
                 }
             }
             // Always skip to upper level since we're at the lowest.
diff --git a/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java b/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
index 42fccf2..173252a 100644
--- a/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
+++ b/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
@@ -127,29 +127,15 @@
     }
 
     SparseArray<CarAudioZone> loadAudioZones() {
-        final CarAudioZone zone = new CarAudioZone(PRIMARY_AUDIO_ZONE,
-                "Primary zone");
-        for (CarVolumeGroup group : loadVolumeGroups()) {
-            zone.addVolumeGroup(group);
-            bindContextsForVolumeGroup(group);
+        CarAudioZone zone = new CarAudioZone(PRIMARY_AUDIO_ZONE, "Primary zone");
+        for (CarVolumeGroup volumeGroup : loadVolumeGroups()) {
+            zone.addVolumeGroup(volumeGroup);
         }
         SparseArray<CarAudioZone> carAudioZones = new SparseArray<>();
         carAudioZones.put(PRIMARY_AUDIO_ZONE, zone);
         return carAudioZones;
     }
 
-    private void bindContextsForVolumeGroup(CarVolumeGroup group) {
-        for (int legacyAudioContext : group.getContexts()) {
-            int busNumber = mLegacyAudioContextToBus.get(legacyAudioContext);
-            CarAudioDeviceInfo info = mBusToCarAudioDeviceInfo.get(busNumber);
-            group.bind(legacyAudioContext, info);
-
-            if (legacyAudioContext == CarAudioService.DEFAULT_AUDIO_CONTEXT) {
-                CarAudioZonesHelper.bindNonLegacyContexts(group, info);
-            }
-        }
-    }
-
     /**
      * @return all {@link CarVolumeGroup} read from configuration.
      */
@@ -186,8 +172,33 @@
         return carVolumeGroups;
     }
 
-    private CarVolumeGroup parseVolumeGroup(int id, AttributeSet attrs, XmlResourceParser parser)
-            throws XmlPullParserException, IOException {
+    private CarVolumeGroup parseVolumeGroup(int id, AttributeSet attrs,
+            XmlResourceParser parser) throws XmlPullParserException, IOException {
+        CarVolumeGroup.Builder builder = new CarVolumeGroup.Builder(PRIMARY_AUDIO_ZONE, id,
+                mCarAudioSettings, /* useCarVolumeGroupMute= */ false);
+
+        List<Integer> audioContexts = parseAudioContexts(parser, attrs);
+
+        for (int i = 0; i < audioContexts.size(); i++) {
+            bindContextToBuilder(builder, audioContexts.get(i));
+        }
+
+        return builder.build();
+    }
+
+
+    private void bindContextToBuilder(CarVolumeGroup.Builder groupBuilder, int legacyAudioContext) {
+        int busNumber = mLegacyAudioContextToBus.get(legacyAudioContext);
+        CarAudioDeviceInfo info = mBusToCarAudioDeviceInfo.get(busNumber);
+        groupBuilder.setDeviceInfoForContext(legacyAudioContext, info);
+
+        if (legacyAudioContext == CarAudioService.DEFAULT_AUDIO_CONTEXT) {
+            CarAudioZonesHelper.setNonLegacyContexts(groupBuilder, info);
+        }
+    }
+
+    private List<Integer> parseAudioContexts(XmlResourceParser parser, AttributeSet attrs)
+            throws IOException, XmlPullParserException {
         List<Integer> contexts = new ArrayList<>();
         int type;
         int innerDepth = parser.getDepth();
@@ -204,8 +215,7 @@
             }
         }
 
-        return new CarVolumeGroup(mCarAudioSettings, PRIMARY_AUDIO_ZONE, id,
-                contexts.stream().mapToInt(i -> i).filter(i -> i >= 0).toArray());
+        return contexts;
     }
 
     /**
diff --git a/service/src/com/android/car/audio/CarVolumeGroup.java b/service/src/com/android/car/audio/CarVolumeGroup.java
index cabce82..4394be0 100644
--- a/service/src/com/android/car/audio/CarVolumeGroup.java
+++ b/service/src/com/android/car/audio/CarVolumeGroup.java
@@ -28,6 +28,7 @@
 import com.android.car.CarLog;
 import com.android.car.audio.CarAudioContext.AudioContext;
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 
 import java.util.ArrayList;
@@ -46,42 +47,50 @@
 /* package */ final class CarVolumeGroup {
 
     private final boolean mUseCarVolumeGroupMute;
+    private final boolean mHasCriticalAudioContexts;
     private final CarAudioSettings mSettingsManager;
-    private final int mZoneId;
+    private final int mDefaultGain;
     private final int mId;
-    private final SparseArray<String> mContextToAddress = new SparseArray<>();
-    private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo = new HashMap<>();
+    private final int mMaxGain;
+    private final int mMinGain;
+    private final int mStepSize;
+    private final int mZoneId;
+    private final SparseArray<String> mContextToAddress;
+    private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo;
 
     private final Object mLock = new Object();
 
-    private int mDefaultGain = Integer.MIN_VALUE;
-    private int mMaxGain = Integer.MIN_VALUE;
-    private int mMinGain = Integer.MAX_VALUE;
-    private int mStepSize = 0;
+    @GuardedBy("mLock")
     private int mStoredGainIndex;
+    @GuardedBy("mLock")
     private int mCurrentGainIndex = -1;
+    @GuardedBy("mLock")
     private boolean mIsMuted;
+    @GuardedBy("mLock")
     private @UserIdInt int mUserId = UserHandle.USER_CURRENT;
 
-    CarVolumeGroup(int zoneId, int id, CarAudioSettings settings, boolean useCarVolumeGroupMute) {
-        mSettingsManager = settings;
+    private CarVolumeGroup(int zoneId, int id, CarAudioSettings settingsManager, int stepSize,
+            int defaultGain, int minGain, int maxGain, SparseArray<String> contextToAddress,
+            Map<String, CarAudioDeviceInfo> addressToCarAudioDeviceInfo,
+            boolean useCarVolumeGroupMute) {
+
+        mSettingsManager = settingsManager;
         mZoneId = zoneId;
         mId = id;
-        mStoredGainIndex = mSettingsManager.getStoredVolumeGainIndexForUser(mUserId, mZoneId, mId);
+        mStepSize = stepSize;
+        mDefaultGain = defaultGain;
+        mMinGain = minGain;
+        mMaxGain = maxGain;
+        mContextToAddress = contextToAddress;
+        mAddressToCarAudioDeviceInfo = addressToCarAudioDeviceInfo;
         mUseCarVolumeGroupMute = useCarVolumeGroupMute;
+
+        mHasCriticalAudioContexts = containsCriticalAudioContext(contextToAddress);
     }
 
-    /**
-     * @deprecated In favor of {@link #CarVolumeGroup(int, int, CarAudioSettings, boolean)}
-     * Only used for legacy configuration via IAudioControl@1.0
-     */
-    @Deprecated
-    CarVolumeGroup(CarAudioSettings settings, int zoneId, int id, @NonNull int[] contexts) {
-        this(zoneId, id, settings, false);
-        // Deal with the pre-populated car audio contexts
-        for (int audioContext : contexts) {
-            mContextToAddress.put(audioContext, null);
-        }
+    void init() {
+        mStoredGainIndex = mSettingsManager.getStoredVolumeGainIndexForUser(mUserId, mZoneId, mId);
+        updateCurrentGainIndexLocked();
     }
 
     @Nullable
@@ -89,7 +98,8 @@
         return mAddressToCarAudioDeviceInfo.get(address);
     }
 
-    @AudioContext int[] getContexts() {
+    @AudioContext
+    int[] getContexts() {
         final int[] carAudioContexts = new int[mContextToAddress.size()];
         for (int i = 0; i < carAudioContexts.length; i++) {
             carAudioContexts[i] = mContextToAddress.keyAt(i);
@@ -106,7 +116,8 @@
         return mContextToAddress.get(audioContext);
     }
 
-    @AudioContext List<Integer> getContextsForAddress(@NonNull String address) {
+    @AudioContext
+    List<Integer> getContextsForAddress(@NonNull String address) {
         List<Integer> carAudioContexts = new ArrayList<>();
         for (int i = 0; i < mContextToAddress.size(); i++) {
             String value = mContextToAddress.valueAt(i);
@@ -121,57 +132,15 @@
         return new ArrayList<>(mAddressToCarAudioDeviceInfo.keySet());
     }
 
-    /**
-     * Binds the context number to physical address and audio device port information.
-     * Because this may change the groups min/max values, thus invalidating an index computed from
-     * a gain before this call, all calls to this function must happen at startup before any
-     * set/getGainIndex calls.
-     *
-     * @param carAudioContext Context to bind audio to {@link CarAudioContext}
-     * @param info {@link CarAudioDeviceInfo} instance relates to the physical address
-     */
-    void bind(int carAudioContext, CarAudioDeviceInfo info) {
-        Preconditions.checkArgument(mContextToAddress.get(carAudioContext) == null,
-                String.format("Context %s has already been bound to %s",
-                        CarAudioContext.toString(carAudioContext),
-                        mContextToAddress.get(carAudioContext)));
-
-        synchronized (mLock) {
-            if (mAddressToCarAudioDeviceInfo.size() == 0) {
-                mStepSize = info.getStepValue();
-            } else {
-                Preconditions.checkArgument(
-                        info.getStepValue() == mStepSize,
-                        "Gain controls within one group must have same step value");
-            }
-
-            mAddressToCarAudioDeviceInfo.put(info.getAddress(), info);
-            mContextToAddress.put(carAudioContext, info.getAddress());
-
-            if (info.getDefaultGain() > mDefaultGain) {
-                // We're arbitrarily selecting the highest
-                // device default gain as the group's default.
-                mDefaultGain = info.getDefaultGain();
-            }
-            if (info.getMaxGain() > mMaxGain) {
-                mMaxGain = info.getMaxGain();
-            }
-            if (info.getMinGain() < mMinGain) {
-                mMinGain = info.getMinGain();
-            }
-            updateCurrentGainIndexLocked();
-        }
-    }
-
     int getMaxGainIndex() {
         synchronized (mLock) {
-            return getIndexForGainLocked(mMaxGain);
+            return getIndexForGain(mMaxGain);
         }
     }
 
     int getMinGainIndex() {
         synchronized (mLock) {
-            return getIndexForGainLocked(mMinGain);
+            return getIndexForGain(mMinGain);
         }
     }
 
@@ -183,19 +152,13 @@
 
     /**
      * Sets the gain on this group, gain will be set on all devices within volume group.
-     * @param gainIndex The gain index
      */
     void setCurrentGainIndex(int gainIndex) {
+        int gainInMillibels = getGainForIndex(gainIndex);
+        Preconditions.checkArgument(isValidGainIndex(gainIndex),
+                "Gain out of range (%d:%d) %d index %d", mMinGain, mMaxGain,
+                gainInMillibels, gainIndex);
         synchronized (mLock) {
-            int gainInMillibels = getGainForIndexLocked(gainIndex);
-            Preconditions.checkArgument(
-                    gainInMillibels >= mMinGain && gainInMillibels <= mMaxGain,
-                    "Gain out of range ("
-                            + mMinGain + ":"
-                            + mMaxGain + ") "
-                            + gainInMillibels + "index "
-                            + gainIndex);
-
             for (String address : mAddressToCarAudioDeviceInfo.keySet()) {
                 CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address);
                 info.setCurrentGain(gainInMillibels);
@@ -217,6 +180,10 @@
         return mAddressToCarAudioDeviceInfo.get(address).getAudioDevicePort();
     }
 
+    boolean hasCriticalAudioContexts() {
+        return mHasCriticalAudioContexts;
+    }
+
     @Override
     public String toString() {
         return "CarVolumeGroup id: " + mId
@@ -229,12 +196,14 @@
         synchronized (mLock) {
             writer.printf("CarVolumeGroup(%d)\n", mId);
             writer.increaseIndent();
+            writer.printf("Zone Id(%b)\n", mZoneId);
             writer.printf("Is Muted(%b)\n", mIsMuted);
             writer.printf("UserId(%d)\n", mUserId);
             writer.printf("Persist Volume Group Mute(%b)\n",
                     mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId));
+            writer.printf("Step size: %d\n", mStepSize);
             writer.printf("Gain values (min / max / default/ current): %d %d %d %d\n", mMinGain,
-                    mMaxGain, mDefaultGain, getGainForIndexLocked(mCurrentGainIndex));
+                    mMaxGain, mDefaultGain, getGainForIndex(mCurrentGainIndex));
             writer.printf("Gain indexes (min / max / default / current): %d %d %d %d\n",
                     getMinGainIndex(), getMaxGainIndex(), getDefaultGainIndex(), mCurrentGainIndex);
             for (int i = 0; i < mContextToAddress.size(); i++) {
@@ -279,6 +248,16 @@
         }
     }
 
+    private static boolean containsCriticalAudioContext(SparseArray<String> contextToAddress) {
+        for (int i = 0; i < contextToAddress.size(); i++) {
+            int audioContext = contextToAddress.keyAt(i);
+            if (CarAudioContext.isCriticalAudioContext(audioContext)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     @GuardedBy("mLock")
     private void updateUserIdLocked(@UserIdInt int userId) {
         mUserId = userId;
@@ -299,21 +278,21 @@
      */
     @GuardedBy("mLock")
     private void updateCurrentGainIndexLocked() {
-        if (isValidGainLocked(mStoredGainIndex)) {
+        if (isValidGainIndex(mStoredGainIndex)) {
             mCurrentGainIndex = mStoredGainIndex;
         } else {
-            mCurrentGainIndex = getIndexForGainLocked(mDefaultGain);
+            mCurrentGainIndex = getIndexForGain(mDefaultGain);
         }
     }
 
-    @GuardedBy("mLock")
-    private boolean isValidGainLocked(int gain) {
-        return gain >= getIndexForGainLocked(mMinGain) && gain <= getIndexForGainLocked(mMaxGain);
+    private boolean isValidGainIndex(int gainIndex) {
+        return gainIndex >= getIndexForGain(mMinGain)
+                && gainIndex <= getIndexForGain(mMaxGain);
     }
 
     private int getDefaultGainIndex() {
         synchronized (mLock) {
-            return getIndexForGainLocked(mDefaultGain);
+            return getIndexForGain(mDefaultGain);
         }
     }
 
@@ -323,12 +302,11 @@
                 mZoneId, mId, gainIndex);
     }
 
-    private int getGainForIndexLocked(int gainIndex) {
+    private int getGainForIndex(int gainIndex) {
         return mMinGain + gainIndex * mStepSize;
     }
 
-    @GuardedBy("mLock")
-    private int getIndexForGainLocked(int gainInMillibel) {
+    private int getIndexForGain(int gainInMillibel) {
         return (gainInMillibel - mMinGain) / mStepSize;
     }
 
@@ -343,4 +321,75 @@
         }
         mIsMuted = mSettingsManager.getVolumeGroupMuteForUser(mUserId, mZoneId, mId);
     }
+
+    static final class Builder {
+        private static final int UNSET_STEP_SIZE = -1;
+
+        private final int mId;
+        private final int mZoneId;
+        private final boolean mUseCarVolumeGroupMute;
+        private final CarAudioSettings mCarAudioSettings;
+        private final SparseArray<String> mContextToAddress = new SparseArray<>();
+        private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo =
+                new HashMap<>();
+
+        @VisibleForTesting
+        int mStepSize = UNSET_STEP_SIZE;
+        @VisibleForTesting
+        int mDefaultGain = Integer.MIN_VALUE;
+        @VisibleForTesting
+        int mMaxGain = Integer.MIN_VALUE;
+        @VisibleForTesting
+        int mMinGain = Integer.MAX_VALUE;
+
+        Builder(int zoneId, int id, CarAudioSettings carAudioSettings,
+                boolean useCarVolumeGroupMute) {
+            mZoneId = zoneId;
+            mId = id;
+            mCarAudioSettings = carAudioSettings;
+            mUseCarVolumeGroupMute = useCarVolumeGroupMute;
+        }
+
+        Builder setDeviceInfoForContext(int carAudioContext, CarAudioDeviceInfo info) {
+            Preconditions.checkArgument(mContextToAddress.get(carAudioContext) == null,
+                    "Context %s has already been set to %s",
+                    CarAudioContext.toString(carAudioContext),
+                    mContextToAddress.get(carAudioContext));
+
+            if (mAddressToCarAudioDeviceInfo.isEmpty()) {
+                mStepSize = info.getStepValue();
+            } else {
+                Preconditions.checkArgument(
+                        info.getStepValue() == mStepSize,
+                        "Gain controls within one group must have same step value");
+            }
+
+            mAddressToCarAudioDeviceInfo.put(info.getAddress(), info);
+            mContextToAddress.put(carAudioContext, info.getAddress());
+
+            if (info.getDefaultGain() > mDefaultGain) {
+                // We're arbitrarily selecting the highest
+                // device default gain as the group's default.
+                mDefaultGain = info.getDefaultGain();
+            }
+            if (info.getMaxGain() > mMaxGain) {
+                mMaxGain = info.getMaxGain();
+            }
+            if (info.getMinGain() < mMinGain) {
+                mMinGain = info.getMinGain();
+            }
+
+            return this;
+        }
+
+        CarVolumeGroup build() {
+            Preconditions.checkArgument(mStepSize != UNSET_STEP_SIZE,
+                    "setDeviceInfoForContext has to be called at least once before building");
+            CarVolumeGroup group = new CarVolumeGroup(mZoneId, mId, mCarAudioSettings, mStepSize,
+                    mDefaultGain, mMinGain, mMaxGain, mContextToAddress,
+                    mAddressToCarAudioDeviceInfo, mUseCarVolumeGroupMute);
+            group.init();
+            return group;
+        }
+    }
 }
diff --git a/service/src/com/android/car/audio/CarVolumeGroupMuting.java b/service/src/com/android/car/audio/CarVolumeGroupMuting.java
index f6d2ce9..7101127 100644
--- a/service/src/com/android/car/audio/CarVolumeGroupMuting.java
+++ b/service/src/com/android/car/audio/CarVolumeGroupMuting.java
@@ -44,6 +44,8 @@
     private final Object mLock = new Object();
     @GuardedBy("mLock")
     private List<MutingInfo> mLastMutingInformation;
+    @GuardedBy("mLock")
+    private boolean mIsMutingRestricted;
 
     CarVolumeGroupMuting(@NonNull SparseArray<CarAudioZone> carAudioZones,
             @NonNull AudioControlWrapper audioControlWrapper) {
@@ -72,11 +74,26 @@
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Slog.d(TAG, "carMuteChanged");
         }
+
         List<MutingInfo> mutingInfo = generateMutingInfo();
         setLastMutingInfo(mutingInfo);
         mAudioControlWrapper.onDevicesToMuteChange(mutingInfo);
     }
 
+    public void setRestrictMuting(boolean isMutingRestricted) {
+        synchronized (mLock) {
+            mIsMutingRestricted = isMutingRestricted;
+        }
+
+        carMuteChanged();
+    }
+
+    private boolean isMutingRestricted() {
+        synchronized (mLock) {
+            return mIsMutingRestricted;
+        }
+    }
+
     private void setLastMutingInfo(List<MutingInfo> mutingInfo) {
         synchronized (mLock) {
             mLastMutingInformation = mutingInfo;
@@ -92,8 +109,11 @@
 
     private List<MutingInfo> generateMutingInfo() {
         List<MutingInfo> mutingInformation = new ArrayList<>(mCarAudioZones.size());
+
+        boolean isMutingRestricted = isMutingRestricted();
         for (int index = 0; index < mCarAudioZones.size(); index++) {
-            mutingInformation.add(generateMutingInfoFromZone(mCarAudioZones.valueAt(index)));
+            mutingInformation.add(generateMutingInfoFromZone(mCarAudioZones.valueAt(index),
+                    isMutingRestricted));
         }
 
         return mutingInformation;
@@ -106,6 +126,7 @@
         writer.println(TAG);
         writer.increaseIndent();
         synchronized (mLock) {
+            writer.printf("Is muting restricted? %b\n", mIsMutingRestricted);
             for (int index = 0; index < mLastMutingInformation.size(); index++) {
                 dumpCarMutingInfo(writer, mLastMutingInformation.get(index));
             }
@@ -134,7 +155,8 @@
     }
 
     @VisibleForTesting
-    static MutingInfo generateMutingInfoFromZone(CarAudioZone audioZone) {
+    static MutingInfo generateMutingInfoFromZone(CarAudioZone audioZone,
+            boolean isMutingRestricted) {
         MutingInfo mutingInfo = new MutingInfo();
         mutingInfo.zoneId = audioZone.getId();
 
@@ -144,11 +166,12 @@
 
         for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
             CarVolumeGroup group = groups[groupIndex];
-            if (group.isMuted()) {
+
+            if (group.isMuted() || (isMutingRestricted && !group.hasCriticalAudioContexts())) {
                 mutedDevices.addAll(group.getAddresses());
-                continue;
+            } else {
+                unMutedDevices.addAll(group.getAddresses());
             }
-            unMutedDevices.addAll(group.getAddresses());
         }
 
         mutingInfo.deviceAddressesToMute = mutedDevices.toArray(new String[mutedDevices.size()]);
diff --git a/tests/carservice_test/src/com/android/car/audio/CarVolumeGroupTest.java b/tests/carservice_test/src/com/android/car/audio/CarVolumeGroupTest.java
deleted file mode 100644
index cb78b50..0000000
--- a/tests/carservice_test/src/com/android/car/audio/CarVolumeGroupTest.java
+++ /dev/null
@@ -1,593 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.audio;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.expectThrows;
-
-import android.annotation.UserIdInt;
-import android.app.ActivityManager;
-import android.car.test.mocks.AbstractExtendedMockitoTestCase;
-import android.os.UserHandle;
-import android.util.SparseBooleanArray;
-import android.util.SparseIntArray;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.google.common.primitives.Ints;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mockito;
-
-import java.util.List;
-
-@RunWith(AndroidJUnit4.class)
-public class CarVolumeGroupTest extends AbstractExtendedMockitoTestCase{
-    private static final int STEP_VALUE = 2;
-    private static final int MIN_GAIN = 0;
-    private static final int MAX_GAIN = 5;
-    private static final int DEFAULT_GAIN = 0;
-    private static final int TEST_USER_10 = 10;
-    private static final int TEST_USER_11 = 11;
-    private static final String OTHER_ADDRESS = "other_address";
-    private static final String MEDIA_DEVICE_ADDRESS = "music";
-    private static final String NAVIGATION_DEVICE_ADDRESS = "navigation";
-
-
-    private CarAudioDeviceInfo mMediaDevice;
-    private CarAudioDeviceInfo mNavigationDevice;
-
-    @Override
-    protected void onSessionBuilder(CustomMockitoSessionBuilder session) {
-        session.spyStatic(ActivityManager.class);
-    }
-
-    @Before
-    public void setUp() {
-        mMediaDevice = generateCarAudioDeviceInfo(MEDIA_DEVICE_ADDRESS);
-        mNavigationDevice = generateCarAudioDeviceInfo(NAVIGATION_DEVICE_ADDRESS);
-    }
-
-    @Test
-    public void bind_associatesDeviceAddresses() {
-        CarVolumeGroup carVolumeGroup =
-                getVolumeGroupWithGainAndUser(2, UserHandle.USER_CURRENT);
-
-        carVolumeGroup.bind(CarAudioContext.MUSIC, mMediaDevice);
-        assertEquals(1, carVolumeGroup.getAddresses().size());
-
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, mNavigationDevice);
-
-        List<String> addresses = carVolumeGroup.getAddresses();
-        assertEquals(2, addresses.size());
-        assertTrue(addresses.contains(MEDIA_DEVICE_ADDRESS));
-        assertTrue(addresses.contains(NAVIGATION_DEVICE_ADDRESS));
-    }
-
-    @Test
-    public void bind_checksForSameStepSize() {
-        CarVolumeGroup carVolumeGroup =
-                getVolumeGroupWithGainAndUser(2, UserHandle.USER_CURRENT);
-
-        carVolumeGroup.bind(CarAudioContext.MUSIC, mMediaDevice);
-        CarAudioDeviceInfo differentStepValueDevice = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE + 1,
-                MIN_GAIN, MAX_GAIN);
-
-        IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
-                () -> carVolumeGroup.bind(CarAudioContext.NAVIGATION, differentStepValueDevice));
-        assertThat(thrown).hasMessageThat()
-                .contains("Gain controls within one group must have same step value");
-    }
-
-    @Test
-    public void bind_updatesMinGainToSmallestValue() {
-        CarVolumeGroup carVolumeGroup =
-                getVolumeGroupWithGainAndUser(2, UserHandle.USER_CURRENT);
-
-        CarAudioDeviceInfo largestMinGain = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, 1, 10, 10);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, largestMinGain);
-
-        assertEquals(0, carVolumeGroup.getMaxGainIndex());
-
-        CarAudioDeviceInfo smallestMinGain = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, 1, 2, 10);
-        carVolumeGroup.bind(CarAudioContext.NOTIFICATION, smallestMinGain);
-
-        assertEquals(8, carVolumeGroup.getMaxGainIndex());
-
-        CarAudioDeviceInfo middleMinGain = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, 1, 7, 10);
-        carVolumeGroup.bind(CarAudioContext.VOICE_COMMAND, middleMinGain);
-
-        assertEquals(8, carVolumeGroup.getMaxGainIndex());
-    }
-
-    @Test
-    public void bind_updatesMaxGainToLargestValue() {
-        CarVolumeGroup carVolumeGroup =
-                getVolumeGroupWithGainAndUser(2, UserHandle.USER_CURRENT);
-
-        CarAudioDeviceInfo smallestMaxGain = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, 1, 1, 5);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, smallestMaxGain);
-
-        assertEquals(4, carVolumeGroup.getMaxGainIndex());
-
-        CarAudioDeviceInfo largestMaxGain = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, 1, 1, 10);
-        carVolumeGroup.bind(CarAudioContext.NOTIFICATION, largestMaxGain);
-
-        assertEquals(9, carVolumeGroup.getMaxGainIndex());
-
-        CarAudioDeviceInfo middleMaxGain = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, 1, 1, 7);
-        carVolumeGroup.bind(CarAudioContext.VOICE_COMMAND, middleMaxGain);
-
-        assertEquals(9, carVolumeGroup.getMaxGainIndex());
-    }
-
-    @Test
-    public void bind_checksThatTheSameContextIsNotBoundTwice() {
-        CarVolumeGroup carVolumeGroup =
-                getVolumeGroupWithGainAndUser(2, UserHandle.USER_CURRENT);
-
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, mMediaDevice);
-
-        IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
-                () -> carVolumeGroup.bind(CarAudioContext.NAVIGATION, mMediaDevice));
-        assertThat(thrown).hasMessageThat()
-                .contains("Context NAVIGATION has already been bound to " + MEDIA_DEVICE_ADDRESS);
-    }
-
-    @Test
-    public void getContexts_returnsAllContextsBoundToVolumeGroup() {
-        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
-
-        int[] contexts = carVolumeGroup.getContexts();
-
-        assertEquals(6, contexts.length);
-
-        List<Integer> contextsList = Ints.asList(contexts);
-        assertTrue(contextsList.contains(CarAudioContext.MUSIC));
-        assertTrue(contextsList.contains(CarAudioContext.CALL));
-        assertTrue(contextsList.contains(CarAudioContext.CALL_RING));
-        assertTrue(contextsList.contains(CarAudioContext.NAVIGATION));
-        assertTrue(contextsList.contains(CarAudioContext.ALARM));
-        assertTrue(contextsList.contains(CarAudioContext.NOTIFICATION));
-    }
-
-    @Test
-    public void getContextsForAddress_returnsContextsBoundToThatAddress() {
-        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
-
-        List<Integer> contextsList = carVolumeGroup.getContextsForAddress(MEDIA_DEVICE_ADDRESS);
-
-        assertThat(contextsList).containsExactly(CarAudioContext.MUSIC,
-                CarAudioContext.CALL, CarAudioContext.CALL_RING);
-    }
-
-    @Test
-    public void getContextsForAddress_returnsEmptyArrayIfAddressNotBound() {
-        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
-
-        List<Integer> contextsList = carVolumeGroup.getContextsForAddress(OTHER_ADDRESS);
-
-        assertThat(contextsList).isEmpty();
-    }
-
-    @Test
-    public void getCarAudioDeviceInfoForAddress_returnsExpectedDevice() {
-        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
-
-        CarAudioDeviceInfo actualDevice = carVolumeGroup.getCarAudioDeviceInfoForAddress(
-                MEDIA_DEVICE_ADDRESS);
-
-        assertEquals(mMediaDevice, actualDevice);
-    }
-
-    @Test
-    public void getCarAudioDeviceInfoForAddress_returnsNullIfAddressNotBound() {
-        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
-
-        CarAudioDeviceInfo actualDevice = carVolumeGroup.getCarAudioDeviceInfoForAddress(
-                OTHER_ADDRESS);
-
-        assertNull(actualDevice);
-    }
-
-    @Test
-    public void setCurrentGainIndex_setsGainOnAllBoundDevices() {
-        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
-
-        carVolumeGroup.setCurrentGainIndex(2);
-        verify(mMediaDevice).setCurrentGain(4);
-        verify(mNavigationDevice).setCurrentGain(4);
-    }
-
-    @Test
-    public void setCurrentGainIndex_updatesCurrentGainIndex() {
-        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
-
-        carVolumeGroup.setCurrentGainIndex(2);
-
-        assertEquals(2, carVolumeGroup.getCurrentGainIndex());
-    }
-
-    @Test
-    public void setCurrentGainIndex_checksNewGainIsAboveMin() {
-        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
-
-        IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
-                () -> carVolumeGroup.setCurrentGainIndex(-1));
-        assertThat(thrown).hasMessageThat().contains("Gain out of range (0:5) -2index -1");
-    }
-
-    @Test
-    public void setCurrentGainIndex_checksNewGainIsBelowMax() {
-        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
-
-        IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
-                () -> carVolumeGroup.setCurrentGainIndex(3));
-        assertThat(thrown).hasMessageThat().contains("Gain out of range (0:5) 6index 3");
-    }
-
-    @Test
-    public void getMinGainIndex_alwaysReturnsZero() {
-        CarVolumeGroup carVolumeGroup =
-                getVolumeGroupWithGainAndUser(2, UserHandle.USER_CURRENT);
-        CarAudioDeviceInfo minGainPlusOneDevice = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, 10, MAX_GAIN);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, minGainPlusOneDevice);
-
-        assertEquals(0, carVolumeGroup.getMinGainIndex());
-
-        CarAudioDeviceInfo minGainDevice = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, 1, MAX_GAIN);
-        carVolumeGroup.bind(CarAudioContext.NOTIFICATION, minGainDevice);
-
-        assertEquals(0, carVolumeGroup.getMinGainIndex());
-    }
-
-    @Test
-    public void loadVolumesSettingsForUser_setsCurrentGainIndexForUser() {
-        CarAudioSettings settings = new CarVolumeGroupSettingsBuilder(0, 0)
-                .setGainIndexForUser(TEST_USER_10, 2)
-                .setGainIndexForUser(TEST_USER_11, 0)
-                .build();
-
-        CarVolumeGroup carVolumeGroup = new CarVolumeGroup(0, 0, settings, false);
-
-        CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
-        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
-
-        assertEquals(2, carVolumeGroup.getCurrentGainIndex());
-
-        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_11);
-
-        assertEquals(0, carVolumeGroup.getCurrentGainIndex());
-    }
-
-    @Test
-    public void loadVolumesSettingsForUser_setsCurrentGainIndexToDefault() {
-        CarAudioSettings settings = new CarVolumeGroupSettingsBuilder(0, 0)
-                .setGainIndexForUser(TEST_USER_10, 10)
-                .build();
-        CarVolumeGroup carVolumeGroup = new CarVolumeGroup(0, 0, settings, false);
-
-        CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
-
-        carVolumeGroup.setCurrentGainIndex(2);
-
-        assertEquals(2, carVolumeGroup.getCurrentGainIndex());
-
-        carVolumeGroup.loadVolumesSettingsForUser(0);
-
-        assertEquals(0, carVolumeGroup.getCurrentGainIndex());
-    }
-
-    @Test
-    public void setCurrentGainIndex_setsCurrentGainIndexForUser() {
-        CarAudioSettings settings = new CarVolumeGroupSettingsBuilder(0, 0)
-                .setGainIndexForUser(TEST_USER_11, 2)
-                .build();
-        CarVolumeGroup carVolumeGroup = new CarVolumeGroup(0, 0, settings, false);
-
-        CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
-        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_11);
-
-        carVolumeGroup.setCurrentGainIndex(MIN_GAIN);
-
-        verify(settings).storeVolumeGainIndexForUser(TEST_USER_11, 0, 0, MIN_GAIN);
-    }
-
-    @Test
-    public void setCurrentGainIndex_setsCurrentGainIndexForDefaultUser() {
-        CarAudioSettings settings = new CarVolumeGroupSettingsBuilder(0, 0)
-                .setGainIndexForUser(UserHandle.USER_CURRENT, 2)
-                .build();
-        CarVolumeGroup carVolumeGroup = new CarVolumeGroup(0, 0, settings, false);
-
-        CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
-
-        carVolumeGroup.setCurrentGainIndex(MIN_GAIN);
-
-        verify(settings)
-                .storeVolumeGainIndexForUser(UserHandle.USER_CURRENT, 0, 0, MIN_GAIN);
-    }
-
-    @Test
-    public void bind_setsCurrentGainIndexToStoredGainIndex() {
-        CarVolumeGroup carVolumeGroup =
-                getVolumeGroupWithGainAndUser(2, UserHandle.USER_CURRENT);
-
-        CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
-
-
-        assertEquals(2, carVolumeGroup.getCurrentGainIndex());
-    }
-
-    @Test
-    public void getAddressForContext_returnsExpectedDeviceAddress() {
-        CarVolumeGroup carVolumeGroup =
-                getVolumeGroupWithGainAndUser(2, UserHandle.USER_CURRENT);
-
-        carVolumeGroup.bind(CarAudioContext.MUSIC, mMediaDevice);
-
-        String mediaAddress = carVolumeGroup.getAddressForContext(CarAudioContext.MUSIC);
-
-        assertEquals(mMediaDevice.getAddress(), mediaAddress);
-    }
-
-    @Test
-    public void getAddressForContext_returnsNull() {
-        CarVolumeGroup carVolumeGroup =
-                getVolumeGroupWithGainAndUser(2, UserHandle.USER_CURRENT);
-
-        String nullAddress = carVolumeGroup.getAddressForContext(CarAudioContext.MUSIC);
-
-        assertNull(nullAddress);
-    }
-
-    @Test
-    public void isMuted_whenDefault_returnsFalse() {
-        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
-
-        assertThat(carVolumeGroup.isMuted()).isFalse();
-    }
-
-    @Test
-    public void isMuted_afterMuting_returnsTrue() {
-        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
-        carVolumeGroup.setMute(true);
-
-        assertThat(carVolumeGroup.isMuted()).isTrue();
-    }
-
-    @Test
-    public void isMuted_afterUnMuting_returnsFalse() {
-        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
-        carVolumeGroup.setMute(false);
-
-        assertThat(carVolumeGroup.isMuted()).isFalse();
-    }
-
-    @Test
-    public void setMute_withMutedState_storesValueToSetting() {
-        CarAudioSettings settings = new CarVolumeGroupSettingsBuilder(0, 0)
-                .setMuteForUser(TEST_USER_10, false)
-                .setIsPersistVolumeGroupEnabled(true)
-                .build();
-        CarVolumeGroup carVolumeGroup = new CarVolumeGroup(0, 0, settings, true);
-        CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
-        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
-
-        carVolumeGroup.setMute(true);
-
-        verify(settings)
-                .storeVolumeGroupMuteForUser(TEST_USER_10, 0, 0, true);
-    }
-
-    @Test
-    public void setMute_withUnMutedState_storesValueToSetting() {
-        CarAudioSettings settings = new CarVolumeGroupSettingsBuilder(0, 0)
-                .setMuteForUser(TEST_USER_10, false)
-                .setIsPersistVolumeGroupEnabled(true)
-                .build();
-        CarVolumeGroup carVolumeGroup = new CarVolumeGroup(0, 0, settings, true);
-        CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
-        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
-
-        carVolumeGroup.setMute(false);
-
-        verify(settings)
-                .storeVolumeGroupMuteForUser(TEST_USER_10, 0, 0, false);
-    }
-
-    @Test
-    public void loadVolumesSettingsForUser_withMutedState_loadsMuteStateForUser() {
-        CarVolumeGroup carVolumeGroup = getVolumeGroupWithMuteForUser(true, true,
-                TEST_USER_10);
-        CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
-
-        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
-
-        assertEquals(true, carVolumeGroup.isMuted());
-    }
-
-    @Test
-    public void loadVolumesSettingsForUser_withDisabledUseVolumeGroupMute_doesNotLoadMute() {
-        CarAudioSettings settings = new CarVolumeGroupSettingsBuilder(0, 0)
-                .setMuteForUser(TEST_USER_10, true)
-                .setIsPersistVolumeGroupEnabled(true)
-                .build();
-        CarVolumeGroup carVolumeGroup = new CarVolumeGroup(0, 0, settings, false);
-        CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
-
-        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
-
-        assertEquals(false, carVolumeGroup.isMuted());
-    }
-
-    @Test
-    public void loadVolumesSettingsForUser_withUnMutedState_loadsMuteStateForUser() {
-        CarVolumeGroup carVolumeGroup = getVolumeGroupWithMuteForUser(false, true,
-                TEST_USER_10);
-        CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
-
-        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
-
-        assertEquals(false, carVolumeGroup.isMuted());
-    }
-
-    @Test
-    public void loadVolumesSettingsForUser_withMutedStateAndNoPersist_returnsDefaultMuteState() {
-        CarVolumeGroup carVolumeGroup = getVolumeGroupWithMuteForUser(true, false,
-                TEST_USER_10);
-        CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
-                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
-
-        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
-
-        assertEquals(false, carVolumeGroup.isMuted());
-    }
-
-    CarVolumeGroup getVolumeGroupWithGainAndUser(int gain, @UserIdInt int userId) {
-        CarAudioSettings settings = new CarVolumeGroupSettingsBuilder(0, 0)
-                .setGainIndexForUser(userId, gain)
-                .build();
-        CarVolumeGroup carVolumeGroup = new CarVolumeGroup(0, 0, settings, false);
-
-        return carVolumeGroup;
-    }
-
-    CarVolumeGroup getVolumeGroupWithMuteForUser(boolean isMuted, boolean persistMute,
-            @UserIdInt int userId) {
-        CarAudioSettings settings = new CarVolumeGroupSettingsBuilder(0, 0)
-                .setMuteForUser(userId, isMuted)
-                .setIsPersistVolumeGroupEnabled(persistMute)
-                .build();
-        CarVolumeGroup carVolumeGroup = new CarVolumeGroup(0, 0, settings, true);
-
-        return carVolumeGroup;
-    }
-
-    private CarVolumeGroup testVolumeGroupSetup() {
-        CarVolumeGroup carVolumeGroup =
-                getVolumeGroupWithGainAndUser(2, UserHandle.USER_CURRENT);
-
-        carVolumeGroup.bind(CarAudioContext.MUSIC, mMediaDevice);
-        carVolumeGroup.bind(CarAudioContext.CALL, mMediaDevice);
-        carVolumeGroup.bind(CarAudioContext.CALL_RING, mMediaDevice);
-
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, mNavigationDevice);
-        carVolumeGroup.bind(CarAudioContext.ALARM, mNavigationDevice);
-        carVolumeGroup.bind(CarAudioContext.NOTIFICATION, mNavigationDevice);
-
-        return carVolumeGroup;
-    }
-
-    private CarAudioDeviceInfo generateCarAudioDeviceInfo(String address) {
-        return generateCarAudioDeviceInfo(address, STEP_VALUE, MIN_GAIN, MAX_GAIN);
-    }
-
-    private CarAudioDeviceInfo generateCarAudioDeviceInfo(String address, int stepValue,
-            int minGain, int maxGain) {
-        CarAudioDeviceInfo cadiMock = Mockito.mock(CarAudioDeviceInfo.class);
-        when(cadiMock.getStepValue()).thenReturn(stepValue);
-        when(cadiMock.getDefaultGain()).thenReturn(DEFAULT_GAIN);
-        when(cadiMock.getMaxGain()).thenReturn(maxGain);
-        when(cadiMock.getMinGain()).thenReturn(minGain);
-        when(cadiMock.getAddress()).thenReturn(address);
-        return cadiMock;
-    }
-
-    private static final class CarVolumeGroupSettingsBuilder {
-        private SparseIntArray mStoredGainIndexes = new SparseIntArray();
-        private SparseBooleanArray mStoreMuteStates = new SparseBooleanArray();
-        private boolean mPersistMute;
-        private final int mZoneId;
-        private final int mGroupId;
-
-        CarVolumeGroupSettingsBuilder(int zoneId, int groupId) {
-            mZoneId = zoneId;
-            mGroupId = groupId;
-        }
-
-        CarVolumeGroupSettingsBuilder setGainIndexForUser(@UserIdInt int userId, int gainIndex) {
-            mStoredGainIndexes.put(userId, gainIndex);
-            return this;
-        }
-
-        CarVolumeGroupSettingsBuilder setMuteForUser(@UserIdInt int userId, boolean mute) {
-            mStoreMuteStates.put(userId, mute);
-            return this;
-        }
-
-        CarVolumeGroupSettingsBuilder setIsPersistVolumeGroupEnabled(boolean persistMute) {
-            mPersistMute = persistMute;
-            return this;
-        }
-
-        CarAudioSettings build() {
-            CarAudioSettings settingsMock = Mockito.mock(CarAudioSettings.class);
-            for (int storeIndex = 0; storeIndex < mStoredGainIndexes.size(); storeIndex++) {
-                int gainUserId = mStoredGainIndexes.keyAt(storeIndex);
-                when(settingsMock
-                        .getStoredVolumeGainIndexForUser(gainUserId, mZoneId,
-                        mGroupId)).thenReturn(mStoredGainIndexes.get(gainUserId, DEFAULT_GAIN));
-            }
-            for (int muteIndex = 0; muteIndex < mStoreMuteStates.size(); muteIndex++) {
-                int muteUserId = mStoreMuteStates.keyAt(muteIndex);
-                when(settingsMock.getVolumeGroupMuteForUser(muteUserId, mZoneId, mGroupId))
-                        .thenReturn(mStoreMuteStates.get(muteUserId, false));
-                when(settingsMock.isPersistVolumeGroupMuteEnabled(muteUserId))
-                        .thenReturn(mPersistMute);
-            }
-            return settingsMock;
-        }
-    }
-}
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioContextTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioContextTest.java
index be2e6dd..88ac26c 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioContextTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioContextTest.java
@@ -27,8 +27,10 @@
 import static com.android.car.audio.CarAudioContext.INVALID;
 import static com.android.car.audio.CarAudioContext.MUSIC;
 import static com.android.car.audio.CarAudioContext.NAVIGATION;
+import static com.android.car.audio.CarAudioContext.isCriticalAudioContext;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.testng.Assert.assertThrows;
 
@@ -125,4 +127,38 @@
 
         assertThat(result).containsExactly(MUSIC, NAVIGATION, EMERGENCY);
     }
+
+    @Test
+    public void isCriticalAudioContext_forNonCritialContexts_returnsFalse() {
+        assertWithMessage("Non-critical context INVALID")
+                .that(isCriticalAudioContext(CarAudioContext.INVALID)).isFalse();
+        assertWithMessage("Non-critical context MUSIC")
+                .that(isCriticalAudioContext(CarAudioContext.MUSIC)).isFalse();
+        assertWithMessage("Non-critical context NAVIGATION")
+                .that(isCriticalAudioContext(CarAudioContext.NAVIGATION)).isFalse();
+        assertWithMessage("Non-critical context VOICE_COMMAND")
+                .that(isCriticalAudioContext(CarAudioContext.VOICE_COMMAND)).isFalse();
+        assertWithMessage("Non-critical context CALL_RING")
+                .that(isCriticalAudioContext(CarAudioContext.CALL_RING)).isFalse();
+        assertWithMessage("Non-critical context CALL")
+                .that(isCriticalAudioContext(CarAudioContext.CALL)).isFalse();
+        assertWithMessage("Non-critical context ALARM")
+                .that(isCriticalAudioContext(CarAudioContext.ALARM)).isFalse();
+        assertWithMessage("Non-critical context NOTIFICATION")
+                .that(isCriticalAudioContext(CarAudioContext.NOTIFICATION)).isFalse();
+        assertWithMessage("Non-critical context SYSTEM_SOUND")
+                .that(isCriticalAudioContext(CarAudioContext.SYSTEM_SOUND)).isFalse();
+        assertWithMessage("Non-critical context VEHICLE_STATUS")
+                .that(isCriticalAudioContext(CarAudioContext.VEHICLE_STATUS)).isFalse();
+        assertWithMessage("Non-critical context ANNOUNCEMENT")
+                .that(isCriticalAudioContext(CarAudioContext.ANNOUNCEMENT)).isFalse();
+    }
+
+    @Test
+    public void isCriticalAudioContext_forCriticalContexts_returnsTrue() {
+        assertWithMessage("Critical context EMERGENCY")
+                .that(isCriticalAudioContext(CarAudioContext.EMERGENCY)).isTrue();
+        assertWithMessage("Critical context SAFETY")
+                .that(isCriticalAudioContext(CarAudioContext.SAFETY)).isTrue();
+    }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPowerListenerTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPowerListenerTest.java
index 1002897..130bee3 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPowerListenerTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPowerListenerTest.java
@@ -67,7 +67,7 @@
 
         listener.startListeningForPolicyChanges();
 
-        verify(mMockCarAudioService).enableAudio();
+        verify(mMockCarAudioService).setAudioEnabled(true);
     }
 
     @Test
@@ -94,7 +94,7 @@
 
         listener.startListeningForPolicyChanges();
 
-        verify(mMockCarAudioService).enableAudio();
+        verify(mMockCarAudioService).setAudioEnabled(true);
     }
 
     @Test
@@ -105,7 +105,7 @@
 
         listener.startListeningForPolicyChanges();
 
-        verify(mMockCarAudioService).enableAudio();
+        verify(mMockCarAudioService).setAudioEnabled(true);
     }
 
     @Test
@@ -116,53 +116,53 @@
 
         listener.startListeningForPolicyChanges();
 
-        verify(mMockCarAudioService).disableAudio();
+        verify(mMockCarAudioService).setAudioEnabled(false);
     }
 
     @Test
     public void onPolicyChange_withPowerSwitchingToEnabled_enablesAudio() throws Exception {
         withAudioInitiallyDisabled();
         ICarPowerPolicyListener changeListener = registerAndGetChangeListener();
-        verify(mMockCarAudioService, never()).enableAudio();
+        verify(mMockCarAudioService, never()).setAudioEnabled(true);
 
         changeListener.onPolicyChanged(EMPTY_POLICY, ENABLED_POLICY);
 
-        verify(mMockCarAudioService).enableAudio();
+        verify(mMockCarAudioService).setAudioEnabled(true);
     }
 
     @Test
     public void onPolicyChange_withPowerRemainingEnabled_doesNothing() throws Exception {
         withAudioInitiallyEnabled();
         ICarPowerPolicyListener changeListener = registerAndGetChangeListener();
-        verify(mMockCarAudioService).enableAudio();
+        verify(mMockCarAudioService).setAudioEnabled(true);
 
         changeListener.onPolicyChanged(EMPTY_POLICY, ENABLED_POLICY);
 
-        verify(mMockCarAudioService).enableAudio();
-        verify(mMockCarAudioService, never()).disableAudio();
+        verify(mMockCarAudioService).setAudioEnabled(true);
+        verify(mMockCarAudioService, never()).setAudioEnabled(false);
     }
 
     @Test
     public void onPolicyChange_withPowerSwitchingToDisabled_disablesAudio() throws Exception {
         withAudioInitiallyEnabled();
         ICarPowerPolicyListener changeListener = registerAndGetChangeListener();
-        verify(mMockCarAudioService, never()).disableAudio();
+        verify(mMockCarAudioService, never()).setAudioEnabled(false);
 
         changeListener.onPolicyChanged(EMPTY_POLICY, DISABLED_POLICY);
 
-        verify(mMockCarAudioService).disableAudio();
+        verify(mMockCarAudioService).setAudioEnabled(false);
     }
 
     @Test
     public void onPolicyChange_withPowerStayingDisabled_doesNothing() throws Exception {
         withAudioInitiallyDisabled();
         ICarPowerPolicyListener changeListener = registerAndGetChangeListener();
-        verify(mMockCarAudioService).disableAudio();
+        verify(mMockCarAudioService).setAudioEnabled(false);
 
         changeListener.onPolicyChanged(EMPTY_POLICY, DISABLED_POLICY);
 
-        verify(mMockCarAudioService).disableAudio();
-        verify(mMockCarAudioService, never()).enableAudio();
+        verify(mMockCarAudioService).setAudioEnabled(false);
+        verify(mMockCarAudioService, never()).setAudioEnabled(true);
     }
 
     private void withAudioInitiallyEnabled() {
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupMutingTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupMutingTest.java
index 18bbbea..5c04921 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupMutingTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupMutingTest.java
@@ -16,8 +16,10 @@
 
 package com.android.car.audio;
 
+import static com.android.car.audio.CarAudioContext.EMERGENCY;
 import static com.android.car.audio.CarAudioContext.MUSIC;
 import static com.android.car.audio.CarAudioContext.NAVIGATION;
+import static com.android.car.audio.CarAudioContext.SAFETY;
 import static com.android.car.audio.CarAudioContext.VOICE_COMMAND;
 
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -48,8 +50,10 @@
     private static final String PRIMARY_MEDIA_ADDRESS = "media";
     private static final String PRIMARY_NAVIGATION_ADDRESS = "navigation";
     private static final String PRIMARY_VOICE_ADDRESS = "voice";
-    private static final String SECONDARY_ADDRESS = "media";
-    private static final String TERTIARY_ADDRESS = "media";
+    private static final String SECONDARY_ADDRESS = "secondary";
+    private static final String TERTIARY_ADDRESS = "tertiary";
+    private static final String EMERGENCY_ADDRESS = "emergency";
+    private static final String SAFETY_ADDRESS = "safety";
     private static final int PRIMARY_ZONE_ID = CarAudioManager.PRIMARY_AUDIO_ZONE;
     private static final int SECONDARY_ZONE_ID = CarAudioManager.PRIMARY_AUDIO_ZONE + 1;
     private static final int TERTIARY_ZONE_ID = CarAudioManager.PRIMARY_AUDIO_ZONE + 2;
@@ -70,21 +74,12 @@
 
     @Before
     public void setUp() {
-        mMusicCarVolumeGroup = new VolumeGroupBuilder()
-                .addDeviceAddressAndContexts(MUSIC, PRIMARY_MEDIA_ADDRESS)
-                .build();
-        mNavigationCarVolumeGroup = new VolumeGroupBuilder()
-                .addDeviceAddressAndContexts(NAVIGATION, PRIMARY_NAVIGATION_ADDRESS)
-                .build();
-        mVoiceCarVolumeGroup = new VolumeGroupBuilder()
-                .addDeviceAddressAndContexts(VOICE_COMMAND, PRIMARY_VOICE_ADDRESS)
-                .build();
-        mSecondaryZoneVolumeGroup = new VolumeGroupBuilder()
-                .addDeviceAddressAndContexts(MUSIC, SECONDARY_ADDRESS)
-                .build();
-        mTertiaryZoneVolumeGroup = new VolumeGroupBuilder()
-                .addDeviceAddressAndContexts(MUSIC, TERTIARY_ADDRESS)
-                .build();
+        mMusicCarVolumeGroup = groupWithContextAndAddress(MUSIC, PRIMARY_MEDIA_ADDRESS);
+        mNavigationCarVolumeGroup = groupWithContextAndAddress(NAVIGATION,
+                PRIMARY_NAVIGATION_ADDRESS);
+        mVoiceCarVolumeGroup = groupWithContextAndAddress(VOICE_COMMAND, PRIMARY_VOICE_ADDRESS);
+        mSecondaryZoneVolumeGroup = groupWithContextAndAddress(MUSIC, SECONDARY_ADDRESS);
+        mTertiaryZoneVolumeGroup = groupWithContextAndAddress(MUSIC, TERTIARY_ADDRESS);
 
         mPrimaryAudioZone =
                 new TestCarAudioZoneBuilder("Primary Zone", PRIMARY_ZONE_ID)
@@ -93,20 +88,14 @@
                         .addVolumeGroup(mVoiceCarVolumeGroup)
                         .build();
 
-        mSingleDevicePrimaryZone =
-                new TestCarAudioZoneBuilder("Primary Zone", PRIMARY_ZONE_ID)
-                        .addVolumeGroup(mMusicCarVolumeGroup)
-                        .build();
+        mSingleDevicePrimaryZone = createAudioZone(mMusicCarVolumeGroup, "Primary Zone",
+                PRIMARY_ZONE_ID);
 
-        mSingleDeviceSecondaryZone =
-                new TestCarAudioZoneBuilder("Secondary Zone", SECONDARY_ZONE_ID)
-                        .addVolumeGroup(mSecondaryZoneVolumeGroup)
-                        .build();
+        mSingleDeviceSecondaryZone = createAudioZone(mSecondaryZoneVolumeGroup, "Secondary Zone",
+                SECONDARY_ZONE_ID);
 
-        mSingleDeviceTertiaryZone =
-                new TestCarAudioZoneBuilder("Tertiary Zone", TERTIARY_ZONE_ID)
-                        .addVolumeGroup(mTertiaryZoneVolumeGroup)
-                        .build();
+        mSingleDeviceTertiaryZone = createAudioZone(mTertiaryZoneVolumeGroup, "Tertiary Zone",
+                TERTIARY_ZONE_ID);
 
         when(mMockAudioControlWrapper
                 .supportsFeature(AudioControlWrapper.AUDIOCONTROL_FEATURE_AUDIO_GROUP_MUTING))
@@ -234,7 +223,7 @@
 
         carGroupMuting.carMuteChanged();
 
-        List<MutingInfo> mutingInfo =  captureMutingInfoList();
+        List<MutingInfo> mutingInfo = captureMutingInfoList();
         MutingInfo info = mutingInfo.get(mutingInfo.size() - 1);
         assertWithMessage("Device addresses to un-mute")
                 .that(info.deviceAddressesToUnmute).asList().containsExactly(
@@ -331,6 +320,49 @@
         }
     }
 
+    @Test
+    public void setRestrictMuting_isMutingRestrictedTrue_mutesNonCriticalVolumeGroups() {
+        setUpCarVolumeGroupIsMuted(mSecondaryZoneVolumeGroup, false);
+        setUpCarVolumeGroupIsMuted(mMusicCarVolumeGroup, false);
+        setUpCarVolumeGroupIsMuted(mTertiaryZoneVolumeGroup, false);
+        CarVolumeGroupMuting carGroupMuting =
+                new CarVolumeGroupMuting(getAudioZones(mSingleDevicePrimaryZone,
+                        mSingleDeviceSecondaryZone, mSingleDeviceTertiaryZone),
+                        mMockAudioControlWrapper);
+
+        carGroupMuting.setRestrictMuting(true);
+
+        for (MutingInfo info : captureMutingInfoList()) {
+            assertWithMessage("Devices addresses to mute for zone %s", info.zoneId)
+                    .that(info.deviceAddressesToMute).asList().hasSize(1);
+        }
+    }
+
+    @Test
+    public void setRestrictMuting_isMutingRestrictedTrue_leavesCriticalGroupsAsIs() {
+        setUpCarVolumeGroupIsMuted(mMusicCarVolumeGroup, false);
+        setUpCarVolumeGroupHasCriticalAudioContexts(mMusicCarVolumeGroup);
+        setUpCarVolumeGroupIsMuted(mSecondaryZoneVolumeGroup, true);
+        setUpCarVolumeGroupHasCriticalAudioContexts(mSecondaryZoneVolumeGroup);
+        CarVolumeGroupMuting carGroupMuting = new CarVolumeGroupMuting(
+                getAudioZones(mSingleDevicePrimaryZone, mSingleDeviceSecondaryZone),
+                mMockAudioControlWrapper);
+
+        carGroupMuting.setRestrictMuting(true);
+
+        for (MutingInfo info : captureMutingInfoList()) {
+            if (info.zoneId == PRIMARY_ZONE_ID) {
+                assertWithMessage("Devices addresses to unmute for zone %s", info.zoneId)
+                        .that(info.deviceAddressesToUnmute).asList().containsExactly(
+                        PRIMARY_MEDIA_ADDRESS);
+
+            } else if (info.zoneId == SECONDARY_ZONE_ID) {
+                assertWithMessage("Devices addresses to mute for zone %s", info.zoneId)
+                        .that(info.deviceAddressesToMute).asList().containsExactly(
+                                SECONDARY_ADDRESS);
+            }
+        }
+    }
 
     @Test
     public void generateMutingInfoFromZone_withNoGroupsMuted_returnsEmptyMutedList() {
@@ -338,7 +370,8 @@
         setUpCarVolumeGroupIsMuted(mNavigationCarVolumeGroup, false);
         setUpCarVolumeGroupIsMuted(mVoiceCarVolumeGroup, false);
 
-        MutingInfo info = CarVolumeGroupMuting.generateMutingInfoFromZone(mPrimaryAudioZone);
+        MutingInfo info = CarVolumeGroupMuting.generateMutingInfoFromZone(mPrimaryAudioZone,
+                /* isMutingRestricted= */ false);
 
         assertWithMessage("Device addresses to mute")
                 .that(info.deviceAddressesToMute).asList().isEmpty();
@@ -350,7 +383,8 @@
         setUpCarVolumeGroupIsMuted(mNavigationCarVolumeGroup, false);
         setUpCarVolumeGroupIsMuted(mVoiceCarVolumeGroup, false);
 
-        MutingInfo info = CarVolumeGroupMuting.generateMutingInfoFromZone(mPrimaryAudioZone);
+        MutingInfo info = CarVolumeGroupMuting.generateMutingInfoFromZone(mPrimaryAudioZone,
+                /* isMutingRestricted= */ false);
 
         assertWithMessage("Device addresses to mute")
                 .that(info.deviceAddressesToMute).asList().containsExactly(PRIMARY_MEDIA_ADDRESS);
@@ -362,7 +396,8 @@
         setUpCarVolumeGroupIsMuted(mNavigationCarVolumeGroup, true);
         setUpCarVolumeGroupIsMuted(mVoiceCarVolumeGroup, true);
 
-        MutingInfo info = CarVolumeGroupMuting.generateMutingInfoFromZone(mPrimaryAudioZone);
+        MutingInfo info = CarVolumeGroupMuting.generateMutingInfoFromZone(mPrimaryAudioZone,
+                /* isMutingRestricted= */ false);
 
         assertWithMessage("Device addresses to mute")
                 .that(info.deviceAddressesToMute).asList().containsExactly(PRIMARY_MEDIA_ADDRESS,
@@ -371,17 +406,16 @@
 
     @Test
     public void generateMutingInfoFromZone_withMutedMultiDeviceGroup_returnsAllDevicesMuted() {
-        CarAudioZone primaryZone =
-                new TestCarAudioZoneBuilder("Primary Zone", PRIMARY_ZONE_ID)
-                        .addVolumeGroup(new VolumeGroupBuilder()
-                                .addDeviceAddressAndContexts(MUSIC, PRIMARY_MEDIA_ADDRESS)
-                                .addDeviceAddressAndContexts(VOICE_COMMAND, PRIMARY_VOICE_ADDRESS)
-                                .addDeviceAddressAndContexts(NAVIGATION, PRIMARY_NAVIGATION_ADDRESS)
-                                .setIsMuted(true)
-                                .build())
-                        .build();
+        CarAudioZone primaryZone = createAudioZone(
+                new VolumeGroupBuilder()
+                        .addDeviceAddressAndContexts(MUSIC, PRIMARY_MEDIA_ADDRESS)
+                        .addDeviceAddressAndContexts(VOICE_COMMAND, PRIMARY_VOICE_ADDRESS)
+                        .addDeviceAddressAndContexts(NAVIGATION, PRIMARY_NAVIGATION_ADDRESS)
+                        .setIsMuted(true)
+                        .build(), "Primary Zone", PRIMARY_ZONE_ID);
 
-        MutingInfo info = CarVolumeGroupMuting.generateMutingInfoFromZone(primaryZone);
+        MutingInfo info = CarVolumeGroupMuting.generateMutingInfoFromZone(primaryZone,
+                /* isMutingRestricted= */ false);
 
         assertWithMessage("Device addresses to mute")
                 .that(info.deviceAddressesToMute).asList().containsExactly(PRIMARY_MEDIA_ADDRESS,
@@ -390,35 +424,95 @@
 
     @Test
     public void generateMutingInfoFromZone_withUnMutedMultiDeviceGroup_returnsAllDevicesUnMuted() {
-        CarAudioZone primaryZone =
-                new TestCarAudioZoneBuilder("Primary Zone", PRIMARY_ZONE_ID)
-                        .addVolumeGroup(new VolumeGroupBuilder()
-                                .addDeviceAddressAndContexts(MUSIC, PRIMARY_MEDIA_ADDRESS)
-                                .addDeviceAddressAndContexts(VOICE_COMMAND, PRIMARY_VOICE_ADDRESS)
-                                .addDeviceAddressAndContexts(NAVIGATION, PRIMARY_NAVIGATION_ADDRESS)
-                                .build())
-                        .build();
+        CarAudioZone primaryZone = createAudioZone(
+                new VolumeGroupBuilder()
+                        .addDeviceAddressAndContexts(MUSIC, PRIMARY_MEDIA_ADDRESS)
+                        .addDeviceAddressAndContexts(VOICE_COMMAND, PRIMARY_VOICE_ADDRESS)
+                        .addDeviceAddressAndContexts(NAVIGATION, PRIMARY_NAVIGATION_ADDRESS)
+                        .build(), "Primary Zone", PRIMARY_ZONE_ID);
 
-        MutingInfo info = CarVolumeGroupMuting.generateMutingInfoFromZone(primaryZone);
+        MutingInfo info = CarVolumeGroupMuting.generateMutingInfoFromZone(primaryZone,
+                /* isMutingRestricted= */ false);
 
         assertWithMessage("Device addresses to un-mute")
                 .that(info.deviceAddressesToUnmute).asList().containsExactly(PRIMARY_MEDIA_ADDRESS,
                 PRIMARY_NAVIGATION_ADDRESS, PRIMARY_VOICE_ADDRESS);
     }
 
+    @Test
+    public void generateMutingInfoFromZone_mutingRestricted_mutesAllNonCriticalDevices() {
+        CarAudioZone primaryZone = createAudioZone(
+                new VolumeGroupBuilder()
+                        .addDeviceAddressAndContexts(MUSIC, PRIMARY_MEDIA_ADDRESS)
+                        .addDeviceAddressAndContexts(VOICE_COMMAND, PRIMARY_VOICE_ADDRESS)
+                        .addDeviceAddressAndContexts(NAVIGATION, PRIMARY_NAVIGATION_ADDRESS)
+                        .build(), "Primary Zone", PRIMARY_ZONE_ID);
+
+        MutingInfo info = CarVolumeGroupMuting.generateMutingInfoFromZone(primaryZone,
+                /* isMutingRestricted= */ true);
+
+        assertWithMessage("Device addresses to un-mute")
+                .that(info.deviceAddressesToMute).asList().containsExactly(PRIMARY_MEDIA_ADDRESS,
+                PRIMARY_NAVIGATION_ADDRESS, PRIMARY_VOICE_ADDRESS);
+    }
+
+    @Test
+    public void generateMutingInfoFromZone_mutingRestricted_setsAllCriticalGroupsToTheirState() {
+        CarAudioZone primaryZone =
+                new TestCarAudioZoneBuilder("Primary Zone", PRIMARY_ZONE_ID)
+                        .addVolumeGroup(new VolumeGroupBuilder()
+                                .addDeviceAddressAndContexts(EMERGENCY, EMERGENCY_ADDRESS)
+                                .addDeviceAddressAndContexts(VOICE_COMMAND, PRIMARY_VOICE_ADDRESS)
+                                .build())
+                        .addVolumeGroup(new VolumeGroupBuilder()
+                                .addDeviceAddressAndContexts(SAFETY, SAFETY_ADDRESS)
+                                .addDeviceAddressAndContexts(NAVIGATION, PRIMARY_NAVIGATION_ADDRESS)
+                                .setIsMuted(true)
+                                .build()
+                        )
+                        .build();
+        setUpCarVolumeGroupHasCriticalAudioContexts(primaryZone.getVolumeGroups()[0]);
+        setUpCarVolumeGroupHasCriticalAudioContexts(primaryZone.getVolumeGroups()[1]);
+
+        MutingInfo info = CarVolumeGroupMuting.generateMutingInfoFromZone(primaryZone,
+                /* isMutingRestricted= */ true);
+
+        assertWithMessage("Device addresses to mute")
+                .that(info.deviceAddressesToMute).asList()
+                .containsExactly(SAFETY_ADDRESS, PRIMARY_NAVIGATION_ADDRESS);
+        assertWithMessage("Device addresses to un-mute")
+                .that(info.deviceAddressesToUnmute).asList()
+                .containsExactly(EMERGENCY_ADDRESS, PRIMARY_VOICE_ADDRESS);
+    }
+
+
+    private CarAudioZone createAudioZone(CarVolumeGroup volumeGroup, String name, int zoneId) {
+        return new TestCarAudioZoneBuilder(name, zoneId)
+                .addVolumeGroup(volumeGroup)
+                .build();
+    }
+
+    private CarVolumeGroup groupWithContextAndAddress(int context, String address) {
+        return new VolumeGroupBuilder().addDeviceAddressAndContexts(context, address).build();
+    }
+
     private List<MutingInfo> captureMutingInfoList() {
         ArgumentCaptor<List<MutingInfo>> captor = ArgumentCaptor.forClass(List.class);
         verify(mMockAudioControlWrapper).onDevicesToMuteChange(captor.capture());
         return captor.getValue();
     }
 
-    private void setUpCarVolumeGroupIsMuted(CarVolumeGroup musicCarVolumeGroup, boolean muted) {
-        when(musicCarVolumeGroup.isMuted()).thenReturn(muted);
+    private void setUpCarVolumeGroupIsMuted(CarVolumeGroup carVolumeGroup, boolean muted) {
+        when(carVolumeGroup.isMuted()).thenReturn(muted);
     }
 
-    private SparseArray<CarAudioZone> getAudioZones(CarAudioZone ...zones) {
+    private void setUpCarVolumeGroupHasCriticalAudioContexts(CarVolumeGroup carVolumeGroup) {
+        when(carVolumeGroup.hasCriticalAudioContexts()).thenReturn(true);
+    }
+
+    private SparseArray<CarAudioZone> getAudioZones(CarAudioZone... zones) {
         SparseArray<CarAudioZone> audioZones = new SparseArray<>();
-        for (CarAudioZone zone: zones) {
+        for (CarAudioZone zone : zones) {
             audioZones.put(zone.getId(), zone);
         }
         return audioZones;
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupUnitTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupUnitTest.java
new file mode 100644
index 0000000..9e38583
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupUnitTest.java
@@ -0,0 +1,635 @@
+/*
+ * 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.
+ */
+
+package com.android.car.audio;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.expectThrows;
+
+import android.annotation.UserIdInt;
+import android.os.UserHandle;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.List;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CarVolumeGroupUnitTest {
+    private static final int ZONE_ID = 0;
+    private static final int GROUP_ID = 0;
+    private static final int STEP_VALUE = 2;
+    private static final int MIN_GAIN = 3;
+    private static final int MAX_GAIN = 10;
+    private static final int DEFAULT_GAIN = 5;
+    private static final int DEFAULT_GAIN_INDEX = (DEFAULT_GAIN - MIN_GAIN) / STEP_VALUE;
+    private static final int MIN_GAIN_INDEX = 0;
+    private static final int MAX_GAIN_INDEX = (MAX_GAIN - MIN_GAIN) / STEP_VALUE;
+    private static final int TEST_GAIN_INDEX = 2;
+    private static final int TEST_USER_10 = 10;
+    private static final int TEST_USER_11 = 11;
+    private static final String MEDIA_DEVICE_ADDRESS = "music";
+    private static final String NAVIGATION_DEVICE_ADDRESS = "navigation";
+    private static final String OTHER_ADDRESS = "other_address";
+
+    private CarAudioDeviceInfo mMediaDeviceInfo;
+    private CarAudioDeviceInfo mNavigationDeviceInfo;
+
+    @Mock
+    CarAudioSettings mSettingsMock;
+
+    @Before
+    public void setUp() {
+        mMediaDeviceInfo = new InfoBuilder(MEDIA_DEVICE_ADDRESS).build();
+        mNavigationDeviceInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS).build();
+    }
+
+    @Test
+    public void setDeviceInfoForContext_associatesDeviceAddresses() {
+        CarVolumeGroup.Builder builder = getBuilder();
+
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, mNavigationDeviceInfo);
+        CarVolumeGroup carVolumeGroup = builder.build();
+
+        assertThat(carVolumeGroup.getAddresses()).containsExactly(MEDIA_DEVICE_ADDRESS,
+                NAVIGATION_DEVICE_ADDRESS);
+    }
+
+    @Test
+    public void setDeviceInfoForContext_associatesContexts() {
+        CarVolumeGroup.Builder builder = getBuilder();
+
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, mNavigationDeviceInfo);
+        CarVolumeGroup carVolumeGroup = builder.build();
+
+        assertThat(carVolumeGroup.getContexts()).asList().containsExactly(CarAudioContext.MUSIC,
+                CarAudioContext.NAVIGATION);
+    }
+
+    @Test
+    public void setDeviceInfoForContext_withDifferentStepSize_throws() {
+        CarVolumeGroup.Builder builder = getBuilder();
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        CarAudioDeviceInfo differentStepValueDevice = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
+                .setStepValue(mMediaDeviceInfo.getStepValue() + 1).build();
+
+        IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
+                () -> builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION,
+                        differentStepValueDevice));
+
+        assertThat(thrown).hasMessageThat()
+                .contains("Gain controls within one group must have same step value");
+    }
+
+    @Test
+    public void setDeviceInfoForContext_withSameContext_throws() {
+        CarVolumeGroup.Builder builder = getBuilder();
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+
+        IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
+                () -> builder.setDeviceInfoForContext(CarAudioContext.MUSIC,
+                        mNavigationDeviceInfo));
+
+        assertThat(thrown).hasMessageThat()
+                .contains("has already been set to");
+    }
+
+    @Test
+    public void setDeviceInfoForContext_withFirstCall_setsMinGain() {
+        CarVolumeGroup.Builder builder = getBuilder();
+
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+
+        assertThat(builder.mMinGain).isEqualTo(mMediaDeviceInfo.getMinGain());
+    }
+
+    @Test
+    public void setDeviceInfoForContext_withFirstCall_setsMaxGain() {
+        CarVolumeGroup.Builder builder = getBuilder();
+
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+
+        assertThat(builder.mMaxGain).isEqualTo(mMediaDeviceInfo.getMaxGain());
+    }
+
+    @Test
+    public void setDeviceInfoForContext_withFirstCall_setsDefaultGain() {
+        CarVolumeGroup.Builder builder = getBuilder();
+
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+
+        assertThat(builder.mDefaultGain).isEqualTo(mMediaDeviceInfo.getDefaultGain());
+    }
+
+    @Test
+    public void setDeviceInfoForContext_SecondCallWithSmallerMinGain_updatesMinGain() {
+        CarVolumeGroup.Builder builder = getBuilder();
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
+                .setMinGain(mMediaDeviceInfo.getMinGain() - 1).build();
+
+        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+
+        assertThat(builder.mMinGain).isEqualTo(secondInfo.getMinGain());
+    }
+
+    @Test
+    public void setDeviceInfoForContext_SecondCallWithLargerMinGain_keepsFirstMinGain() {
+        CarVolumeGroup.Builder builder = getBuilder();
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
+                .setMinGain(mMediaDeviceInfo.getMinGain() + 1).build();
+
+        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+
+        assertThat(builder.mMinGain).isEqualTo(mMediaDeviceInfo.getMinGain());
+    }
+
+    @Test
+    public void setDeviceInfoForContext_SecondCallWithLargerMaxGain_updatesMaxGain() {
+        CarVolumeGroup.Builder builder = getBuilder();
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
+                .setMaxGain(mMediaDeviceInfo.getMaxGain() + 1).build();
+
+        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+
+        assertThat(builder.mMaxGain).isEqualTo(secondInfo.getMaxGain());
+    }
+
+    @Test
+    public void setDeviceInfoForContext_SecondCallWithSmallerMaxGain_keepsFirstMaxGain() {
+        CarVolumeGroup.Builder builder = getBuilder();
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
+                .setMaxGain(mMediaDeviceInfo.getMaxGain() - 1).build();
+
+        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+
+        assertThat(builder.mMaxGain).isEqualTo(mMediaDeviceInfo.getMaxGain());
+    }
+
+    @Test
+    public void setDeviceInfoForContext_SecondCallWithLargerDefaultGain_updatesDefaultGain() {
+        CarVolumeGroup.Builder builder = getBuilder();
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
+                .setDefaultGain(mMediaDeviceInfo.getDefaultGain() + 1).build();
+
+        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+
+        assertThat(builder.mDefaultGain).isEqualTo(secondInfo.getDefaultGain());
+    }
+
+    @Test
+    public void setDeviceInfoForContext_SecondCallWithSmallerDefaultGain_keepsFirstDefaultGain() {
+        CarVolumeGroup.Builder builder = getBuilder();
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
+                .setDefaultGain(mMediaDeviceInfo.getDefaultGain() - 1).build();
+
+        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+
+        assertThat(builder.mDefaultGain).isEqualTo(mMediaDeviceInfo.getDefaultGain());
+    }
+
+    @Test
+    public void builderBuild_withNoCallToSetDeviceInfoForContext_throws() {
+        CarVolumeGroup.Builder builder = getBuilder();
+
+        Exception e = expectThrows(IllegalArgumentException.class, builder::build);
+
+        assertThat(e).hasMessageThat().isEqualTo(
+                "setDeviceInfoForContext has to be called at least once before building");
+    }
+
+    @Test
+    public void builderBuild_withNoStoredGain_usesDefaultGain() {
+        CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(CarAudioContext.MUSIC,
+                mMediaDeviceInfo);
+        when(mSettingsMock.getStoredVolumeGainIndexForUser(UserHandle.USER_CURRENT, ZONE_ID,
+                GROUP_ID)).thenReturn(-1);
+
+
+        CarVolumeGroup carVolumeGroup = builder.build();
+
+        assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
+    }
+
+    @Test
+    public void builderBuild_withTooLargeStoredGain_usesDefaultGain() {
+        CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(CarAudioContext.MUSIC,
+                mMediaDeviceInfo);
+        when(mSettingsMock.getStoredVolumeGainIndexForUser(UserHandle.USER_CURRENT, ZONE_ID,
+                GROUP_ID)).thenReturn(MAX_GAIN_INDEX + 1);
+
+        CarVolumeGroup carVolumeGroup = builder.build();
+
+        assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
+    }
+
+    @Test
+    public void builderBuild_withTooSmallStoredGain_usesDefaultGain() {
+        CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(CarAudioContext.MUSIC,
+                mMediaDeviceInfo);
+        when(mSettingsMock.getStoredVolumeGainIndexForUser(UserHandle.USER_CURRENT, ZONE_ID,
+                GROUP_ID)).thenReturn(MIN_GAIN_INDEX - 1);
+
+        CarVolumeGroup carVolumeGroup = builder.build();
+
+        assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
+    }
+
+    @Test
+    public void builderBuild_withValidStoredGain_usesStoredGain() {
+        CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(CarAudioContext.MUSIC,
+                mMediaDeviceInfo);
+        when(mSettingsMock.getStoredVolumeGainIndexForUser(UserHandle.USER_CURRENT, ZONE_ID,
+                GROUP_ID)).thenReturn(MAX_GAIN_INDEX - 1);
+
+        CarVolumeGroup carVolumeGroup = builder.build();
+
+        assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(MAX_GAIN_INDEX - 1);
+    }
+
+    @Test
+    public void getAddressForContext_withSupportedContext_returnsAddress() {
+        CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
+
+        assertThat(carVolumeGroup.getAddressForContext(CarAudioContext.MUSIC))
+                .isEqualTo(mMediaDeviceInfo.getAddress());
+    }
+
+    @Test
+    public void getAddressForContext_withUnsupportedContext_returnsNull() {
+        CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
+
+        assertThat(carVolumeGroup.getAddressForContext(CarAudioContext.NAVIGATION)).isNull();
+    }
+
+    @Test
+    public void isMuted_whenDefault_returnsFalse() {
+        CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
+
+        assertThat(carVolumeGroup.isMuted()).isFalse();
+    }
+
+    @Test
+    public void isMuted_afterMuting_returnsTrue() {
+        CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
+
+        carVolumeGroup.setMute(true);
+
+        assertThat(carVolumeGroup.isMuted()).isTrue();
+    }
+
+    @Test
+    public void isMuted_afterUnMuting_returnsFalse() {
+        CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
+
+        carVolumeGroup.setMute(false);
+
+        assertThat(carVolumeGroup.isMuted()).isFalse();
+    }
+
+    @Test
+    public void setMute_withMutedState_storesValueToSetting() {
+        CarAudioSettings settings = new SettingsBuilder(0, 0)
+                .setMuteForUser10(false)
+                .setIsPersistVolumeGroupEnabled(true)
+                .build();
+        CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithNavigationBound(settings, true);
+        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
+
+        carVolumeGroup.setMute(true);
+
+        verify(settings)
+                .storeVolumeGroupMuteForUser(TEST_USER_10, 0, 0, true);
+    }
+
+    @Test
+    public void setMute_withUnMutedState_storesValueToSetting() {
+        CarAudioSettings settings = new SettingsBuilder(0, 0)
+                .setMuteForUser10(false)
+                .setIsPersistVolumeGroupEnabled(true)
+                .build();
+        CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithNavigationBound(settings, true);
+        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
+
+        carVolumeGroup.setMute(false);
+
+        verify(settings)
+                .storeVolumeGroupMuteForUser(TEST_USER_10, 0, 0, false);
+    }
+
+    @Test
+    public void getContextsForAddress_returnsContextsBoundToThatAddress() {
+        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
+
+        List<Integer> contextsList = carVolumeGroup.getContextsForAddress(MEDIA_DEVICE_ADDRESS);
+
+        assertThat(contextsList).containsExactly(CarAudioContext.MUSIC,
+                CarAudioContext.CALL, CarAudioContext.CALL_RING);
+    }
+
+    @Test
+    public void getContextsForAddress_returnsEmptyArrayIfAddressNotBound() {
+        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
+
+        List<Integer> contextsList = carVolumeGroup.getContextsForAddress(OTHER_ADDRESS);
+
+        assertThat(contextsList).isEmpty();
+    }
+
+    @Test
+    public void getCarAudioDeviceInfoForAddress_returnsExpectedDevice() {
+        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
+
+        CarAudioDeviceInfo actualDevice = carVolumeGroup.getCarAudioDeviceInfoForAddress(
+                MEDIA_DEVICE_ADDRESS);
+
+        assertThat(actualDevice).isEqualTo(mMediaDeviceInfo);
+    }
+
+    @Test
+    public void getCarAudioDeviceInfoForAddress_returnsNullIfAddressNotBound() {
+        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
+
+        CarAudioDeviceInfo actualDevice = carVolumeGroup.getCarAudioDeviceInfoForAddress(
+                OTHER_ADDRESS);
+
+        assertThat(actualDevice).isNull();
+    }
+
+    @Test
+    public void setCurrentGainIndex_setsGainOnAllBoundDevices() {
+        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
+
+        carVolumeGroup.setCurrentGainIndex(TEST_GAIN_INDEX);
+
+        verify(mMediaDeviceInfo).setCurrentGain(7);
+        verify(mNavigationDeviceInfo).setCurrentGain(7);
+    }
+
+    @Test
+    public void setCurrentGainIndex_updatesCurrentGainIndex() {
+        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
+
+        carVolumeGroup.setCurrentGainIndex(TEST_GAIN_INDEX);
+
+        assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(TEST_GAIN_INDEX);
+    }
+
+    @Test
+    public void setCurrentGainIndex_checksNewGainIsAboveMin() {
+        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
+
+        IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
+                () -> carVolumeGroup.setCurrentGainIndex(MIN_GAIN_INDEX - 1));
+        assertThat(thrown).hasMessageThat()
+                .contains("Gain out of range (" + MIN_GAIN + ":" + MAX_GAIN + ")");
+    }
+
+    @Test
+    public void setCurrentGainIndex_checksNewGainIsBelowMax() {
+        CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
+
+        IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
+                () -> carVolumeGroup.setCurrentGainIndex(MAX_GAIN_INDEX + 1));
+        assertThat(thrown).hasMessageThat()
+                .contains("Gain out of range (" + MIN_GAIN + ":" + MAX_GAIN + ")");
+    }
+
+    @Test
+    public void setCurrentGainIndex_setsCurrentGainIndexForUser() {
+        CarAudioSettings settings = new SettingsBuilder(0, 0)
+                .setGainIndexForUser(TEST_USER_11)
+                .build();
+        CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithNavigationBound(settings, false);
+        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_11);
+
+        carVolumeGroup.setCurrentGainIndex(MIN_GAIN);
+
+        verify(settings).storeVolumeGainIndexForUser(TEST_USER_11, 0, 0, MIN_GAIN);
+    }
+
+    @Test
+    public void setCurrentGainIndex_setsCurrentGainIndexForDefaultUser() {
+        CarAudioSettings settings = new SettingsBuilder(0, 0)
+                .setGainIndexForUser(UserHandle.USER_CURRENT)
+                .build();
+        CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithNavigationBound(settings, false);
+
+        carVolumeGroup.setCurrentGainIndex(MIN_GAIN);
+
+        verify(settings)
+                .storeVolumeGainIndexForUser(UserHandle.USER_CURRENT, 0, 0, MIN_GAIN);
+    }
+
+    @Test
+    public void loadVolumesSettingsForUser_withMutedState_loadsMuteStateForUser() {
+        CarVolumeGroup carVolumeGroup = getVolumeGroupWithMuteAndNavBound(true, true, true);
+
+        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
+
+        assertThat(carVolumeGroup.isMuted()).isTrue();
+    }
+
+    @Test
+    public void loadVolumesSettingsForUser_withDisabledUseVolumeGroupMute_doesNotLoadMute() {
+        CarVolumeGroup carVolumeGroup = getVolumeGroupWithMuteAndNavBound(true, true, false);
+
+        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
+
+        assertThat(carVolumeGroup.isMuted()).isFalse();
+    }
+
+    @Test
+    public void loadVolumesSettingsForUser_withUnMutedState_loadsMuteStateForUser() {
+        CarVolumeGroup carVolumeGroup = getVolumeGroupWithMuteAndNavBound(false, true, true);
+
+        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
+
+        assertThat(carVolumeGroup.isMuted()).isFalse();
+    }
+
+    @Test
+    public void loadVolumesSettingsForUser_withMutedStateAndNoPersist_returnsDefaultMuteState() {
+        CarVolumeGroup carVolumeGroup = getVolumeGroupWithMuteAndNavBound(true, false, true);
+
+        carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
+
+        assertThat(carVolumeGroup.isMuted()).isFalse();
+    }
+
+    @Test
+    public void hasCriticalAudioContexts_withoutCriticalContexts_returnsFalse() {
+        CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
+
+        assertThat(carVolumeGroup.hasCriticalAudioContexts()).isFalse();
+    }
+
+    @Test
+    public void hasCriticalAudioContexts_withCriticalContexts_returnsTrue() {
+        CarVolumeGroup carVolumeGroup = getBuilder()
+                .setDeviceInfoForContext(CarAudioContext.EMERGENCY, mMediaDeviceInfo)
+                .build();
+
+        assertThat(carVolumeGroup.hasCriticalAudioContexts()).isTrue();
+    }
+
+    private CarVolumeGroup getCarVolumeGroupWithMusicBound() {
+        return getBuilder()
+                .setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo)
+                .build();
+    }
+
+    private CarVolumeGroup getCarVolumeGroupWithNavigationBound(CarAudioSettings settings,
+            boolean useCarVolumeGroupMute) {
+        return new CarVolumeGroup.Builder(0, 0, settings, useCarVolumeGroupMute)
+                .setDeviceInfoForContext(CarAudioContext.NAVIGATION, mNavigationDeviceInfo)
+                .build();
+    }
+
+    CarVolumeGroup getVolumeGroupWithMuteAndNavBound(boolean isMuted, boolean persistMute,
+            boolean useCarVolumeGroupMute) {
+        CarAudioSettings settings = new SettingsBuilder(0, 0)
+                .setMuteForUser10(isMuted)
+                .setIsPersistVolumeGroupEnabled(persistMute)
+                .build();
+        return getCarVolumeGroupWithNavigationBound(settings, useCarVolumeGroupMute);
+    }
+
+    private CarVolumeGroup testVolumeGroupSetup() {
+        CarVolumeGroup.Builder builder = getBuilder();
+
+        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(CarAudioContext.CALL, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(CarAudioContext.CALL_RING, mMediaDeviceInfo);
+
+        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, mNavigationDeviceInfo);
+        builder.setDeviceInfoForContext(CarAudioContext.ALARM, mNavigationDeviceInfo);
+        builder.setDeviceInfoForContext(CarAudioContext.NOTIFICATION, mNavigationDeviceInfo);
+
+        return builder.build();
+    }
+
+    CarVolumeGroup.Builder getBuilder() {
+        return new CarVolumeGroup.Builder(ZONE_ID, GROUP_ID, mSettingsMock, true);
+    }
+
+    private static final class SettingsBuilder {
+        private final SparseIntArray mStoredGainIndexes = new SparseIntArray();
+        private final SparseBooleanArray mStoreMuteStates = new SparseBooleanArray();
+        private final int mZoneId;
+        private final int mGroupId;
+
+        private boolean mPersistMute;
+
+        SettingsBuilder(int zoneId, int groupId) {
+            mZoneId = zoneId;
+            mGroupId = groupId;
+        }
+
+        SettingsBuilder setGainIndexForUser(@UserIdInt int userId) {
+            mStoredGainIndexes.put(userId, TEST_GAIN_INDEX);
+            return this;
+        }
+
+        SettingsBuilder setMuteForUser10(boolean mute) {
+            mStoreMuteStates.put(CarVolumeGroupUnitTest.TEST_USER_10, mute);
+            return this;
+        }
+
+        SettingsBuilder setIsPersistVolumeGroupEnabled(boolean persistMute) {
+            mPersistMute = persistMute;
+            return this;
+        }
+
+        CarAudioSettings build() {
+            CarAudioSettings settingsMock = Mockito.mock(CarAudioSettings.class);
+            for (int storeIndex = 0; storeIndex < mStoredGainIndexes.size(); storeIndex++) {
+                int gainUserId = mStoredGainIndexes.keyAt(storeIndex);
+                when(settingsMock
+                        .getStoredVolumeGainIndexForUser(gainUserId, mZoneId,
+                                mGroupId)).thenReturn(
+                        mStoredGainIndexes.get(gainUserId, DEFAULT_GAIN));
+            }
+            for (int muteIndex = 0; muteIndex < mStoreMuteStates.size(); muteIndex++) {
+                int muteUserId = mStoreMuteStates.keyAt(muteIndex);
+                when(settingsMock.getVolumeGroupMuteForUser(muteUserId, mZoneId, mGroupId))
+                        .thenReturn(mStoreMuteStates.get(muteUserId, false));
+                when(settingsMock.isPersistVolumeGroupMuteEnabled(muteUserId))
+                        .thenReturn(mPersistMute);
+            }
+            return settingsMock;
+        }
+    }
+
+    private static final class InfoBuilder {
+        private final String mAddress;
+
+        private int mStepValue = STEP_VALUE;
+        private int mDefaultGain = DEFAULT_GAIN;
+        private int mMinGain = MIN_GAIN;
+        private int mMaxGain = MAX_GAIN;
+
+        InfoBuilder(String address) {
+            mAddress = address;
+        }
+
+        InfoBuilder setStepValue(int stepValue) {
+            mStepValue = stepValue;
+            return this;
+        }
+
+        InfoBuilder setDefaultGain(int defaultGain) {
+            mDefaultGain = defaultGain;
+            return this;
+        }
+
+        InfoBuilder setMinGain(int minGain) {
+            mMinGain = minGain;
+            return this;
+        }
+
+        InfoBuilder setMaxGain(int maxGain) {
+            mMaxGain = maxGain;
+            return this;
+        }
+
+        CarAudioDeviceInfo build() {
+            CarAudioDeviceInfo infoMock = Mockito.mock(CarAudioDeviceInfo.class);
+            when(infoMock.getStepValue()).thenReturn(mStepValue);
+            when(infoMock.getDefaultGain()).thenReturn(mDefaultGain);
+            when(infoMock.getMaxGain()).thenReturn(mMaxGain);
+            when(infoMock.getMinGain()).thenReturn(mMinGain);
+            when(infoMock.getAddress()).thenReturn(mAddress);
+            return infoMock;
+        }
+    }
+}