Merge "Implement display/voice interaction mediator" into sc-dev
diff --git a/car-admin-ui-lib/src/main/java/com/android/car/admin/ui/UserAvatarView.java b/car-admin-ui-lib/src/main/java/com/android/car/admin/ui/UserAvatarView.java
index a011e24..e4d2a15 100644
--- a/car-admin-ui-lib/src/main/java/com/android/car/admin/ui/UserAvatarView.java
+++ b/car-admin-ui-lib/src/main/java/com/android/car/admin/ui/UserAvatarView.java
@@ -135,4 +135,8 @@
         mDrawable.setIconDrawable(d);
         mDrawable.setBadgeIfManagedUser(getContext(), userId);
     }
+
+    public UserIconDrawable getUserIconDrawable() {
+        return mDrawable;
+    }
 }
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index d70e079..59cc88a 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -1064,7 +1064,6 @@
   public static final class CarSettings.Secure {
     field public static final String KEY_AUDIO_FOCUS_NAVIGATION_REJECTED_DURING_CALL = "android.car.KEY_AUDIO_FOCUS_NAVIGATION_REJECTED_DURING_CALL";
     field public static final String KEY_AUDIO_PERSIST_VOLUME_GROUP_MUTE_STATES = "android.car.KEY_AUDIO_PERSIST_VOLUME_GROUP_MUTE_STATES";
-    field public static final String KEY_CAR_VOLUME_KEY_EVENT_TIMEOUT_MS = "android.car.KEY_CAR_VOLUME_KEY_EVENT_TIMEOUT_MS";
     field public static final String KEY_ENABLE_INITIAL_NOTICE_SCREEN_TO_USER = "android.car.ENABLE_INITIAL_NOTICE_SCREEN_TO_USER";
     field public static final String KEY_SETUP_WIZARD_IN_PROGRESS = "android.car.SETUP_WIZARD_IN_PROGRESS";
   }
diff --git a/car-lib/src/android/car/settings/CarSettings.java b/car-lib/src/android/car/settings/CarSettings.java
index 94681e9..13c52e0 100644
--- a/car-lib/src/android/car/settings/CarSettings.java
+++ b/car-lib/src/android/car/settings/CarSettings.java
@@ -145,17 +145,6 @@
                 "android.car.KEY_AUDIO_FOCUS_NAVIGATION_REJECTED_DURING_CALL";
 
         /**
-         * Key to indicate the timeout in milliseconds while a car volume group will be considered
-         * active (after playback has stopped) for volume control changes during volume key events.
-         * <p>The value is an int between 0 ms and 3000 ms (i.e. 0 - 3 seconds)
-         *
-         * @hide
-         */
-        @SystemApi
-        public static final String KEY_CAR_VOLUME_KEY_EVENT_TIMEOUT_MS =
-                "android.car.KEY_CAR_VOLUME_KEY_EVENT_TIMEOUT_MS";
-
-        /**
          * Key to indicate if mute state should be persisted across boot cycles.
          * <p>The value is a boolean (1 or 0) where:
          * <ul>
diff --git a/cpp/evs/manager/1.1/HalDisplay.cpp b/cpp/evs/manager/1.1/HalDisplay.cpp
index b1e1aa6..64fa6cb 100644
--- a/cpp/evs/manager/1.1/HalDisplay.cpp
+++ b/cpp/evs/manager/1.1/HalDisplay.cpp
@@ -20,7 +20,7 @@
 
 #include <android-base/logging.h>
 #include <android-base/stringprintf.h>
-#include <ui/DisplayConfig.h>
+#include <ui/DisplayMode.h>
 #include <ui/DisplayState.h>
 
 using android::base::StringAppendF;
@@ -126,7 +126,7 @@
 
 std::string HalDisplay::toString(const char* indent) {
     std::string buffer;
-    android::DisplayConfig displayConfig;
+    android::ui::DisplayMode displayMode;
     android::ui::DisplayState displayState;
 
     if (mId == std::numeric_limits<int32_t>::min()) {
@@ -137,18 +137,18 @@
     }
 
     getDisplayInfo_1_1([&](auto& config, auto& state) {
-        displayConfig =
-            *(reinterpret_cast<const android::DisplayConfig*>(config.data()));
+        displayMode =
+            *(reinterpret_cast<const android::ui::DisplayMode*>(config.data()));
         displayState =
             *(reinterpret_cast<const android::ui::DisplayState*>(state.data()));
     });
 
     StringAppendF(&buffer, "%sWidth: %" PRId32 "\n",
-                           indent, displayConfig.resolution.getWidth());
+                           indent, displayMode.resolution.getWidth());
     StringAppendF(&buffer, "%sHeight: %" PRId32 "\n",
-                           indent, displayConfig.resolution.getHeight());
+                           indent, displayMode.resolution.getHeight());
     StringAppendF(&buffer, "%sRefresh rate: %f\n",
-                           indent, displayConfig.refreshRate);
+                           indent, displayMode.refreshRate);
     StringAppendF(&buffer, "%sRotation: %" PRId32 "\n",
                            indent, static_cast<int32_t>(displayState.orientation));
 
diff --git a/cpp/evs/sampleDriver/GlWrapper.cpp b/cpp/evs/sampleDriver/GlWrapper.cpp
index 59d677d..6373625 100644
--- a/cpp/evs/sampleDriver/GlWrapper.cpp
+++ b/cpp/evs/sampleDriver/GlWrapper.cpp
@@ -22,7 +22,7 @@
 
 #include <utility>
 
-#include <ui/DisplayConfig.h>
+#include <ui/DisplayMode.h>
 #include <ui/DisplayState.h>
 #include <ui/GraphicBuffer.h>
 
@@ -198,7 +198,7 @@
 
     // We will use the first display in the list as the primary.
     pWindowProxy->getDisplayInfo(displayId, [this](auto dpyConfig, auto dpyState) {
-        DisplayConfig *pConfig = (DisplayConfig*)dpyConfig.data();
+        ui::DisplayMode *pConfig = (ui::DisplayMode*)dpyConfig.data();
         mWidth = pConfig->resolution.getWidth();
         mHeight = pConfig->resolution.getHeight();
 
diff --git a/cpp/powerpolicy/product/sample_power_policy.xml b/cpp/powerpolicy/product/sample_power_policy.xml
index e79a01e..a5db24b 100644
--- a/cpp/powerpolicy/product/sample_power_policy.xml
+++ b/cpp/powerpolicy/product/sample_power_policy.xml
@@ -13,8 +13,6 @@
         <policyGroup id="basic_policy_group">
             <defaultPolicy state="WaitForVHAL" id="sample_policy_01"/>
             <defaultPolicy state="On" id="sample_policy_02"/>
-            <defaultPolicy state="DeepSleepEntry" id="sample_policy_03"/>
-            <defaultPolicy state="ShutdownStart" id="sample_policy_04"/>
         </policyGroup>
         <policyGroup id="no_default_policy">
             <noDefaultPolicy state="WaitForVHAL"/>
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index b7282c7..d16484a 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -48,6 +48,17 @@
           true, this will have no impact on persisting mute changes as mute changes will be based
           on individual volume group.-->
     <bool name="audioPersistMasterMuteState">true</bool>
+
+    <!--  Configuration to indicate the timeout in milliseconds while a car volume group will be
+          considered active for volume control changes during volume key events. The configuration
+          will be used as follows:
+          - The timeout will be used to determine if a playback (audio context associated with the
+            playback's audio usage) can still be considered for automatic volume selection after it
+            has stopped playing.
+          - The timeout will also be used as the pause duration required in between automatic volume
+            adjustments to change what user is adjusting. -->
+    <integer name="audioVolumeKeyEventTimeoutMs">3000</integer>
+
     <!-- Whether to block other audio while media audio is muted with display off. When set to true,
          other sounds cannot be played either while display is off. If false, only media is muted
          and other sounds can be still played. -->
diff --git a/service/src/com/android/car/audio/CarAudioPlaybackCallback.java b/service/src/com/android/car/audio/CarAudioPlaybackCallback.java
new file mode 100644
index 0000000..4d07aac
--- /dev/null
+++ b/service/src/com/android/car/audio/CarAudioPlaybackCallback.java
@@ -0,0 +1,147 @@
+/*
+ * 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.android.car.audio.CarAudioContext.AudioContext;
+import static com.android.car.audio.CarAudioService.SystemClockWrapper;
+import static com.android.car.audio.CarAudioUtils.hasExpired;
+
+import android.annotation.NonNull;
+import android.media.AudioManager;
+import android.media.AudioPlaybackConfiguration;
+import android.util.SparseLongArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+final class CarAudioPlaybackCallback extends AudioManager.AudioPlaybackCallback {
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private final SparseLongArray mContextStartTime = new SparseLongArray();
+    @GuardedBy("mLock")
+    private final Map<String, AudioPlaybackConfiguration> mLastActiveConfigs = new HashMap<>();
+    private final CarAudioZone mCarPrimaryAudioZone;
+    private final SystemClockWrapper mClock;
+    private final int mVolumeKeyEventTimeoutMs;
+
+    CarAudioPlaybackCallback(@NonNull CarAudioZone carPrimaryAudioZone,
+            @NonNull SystemClockWrapper clock,
+            int volumeKeyEventTimeoutMs) {
+        mCarPrimaryAudioZone = Objects.requireNonNull(carPrimaryAudioZone);
+        mClock = Objects.requireNonNull(clock);
+        mVolumeKeyEventTimeoutMs = Preconditions.checkArgumentNonnegative(volumeKeyEventTimeoutMs);
+    }
+
+    @Override
+    public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configurations) {
+        Map<String, AudioPlaybackConfiguration> newActiveConfigs =
+                filterNewActiveConfiguration(configurations);
+
+        synchronized (mLock) {
+            List<AudioPlaybackConfiguration> newlyInactiveConfigurations =
+                    getNewlyInactiveConfigurationsLocked(newActiveConfigs);
+
+            mLastActiveConfigs.clear();
+            mLastActiveConfigs.putAll(newActiveConfigs);
+
+            startTimersForContextThatBecameInactiveLocked(newlyInactiveConfigurations);
+        }
+    }
+
+    /**
+     * Returns all active contexts for the primary zone
+     * @return all active audio contexts, including those that recently became inactive but are
+     * considered active due to the audio playback timeout.
+     */
+    public List<Integer> getAllActiveContextsForPrimaryZone() {
+        synchronized (mLock) {
+            List<Integer> activeContexts = getCurrentlyActiveContextsLocked();
+            activeContexts
+                    .addAll(getStillActiveContextAndRemoveExpiredContextsLocked());
+            return activeContexts;
+        }
+    }
+
+    private void startTimersForContextThatBecameInactiveLocked(
+            List<AudioPlaybackConfiguration> inactiveConfigs) {
+        List<Integer> activeContexts = mCarPrimaryAudioZone
+                .findActiveContextsFromPlaybackConfigurations(inactiveConfigs);
+
+        for (int activeContext : activeContexts) {
+            mContextStartTime.put(activeContext, mClock.uptimeMillis());
+        }
+    }
+
+    private List<AudioPlaybackConfiguration> getNewlyInactiveConfigurationsLocked(
+            Map<String, AudioPlaybackConfiguration> newActiveConfigurations) {
+        List<AudioPlaybackConfiguration> newlyInactiveConfigurations = new ArrayList<>();
+        for (String address : mLastActiveConfigs.keySet()) {
+            if (newActiveConfigurations.containsKey(address)) {
+                continue;
+            }
+            newlyInactiveConfigurations.add(mLastActiveConfigs.get(address));
+        }
+        return newlyInactiveConfigurations;
+    }
+
+    private Map<String, AudioPlaybackConfiguration> filterNewActiveConfiguration(
+            List<AudioPlaybackConfiguration> configurations) {
+        Map<String, AudioPlaybackConfiguration> newActiveConfigs = new HashMap<>();
+        for (int index = 0; index < configurations.size(); index++) {
+            AudioPlaybackConfiguration configuration = configurations.get(index);
+            if (!configuration.isActive()) {
+                continue;
+            }
+            if (mCarPrimaryAudioZone
+                    .isAudioDeviceInfoValidForZone(configuration.getAudioDeviceInfo())) {
+                newActiveConfigs.put(
+                        configuration.getAudioDeviceInfo().getAddress(), configuration);
+            }
+        }
+        return newActiveConfigs;
+    }
+
+    private List<Integer> getCurrentlyActiveContextsLocked() {
+        return mCarPrimaryAudioZone.findActiveContextsFromPlaybackConfigurations(
+                new ArrayList<>(mLastActiveConfigs.values()));
+    }
+
+    private List<Integer> getStillActiveContextAndRemoveExpiredContextsLocked() {
+        List<Integer> contextsToRemove = new ArrayList<>();
+        List<Integer> stillActiveContexts = new ArrayList<>();
+        for (int index = 0; index < mContextStartTime.size(); index++) {
+            @AudioContext int context = mContextStartTime.keyAt(index);
+            if (hasExpired(mContextStartTime.valueAt(index),
+                    mClock.uptimeMillis(), mVolumeKeyEventTimeoutMs)) {
+                contextsToRemove.add(context);
+                continue;
+            }
+            stillActiveContexts.add(context);
+        }
+
+        for (int indexToRemove = 0; indexToRemove < contextsToRemove.size(); indexToRemove++) {
+            mContextStartTime.delete(contextsToRemove.get(indexToRemove));
+        }
+        return stillActiveContexts;
+    }
+}
diff --git a/service/src/com/android/car/audio/CarAudioService.java b/service/src/com/android/car/audio/CarAudioService.java
index fe887be..7b7fc59 100644
--- a/service/src/com/android/car/audio/CarAudioService.java
+++ b/service/src/com/android/car/audio/CarAudioService.java
@@ -19,6 +19,10 @@
 import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_MUTING;
 import static android.car.media.CarAudioManager.CarAudioFeature;
 import static android.car.media.CarAudioManager.INVALID_VOLUME_GROUP_ID;
+import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
+
+import static com.android.car.audio.hal.AudioControlWrapper.AUDIOCONTROL_FEATURE_AUDIO_DUCKING;
+import static com.android.car.audio.hal.AudioControlWrapper.AUDIOCONTROL_FEATURE_AUDIO_FOCUS;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -52,6 +56,7 @@
 import android.media.audiopolicy.AudioPolicy;
 import android.os.IBinder;
 import android.os.Looper;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.telephony.Annotation.CallState;
 import android.telephony.TelephonyManager;
@@ -136,6 +141,7 @@
     private final boolean mPersistMasterMuteState;
     private final CarAudioSettings mCarAudioSettings;
     private final CarVolume mCarVolume;
+    private final int mKeyEventTimeoutMs;
     private AudioControlWrapper mAudioControlWrapper;
     private CarDucking mCarDucking;
     private HalAudioFocus mHalAudioFocus;
@@ -146,52 +152,56 @@
 
     private final AudioPolicy.AudioPolicyVolumeCallback mAudioPolicyVolumeCallback =
             new AudioPolicy.AudioPolicyVolumeCallback() {
-        @Override
-        public void onVolumeAdjustment(int adjustment) {
-            int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
-            @AudioContext int suggestedContext = getSuggestedAudioContext(zoneId);
+                @Override
+                public void onVolumeAdjustment(int adjustment) {
+                    @AudioContext int suggestedContext = getSuggestedAudioContextForPrimaryZone();
 
-            int groupId;
-            synchronized (mImplLock) {
-                groupId = getVolumeGroupIdForAudioContextLocked(zoneId, suggestedContext);
-            }
+                    int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
+                    int groupId;
+                    synchronized (mImplLock) {
+                        groupId = getVolumeGroupIdForAudioContext(zoneId, suggestedContext);
+                    }
 
-            if (Log.isLoggable(CarLog.TAG_AUDIO, Log.VERBOSE)) {
-                Slog.v(CarLog.TAG_AUDIO, "onVolumeAdjustment: "
-                        + AudioManager.adjustToString(adjustment) + " suggested audio context: "
-                        + CarAudioContext.toString(suggestedContext) + " suggested volume group: "
-                        + groupId);
-            }
+                    if (Log.isLoggable(CarLog.TAG_AUDIO, Log.VERBOSE)) {
+                        Slog.v(CarLog.TAG_AUDIO, "onVolumeAdjustment: "
+                                + AudioManager.adjustToString(adjustment)
+                                + " suggested audio context: "
+                                + CarAudioContext.toString(suggestedContext)
+                                + " suggested volume group: "
+                                + groupId);
+                    }
 
-            final int currentVolume = getGroupVolume(zoneId, groupId);
-            final int flags = AudioManager.FLAG_FROM_KEY | AudioManager.FLAG_SHOW_UI;
-            switch (adjustment) {
-                case AudioManager.ADJUST_LOWER:
-                    int minValue = Math.max(currentVolume - 1, getGroupMinVolume(zoneId, groupId));
-                    setGroupVolume(zoneId, groupId, minValue , flags);
-                    break;
-                case AudioManager.ADJUST_RAISE:
-                    int maxValue =  Math.min(currentVolume + 1, getGroupMaxVolume(zoneId, groupId));
-                    setGroupVolume(zoneId, groupId, maxValue, flags);
-                    break;
-                case AudioManager.ADJUST_MUTE:
-                    setMasterMute(true, flags);
-                    callbackMasterMuteChange(zoneId, flags);
-                    break;
-                case AudioManager.ADJUST_UNMUTE:
-                    setMasterMute(false, flags);
-                    callbackMasterMuteChange(zoneId, flags);
-                    break;
-                case AudioManager.ADJUST_TOGGLE_MUTE:
-                    setMasterMute(!mAudioManager.isMasterMute(), flags);
-                    callbackMasterMuteChange(zoneId, flags);
-                    break;
-                case AudioManager.ADJUST_SAME:
-                default:
-                    break;
-            }
-        }
-    };
+                    final int currentVolume = getGroupVolume(zoneId, groupId);
+                    final int flags = AudioManager.FLAG_FROM_KEY | AudioManager.FLAG_SHOW_UI;
+                    switch (adjustment) {
+                        case AudioManager.ADJUST_LOWER:
+                            int minValue =
+                                    Math.max(currentVolume - 1, getGroupMinVolume(zoneId, groupId));
+                            setGroupVolume(zoneId, groupId, minValue , flags);
+                            break;
+                        case AudioManager.ADJUST_RAISE:
+                            int maxValue =
+                                    Math.min(currentVolume + 1, getGroupMaxVolume(zoneId, groupId));
+                            setGroupVolume(zoneId, groupId, maxValue, flags);
+                            break;
+                        case AudioManager.ADJUST_MUTE:
+                            setMasterMute(true, flags);
+                            callbackMasterMuteChange(zoneId, flags);
+                            break;
+                        case AudioManager.ADJUST_UNMUTE:
+                            setMasterMute(false, flags);
+                            callbackMasterMuteChange(zoneId, flags);
+                            break;
+                        case AudioManager.ADJUST_TOGGLE_MUTE:
+                            setMasterMute(!mAudioManager.isMasterMute(), flags);
+                            callbackMasterMuteChange(zoneId, flags);
+                            break;
+                        case AudioManager.ADJUST_SAME:
+                        default:
+                            break;
+                    }
+                }
+            };
 
     /**
      * Simulates {@link ICarVolumeCallback} when it's running in legacy mode.
@@ -226,6 +236,7 @@
     private SparseArray<CarAudioZone> mCarAudioZones;
     private final CarVolumeCallbackHandler mCarVolumeCallbackHandler;
     private final SparseIntArray mAudioZoneIdToUserIdMapping;
+    private final SystemClockWrapper mClock = new SystemClockWrapper();
 
 
     // TODO do not store uid mapping here instead use the uid
@@ -233,6 +244,7 @@
     private Map<Integer, Integer> mUidToZoneMap;
     private OccupantZoneConfigChangeListener
             mOccupantZoneConfigChangeListener = new CarAudioOccupantConfigChangeListener();
+    private CarAudioPlaybackCallback mCarAudioPlaybackCallback;
 
     public CarAudioService(Context context) {
         mContext = context;
@@ -243,13 +255,16 @@
                 R.bool.audioUseCarVolumeGroupMuting);
         mPersistMasterMuteState = !mUseCarVolumeGroupMuting && mContext.getResources().getBoolean(
                 R.bool.audioPersistMasterMuteState);
+        mKeyEventTimeoutMs =
+                mContext.getResources().getInteger(R.integer.audioVolumeKeyEventTimeoutMs);
         mUidToZoneMap = new HashMap<>();
         mCarVolumeCallbackHandler = new CarVolumeCallbackHandler();
         mCarAudioSettings = new CarAudioSettings(mContext.getContentResolver());
         mAudioZoneIdToUserIdMapping = new SparseIntArray();
         mAudioVolumeAdjustmentContextsVersion =
                 mContext.getResources().getInteger(R.integer.audioVolumeAdjustmentContextsVersion);
-        mCarVolume = new CarVolume(mAudioVolumeAdjustmentContextsVersion);
+        mCarVolume = new CarVolume(mClock,
+                mAudioVolumeAdjustmentContextsVersion, mKeyEventTimeoutMs);
     }
 
     /**
@@ -265,6 +280,7 @@
             if (mUseDynamicRouting) {
                 setupDynamicRoutingLocked();
                 setupHalAudioFocusListenerLocked();
+                setupAudioConfigurationCallbackLocked();
             } else {
                 Slog.i(CarLog.TAG_AUDIO, "Audio dynamic routing not enabled, run in legacy mode");
                 setupLegacyVolumeChangedListener();
@@ -324,6 +340,7 @@
         writer.printf("Master muted? %b\n", mAudioManager.isMasterMute());
         writer.printf("Volume context priority list version: %d\n",
                 mAudioVolumeAdjustmentContextsVersion);
+        writer.printf("Volume key event timeout ms: %d\n", mKeyEventTimeoutMs);
         if (mCarAudioConfigurationPath != null) {
             writer.printf("Car audio configuration path: %s\n", mCarAudioConfigurationPath);
         }
@@ -584,7 +601,10 @@
         builder.setAudioPolicyVolumeCallback(mAudioPolicyVolumeCallback);
 
         if (sUseCarAudioFocus) {
-            mCarDucking = new CarDucking(mCarAudioZones);
+            AudioControlWrapper audioControlWrapper = getAudioControlWrapperLocked();
+            if (audioControlWrapper.supportsFeature(AUDIOCONTROL_FEATURE_AUDIO_DUCKING)) {
+                mCarDucking = new CarDucking(mCarAudioZones);
+            }
 
             // Configure our AudioPolicy to handle focus events.
             // This gives us the ability to decide which audio focus requests to accept and bypasses
@@ -613,6 +633,13 @@
         setupOccupantZoneInfo();
     }
 
+    private void setupAudioConfigurationCallbackLocked() {
+        mCarAudioPlaybackCallback =
+                new CarAudioPlaybackCallback(getCarAudioZone(PRIMARY_AUDIO_ZONE),
+                        mClock, mKeyEventTimeoutMs);
+        mAudioManager.registerAudioPlaybackCallback(mCarAudioPlaybackCallback, null);
+    }
+
     private void setupOccupantZoneInfo() {
         CarOccupantZoneService occupantZoneService;
         CarOccupantZoneManager occupantZoneManager;
@@ -630,7 +657,7 @@
 
     private void setupHalAudioFocusListenerLocked() {
         AudioControlWrapper audioControlWrapper = getAudioControlWrapperLocked();
-        if (!audioControlWrapper.supportsHalAudioFocus()) {
+        if (!audioControlWrapper.supportsFeature(AUDIOCONTROL_FEATURE_AUDIO_FOCUS)) {
             Slog.d(CarLog.TAG_AUDIO, "HalAudioFocus is not supported on this device");
             return;
         }
@@ -1234,10 +1261,11 @@
         return group.getAudioDevicePortForContext(CarAudioContext.getContextForUsage(usage));
     }
 
-    private @AudioContext int getSuggestedAudioContext(int zoneId) {
-        return mCarVolume.getSuggestedAudioContext(
-                getActiveContextsFromPlaybackConfigurations(zoneId),
-                getCallStateForZone(zoneId), getActiveHalUsagesForZone(zoneId));
+    @AudioContext int getSuggestedAudioContextForPrimaryZone() {
+        int zoneId = PRIMARY_AUDIO_ZONE;
+        return mCarVolume.getSuggestedAudioContextAndSaveIfFound(
+                getAllActiveContextsForPrimaryZone(), getCallStateForZone(zoneId),
+                getActiveHalUsagesForZone(zoneId));
     }
 
     private int[] getActiveHalUsagesForZone(int zoneId) {
@@ -1407,6 +1435,12 @@
                 "Invalid audio zone Id " + zoneId);
     }
 
+    int getVolumeGroupIdForAudioContext(int zoneId, int suggestedContext) {
+        synchronized (mImplLock) {
+            return getVolumeGroupIdForAudioContextLocked(zoneId, suggestedContext);
+        }
+    }
+
     private class CarAudioOccupantConfigChangeListener implements OccupantZoneConfigChangeListener {
         @Override
         public void onOccupantZoneConfigChanged(int flags) {
@@ -1422,4 +1456,16 @@
             }
         }
     }
+
+    private List<Integer> getAllActiveContextsForPrimaryZone() {
+        synchronized (mImplLock) {
+            return mCarAudioPlaybackCallback.getAllActiveContextsForPrimaryZone();
+        }
+    }
+
+    static final class SystemClockWrapper {
+        public long uptimeMillis() {
+            return SystemClock.uptimeMillis();
+        }
+    }
 }
diff --git a/service/src/com/android/car/audio/CarAudioUtils.java b/service/src/com/android/car/audio/CarAudioUtils.java
new file mode 100644
index 0000000..6724156
--- /dev/null
+++ b/service/src/com/android/car/audio/CarAudioUtils.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+class CarAudioUtils {
+    static boolean hasExpired(long startTimeMs, long currentTimeMs, int timeoutMs) {
+        return (currentTimeMs - startTimeMs) > timeoutMs;
+    }
+}
diff --git a/service/src/com/android/car/audio/CarAudioZone.java b/service/src/com/android/car/audio/CarAudioZone.java
index d7ae54d..e207aef 100644
--- a/service/src/com/android/car/audio/CarAudioZone.java
+++ b/service/src/com/android/car/audio/CarAudioZone.java
@@ -213,11 +213,7 @@
         for (int index = 0; index < configurations.size(); index++) {
             AudioPlaybackConfiguration configuration = configurations.get(index);
             if (configuration.isActive()) {
-                AudioDeviceInfo info = configuration.getAudioDeviceInfo();
-                if (info == null || info.getAddress() == null || info.getAddress().isEmpty()) {
-                    continue;
-                }
-                if (mDeviceAddresses.contains(info.getAddress())) {
+                if (isAudioDeviceInfoValidForZone(configuration.getAudioDeviceInfo())) {
                     // Note that address's context and the context actually supplied could be
                     // different
                     activeContexts.add(CarAudioContext.getContextForUsage(
@@ -227,4 +223,15 @@
         }
         return activeContexts;
     }
+
+    boolean isAudioDeviceInfoValidForZone(AudioDeviceInfo info) {
+        return info != null
+                && info.getAddress() != null
+                && !info.getAddress().isEmpty()
+                && containsDeviceAddress(info.getAddress());
+    }
+
+    private boolean containsDeviceAddress(String deviceAddress) {
+        return mDeviceAddresses.contains(deviceAddress);
+    }
 }
diff --git a/service/src/com/android/car/audio/CarDucking.java b/service/src/com/android/car/audio/CarDucking.java
index c10370a..690e39d 100644
--- a/service/src/com/android/car/audio/CarDucking.java
+++ b/service/src/com/android/car/audio/CarDucking.java
@@ -17,7 +17,6 @@
 package com.android.car.audio;
 
 import android.annotation.NonNull;
-import android.media.AudioAttributes;
 import android.media.AudioFocusInfo;
 import android.util.IndentingPrintWriter;
 import android.util.SparseArray;
@@ -28,7 +27,6 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 
 final class CarDucking implements CarFocusCallback {
     private static final String TAG = CarDucking.class.getSimpleName();
@@ -86,37 +84,4 @@
 
         return new CarDuckingInfo(zoneId, addressesToDuck, addressesToUnduck, usagesHoldingFocus);
     }
-
-    @VisibleForTesting
-    static final class CarDuckingInfo {
-        final int mZoneId;
-        final List<String> mAddressesToDuck;
-        final List<String> mAddressesToUnduck;
-        final int[] mUsagesHoldingFocus;
-
-        CarDuckingInfo(int zoneId, List<String> addressesToDuck, List<String> addressesToUnduck,
-                int[] usagesHoldingFocus) {
-            mZoneId = zoneId;
-            mAddressesToDuck = Objects.requireNonNull(addressesToDuck);
-            mAddressesToUnduck = Objects.requireNonNull(addressesToUnduck);
-            mUsagesHoldingFocus = usagesHoldingFocus;
-        }
-
-        void dump(IndentingPrintWriter writer) {
-            writer.printf("Ducking Info for zone %d \n", mZoneId);
-            writer.increaseIndent();
-            writer.printf("Addresses to duck: %s\n",
-                    String.join(", ", mAddressesToDuck));
-            writer.printf("Addresses to unduck: %s\n",
-                    String.join(", ", mAddressesToUnduck));
-            writer.println("Usages holding focus:");
-            writer.increaseIndent();
-            for (int usage : mUsagesHoldingFocus) {
-                writer.printf("%s, ", AudioAttributes.usageToXsdString(usage));
-            }
-            writer.decreaseIndent();
-            writer.println();
-            writer.decreaseIndent();
-        }
-    }
 }
diff --git a/service/src/com/android/car/audio/CarDuckingInfo.java b/service/src/com/android/car/audio/CarDuckingInfo.java
new file mode 100644
index 0000000..12c2aa5
--- /dev/null
+++ b/service/src/com/android/car/audio/CarDuckingInfo.java
@@ -0,0 +1,79 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.hardware.automotive.audiocontrol.DuckingInfo;
+import android.media.AudioAttributes;
+import android.util.IndentingPrintWriter;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Ducking information for a given car audio zone based on its focus state.
+ */
+public final class CarDuckingInfo {
+    public final int mZoneId;
+    public final List<String> mAddressesToDuck;
+    public final List<String> mAddressesToUnduck;
+    public final int[] mUsagesHoldingFocus;
+
+    public CarDuckingInfo(int zoneId, @NonNull List<String> addressesToDuck,
+            @NonNull List<String> addressesToUnduck, @NonNull int[] usagesHoldingFocus) {
+        mZoneId = zoneId;
+        mAddressesToDuck = Objects.requireNonNull(addressesToDuck);
+        mAddressesToUnduck = Objects.requireNonNull(addressesToUnduck);
+        mUsagesHoldingFocus = Objects.requireNonNull(usagesHoldingFocus);
+    }
+
+    /**
+     * Creates {@link DuckingInfo} instance from contents of {@link CarDuckingInfo}.
+     *
+     * <p>Converts usages to XSD strings as part of this process.
+     */
+    public DuckingInfo generateDuckingInfo() {
+        DuckingInfo duckingInfo = new DuckingInfo();
+        duckingInfo.zoneId = mZoneId;
+        duckingInfo.deviceAddressesToDuck = mAddressesToDuck.toArray(new String[0]);
+        duckingInfo.deviceAddressesToUnduck = mAddressesToUnduck.toArray(new String[0]);
+        String[] usageStrings = new String[mUsagesHoldingFocus.length];
+        for (int i = 0; i < mUsagesHoldingFocus.length; i++) {
+            usageStrings[i] = AudioAttributes.usageToXsdString(mUsagesHoldingFocus[i]);
+        }
+        duckingInfo.usagesHoldingFocus = usageStrings;
+
+        return duckingInfo;
+    }
+
+    void dump(IndentingPrintWriter writer) {
+        writer.printf("Ducking Info for zone %d \n", mZoneId);
+        writer.increaseIndent();
+        writer.printf("Addresses to duck: %s\n",
+                String.join(", ", mAddressesToDuck));
+        writer.printf("Addresses to unduck: %s\n",
+                String.join(", ", mAddressesToUnduck));
+        writer.println("Usages holding focus:");
+        writer.increaseIndent();
+        for (int usage : mUsagesHoldingFocus) {
+            writer.printf("%s, ", AudioAttributes.usageToXsdString(usage));
+        }
+        writer.decreaseIndent();
+        writer.println();
+        writer.decreaseIndent();
+    }
+}
diff --git a/service/src/com/android/car/audio/CarVolume.java b/service/src/com/android/car/audio/CarVolume.java
index 4eb297f..6a9dd90 100644
--- a/service/src/com/android/car/audio/CarVolume.java
+++ b/service/src/com/android/car/audio/CarVolume.java
@@ -17,6 +17,8 @@
 package com.android.car.audio;
 
 import static com.android.car.audio.CarAudioService.DEFAULT_AUDIO_CONTEXT;
+import static com.android.car.audio.CarAudioService.SystemClockWrapper;
+import static com.android.car.audio.CarAudioUtils.hasExpired;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -27,6 +29,7 @@
 import android.util.SparseIntArray;
 
 import com.android.car.audio.CarAudioContext.AudioContext;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -39,8 +42,7 @@
  * CarVolume is responsible for determining which audio contexts to prioritize when adjusting volume
  */
 final class CarVolume {
-    private static final String TAG = CarVolume.class.getSimpleName();
-    private static final int CONTEXT_HEIGHEST_PRIORITY = 0;
+    private static final int CONTEXT_HIGHEST_PRIORITY = 0;
     private static final int CONTEXT_NOT_PRIORITIZED = -1;
 
     private static final int VERSION_ONE = 1;
@@ -70,51 +72,76 @@
     };
 
     private final SparseIntArray mVolumePriorityByAudioContext = new SparseIntArray();
+    private final SystemClockWrapper mClock;
+    private final Object mLock = new Object();
+    private final int mVolumeKeyEventTimeoutMs;
+    private final int mLowestPriority;
+    @GuardedBy("mLock")
+    @AudioContext private int mLastActiveContext;
+    @GuardedBy("mLock")
+    private long mLastActiveContextStartTime;
+
 
     /**
-     * Creates a car volume with the specify context priority
-     *
-     * @param contextVolumePriority car volume priority of {#code @AudioContext}'s,
-     * arranged in priority from highest first to lowest.
+     * Creates car volume for management of volume priority and last selected audio context.
+     * @param clockWrapper time keeper for expiration of last selected context.
+     * @param audioVolumeAdjustmentContextsVersion audio priority list version number, can be
+     *      any version defined in {@link CarVolumeListVersion}
+     * @param volumeKeyEventTimeoutMs timeout in ms used to measure expiration of last selected
+     *      context
      */
-    CarVolume(@NonNull @AudioContext int[] contextVolumePriority) {
-        Objects.requireNonNull(contextVolumePriority);
-        for (int priority = CONTEXT_HEIGHEST_PRIORITY;
-                priority < contextVolumePriority.length; priority++) {
-            mVolumePriorityByAudioContext.append(contextVolumePriority[priority], priority);
-        }
-    }
-
-    /**
-     * Creates a car volume with the specify context priority version number
-     *
-     * @param audioVolumeAdjustmentContextsVersion car volume priority list version number.
-     */
-    CarVolume(@CarVolumeListVersion int audioVolumeAdjustmentContextsVersion) {
+    CarVolume(@NonNull SystemClockWrapper clockWrapper,
+            @CarVolumeListVersion int audioVolumeAdjustmentContextsVersion,
+            int volumeKeyEventTimeoutMs) {
         Preconditions.checkArgumentInRange(audioVolumeAdjustmentContextsVersion, 1, 2,
                 "audioVolumeAdjustmentContextsVersion");
+        mClock = Objects.requireNonNull(clockWrapper);
+        mVolumeKeyEventTimeoutMs = Preconditions.checkArgumentNonnegative(volumeKeyEventTimeoutMs);
+        mLastActiveContext = CarAudioContext.INVALID;
+        mLastActiveContextStartTime = mClock.uptimeMillis();
         @AudioContext int[] contextVolumePriority = AUDIO_CONTEXT_VOLUME_PRIORITY_V1;
         if (audioVolumeAdjustmentContextsVersion == VERSION_TWO) {
             contextVolumePriority = AUDIO_CONTEXT_VOLUME_PRIORITY_V2;
         }
-        for (int priority = CONTEXT_HEIGHEST_PRIORITY;
+        for (int priority = CONTEXT_HIGHEST_PRIORITY;
                 priority < contextVolumePriority.length; priority++) {
             mVolumePriorityByAudioContext.append(contextVolumePriority[priority], priority);
         }
+        mLowestPriority = CONTEXT_HIGHEST_PRIORITY + mVolumePriorityByAudioContext.size();
     }
 
     /**
-     * Suggests a {@link AudioContext} that should be adjusted based on the current
-     * {@link AudioPlaybackConfiguration}s, {@link CallState}, and active HAL usages
+     * Finds a {@link AudioContext} that should be adjusted based on the current
+     * {@link AudioPlaybackConfiguration}s, {@link CallState}, and active HAL usages. If an active
+     * context is found it be will saved and retrieved later on.
      */
-    @AudioContext int getSuggestedAudioContext(@NonNull List<Integer> activePlaybackContexts,
-            @CallState int callState, @NonNull @AttributeUsage int[] activeHalUsages) {
-        int currentContext = DEFAULT_AUDIO_CONTEXT;
-        int currentPriority = CONTEXT_HEIGHEST_PRIORITY + mVolumePriorityByAudioContext.size();
+    @AudioContext int getSuggestedAudioContextAndSaveIfFound(
+            @NonNull List<Integer> activePlaybackContexts, @CallState int callState,
+            @NonNull @AttributeUsage int[] activeHalUsages) {
+
+        int activeContext = getAudioContextStillActive();
+        if (activeContext != CarAudioContext.INVALID) {
+            setAudioContextStillActive(activeContext);
+            return activeContext;
+        }
 
         Set<Integer> activeContexts = getActiveContexts(activePlaybackContexts, callState,
                 activeHalUsages);
 
+
+        @AudioContext int context =
+                findActiveContextWithHighestPriority(activeContexts);
+
+        setAudioContextStillActive(context);
+
+        return context;
+    }
+
+    private @AudioContext int findActiveContextWithHighestPriority(
+            Set<Integer> activeContexts) {
+        int currentContext = DEFAULT_AUDIO_CONTEXT;
+        int currentPriority = mLowestPriority;
+
         for (@AudioContext int context : activeContexts) {
             int priority = mVolumePriorityByAudioContext.get(context, CONTEXT_NOT_PRIORITIZED);
             if (priority == CONTEXT_NOT_PRIORITIZED) {
@@ -124,9 +151,9 @@
             if (priority < currentPriority) {
                 currentContext = context;
                 currentPriority = priority;
-                // If the highest priority has been found, return early.
-                if (currentPriority == CONTEXT_HEIGHEST_PRIORITY) {
-                    return currentContext;
+                // If the highest priority has been found, break early.
+                if (currentPriority == CONTEXT_HIGHEST_PRIORITY) {
+                    break;
                 }
             }
         }
@@ -134,6 +161,13 @@
         return currentContext;
     }
 
+    private void setAudioContextStillActive(@AudioContext int context) {
+        synchronized (mLock) {
+            mLastActiveContext = context;
+            mLastActiveContextStartTime = mClock.uptimeMillis();
+        }
+    }
+
     public static boolean isAnyContextActive(@NonNull @AudioContext int [] contexts,
             @NonNull List<Integer> activePlaybackContext, @CallState int callState,
             @NonNull @AttributeUsage int[] activeHalUsages) {
@@ -170,6 +204,25 @@
         return contexts;
     }
 
+    private @AudioContext int getAudioContextStillActive() {
+        @AudioContext int context;
+        long contextStartTime;
+        synchronized (mLock) {
+            context = mLastActiveContext;
+            contextStartTime = mLastActiveContextStartTime;
+        }
+
+        if (context == CarAudioContext.INVALID) {
+            return CarAudioContext.INVALID;
+        }
+
+        if (hasExpired(contextStartTime, mClock.uptimeMillis(), mVolumeKeyEventTimeoutMs)) {
+            return CarAudioContext.INVALID;
+        }
+
+        return context;
+    }
+
     @IntDef({
             VERSION_ONE,
             VERSION_TWO
diff --git a/service/src/com/android/car/audio/CarZonesAudioFocus.java b/service/src/com/android/car/audio/CarZonesAudioFocus.java
index 444b832..5bf5c99 100644
--- a/service/src/com/android/car/audio/CarZonesAudioFocus.java
+++ b/service/src/com/android/car/audio/CarZonesAudioFocus.java
@@ -58,14 +58,14 @@
             @NonNull SparseArray<CarAudioZone> carAudioZones,
             @NonNull CarAudioSettings carAudioSettings,
             boolean enableDelayedAudioFocus,
-            @NonNull CarFocusCallback carFocusCallback) {
+            CarFocusCallback carFocusCallback) {
         //Create the zones here, the policy will be set setOwningPolicy,
         // which is called right after this constructor.
         Objects.requireNonNull(audioManager);
         Objects.requireNonNull(packageManager);
         Objects.requireNonNull(carAudioZones);
         Objects.requireNonNull(carAudioSettings);
-        mCarFocusCallback = Objects.requireNonNull(carFocusCallback);
+        mCarFocusCallback = carFocusCallback;
         Preconditions.checkArgument(carAudioZones.size() != 0,
                 "There must be a minimum of one audio zone");
 
@@ -199,6 +199,9 @@
     }
 
     private void notifyFocusCallback(int audioZoneId) {
+        if (mCarFocusCallback == null) {
+            return;
+        }
         List<AudioFocusInfo> focusHolders = mFocusZones.get(audioZoneId).getAudioFocusHolders();
         mCarFocusCallback.onFocusChange(audioZoneId, focusHolders);
     }
diff --git a/service/src/com/android/car/audio/hal/AudioControlWrapper.java b/service/src/com/android/car/audio/hal/AudioControlWrapper.java
index e2becb5..67a01f3 100644
--- a/service/src/com/android/car/audio/hal/AudioControlWrapper.java
+++ b/service/src/com/android/car/audio/hal/AudioControlWrapper.java
@@ -16,15 +16,32 @@
 
 package com.android.car.audio.hal;
 
+import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.media.AudioAttributes.AttributeUsage;
 import android.util.IndentingPrintWriter;
 
+import com.android.car.audio.CarDuckingInfo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * AudioControlWrapper wraps IAudioControl HAL interface, handling version specific support so that
  * the rest of CarAudioService doesn't need to know about it.
  */
 public interface AudioControlWrapper {
+    int AUDIOCONTROL_FEATURE_AUDIO_FOCUS = 0;
+    int AUDIOCONTROL_FEATURE_AUDIO_DUCKING = 1;
+
+    @IntDef({
+            AUDIOCONTROL_FEATURE_AUDIO_FOCUS,
+            AUDIOCONTROL_FEATURE_AUDIO_DUCKING
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface AudioControlFeature {
+    }
 
     /**
      * Closes the focus listener that's registered on the AudioControl HAL
@@ -32,9 +49,13 @@
     void unregisterFocusListener();
 
     /**
-     * Indicates if HAL can support making and abandoning audio focus requests.
+     * Indicates if HAL can support specified feature
+     *
+     * @param feature to check support for. it's expected to be one of the features defined by
+     * {@link AudioControlWrapper.AudioControlFeature}.
+     * @return boolean indicating whether feature is supported
      */
-    boolean supportsHalAudioFocus();
+    boolean supportsFeature(@AudioControlFeature int feature);
 
     /**
      * Registers listener for HAL audio focus requests with IAudioControl. Only works if
@@ -75,7 +96,16 @@
     void setBalanceTowardRight(float value);
 
     /**
+     * Notifies HAL of changes in usages holding focus and the corresponding ducking changes for a
+     * given zone.
+     *
+     * @param carDuckingInfo information about focus and addresses to duck to relay to the HAL.
+     */
+    void onDevicesToDuckChange(@NonNull CarDuckingInfo carDuckingInfo);
+
+    /**
      * Registers recipient to be notified if AudioControl HAL service dies.
+     *
      * @param deathRecipient to be notified upon HAL service death.
      */
     void linkToDeath(@Nullable AudioControlDeathRecipient deathRecipient);
diff --git a/service/src/com/android/car/audio/hal/AudioControlWrapperAidl.java b/service/src/com/android/car/audio/hal/AudioControlWrapperAidl.java
index ddcf790..656de9a 100644
--- a/service/src/com/android/car/audio/hal/AudioControlWrapperAidl.java
+++ b/service/src/com/android/car/audio/hal/AudioControlWrapperAidl.java
@@ -16,7 +16,9 @@
 
 package com.android.car.audio.hal;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.hardware.automotive.audiocontrol.DuckingInfo;
 import android.hardware.automotive.audiocontrol.IAudioControl;
 import android.hardware.automotive.audiocontrol.IFocusListener;
 import android.media.AudioAttributes;
@@ -29,6 +31,8 @@
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.car.audio.CarDuckingInfo;
+
 import java.util.Objects;
 
 /**
@@ -45,12 +49,8 @@
     private AudioControlDeathRecipient mDeathRecipient;
 
     static @Nullable IBinder getService() {
-        IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService(
+        return Binder.allowBlocking(ServiceManager.waitForDeclaredService(
                 AUDIO_CONTROL_SERVICE));
-        if (binder != null) {
-            return binder;
-        }
-        return null;
     }
 
     AudioControlWrapperAidl(IBinder binder) {
@@ -64,8 +64,9 @@
     }
 
     @Override
-    public boolean supportsHalAudioFocus() {
-        return true;
+    public boolean supportsFeature(int feature) {
+        return feature == AUDIOCONTROL_FEATURE_AUDIO_FOCUS
+                || feature == AUDIOCONTROL_FEATURE_AUDIO_DUCKING;
     }
 
     @Override
@@ -102,6 +103,13 @@
         writer.println("*AudioControlWrapperAidl*");
         writer.increaseIndent();
         writer.printf("Focus listener registered on HAL? %b\n", mListenerRegistered);
+
+        writer.println("Supported Features");
+        writer.increaseIndent();
+        writer.println("- AUDIOCONTROL_FEATURE_AUDIO_FOCUS");
+        writer.println("- AUDIOCONTROL_FEATURE_AUDIO_DUCKING");
+        writer.decreaseIndent();
+
         writer.decreaseIndent();
     }
 
@@ -124,6 +132,19 @@
     }
 
     @Override
+    public void onDevicesToDuckChange(@NonNull CarDuckingInfo carDuckingInfo) {
+        Objects.requireNonNull(carDuckingInfo);
+        DuckingInfo duckingInfo = carDuckingInfo.generateDuckingInfo();
+
+        try {
+            mAudioControl.onDevicesToDuckChange(new DuckingInfo[] {duckingInfo});
+        } catch (RemoteException e) {
+            Slog.e(TAG, "onDevicesToDuckChange for zone " + carDuckingInfo.mZoneId
+                    + " failed", e);
+        }
+    }
+
+    @Override
     public void linkToDeath(@Nullable AudioControlDeathRecipient deathRecipient) {
         try {
             mBinder.linkToDeath(this::binderDied, 0);
@@ -150,7 +171,7 @@
         }
     }
 
-    private final class FocusListenerWrapper extends IFocusListener.Stub {
+    private static final class FocusListenerWrapper extends IFocusListener.Stub {
         private final HalFocusListener mListener;
 
         FocusListenerWrapper(HalFocusListener halFocusListener) {
@@ -158,14 +179,13 @@
         }
 
         @Override
-        public void requestAudioFocus(String usage, int zoneId, int focusGain)
-                throws RemoteException {
+        public void requestAudioFocus(String usage, int zoneId, int focusGain) {
             @AttributeUsage int usageValue = AudioAttributes.xsdStringToUsage(usage);
             mListener.requestAudioFocus(usageValue, zoneId, focusGain);
         }
 
         @Override
-        public void abandonAudioFocus(String usage, int zoneId) throws RemoteException {
+        public void abandonAudioFocus(String usage, int zoneId) {
             @AttributeUsage int usageValue = AudioAttributes.xsdStringToUsage(usage);
             mListener.abandonAudioFocus(usageValue, zoneId);
         }
diff --git a/service/src/com/android/car/audio/hal/AudioControlWrapperV1.java b/service/src/com/android/car/audio/hal/AudioControlWrapperV1.java
index a213ba2..ab66608 100644
--- a/service/src/com/android/car/audio/hal/AudioControlWrapperV1.java
+++ b/service/src/com/android/car/audio/hal/AudioControlWrapperV1.java
@@ -23,6 +23,7 @@
 import android.util.Slog;
 
 import com.android.car.audio.CarAudioContext;
+import com.android.car.audio.CarDuckingInfo;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.NoSuchElementException;
@@ -56,11 +57,6 @@
     }
 
     @Override
-    public boolean supportsHalAudioFocus() {
-        return false;
-    }
-
-    @Override
     public void registerFocusListener(HalFocusListener focusListener) {
         throw new UnsupportedOperationException(
                 "Focus listener is unsupported for IAudioControl@1.0");
@@ -73,6 +69,11 @@
     }
 
     @Override
+    public boolean supportsFeature(int feature) {
+        return false;
+    }
+
+    @Override
     public void onAudioFocusChange(int usage, int zoneId, int focusChange) {
         throw new UnsupportedOperationException(
                 "Focus listener is unsupported for IAudioControl@1.0");
@@ -81,6 +82,7 @@
     @Override
     public void dump(IndentingPrintWriter writer) {
         writer.println("*AudioControlWrapperV1*");
+        writer.println("Supported Features - none");
     }
 
     @Override
@@ -101,6 +103,11 @@
         }
     }
 
+    @Override
+    public void onDevicesToDuckChange(CarDuckingInfo carDuckingInfo) {
+        throw new UnsupportedOperationException("HAL ducking is unsupported for IAudioControl@1.0");
+    }
+
     /**
      * Gets the bus associated with CarAudioContext.
      *
diff --git a/service/src/com/android/car/audio/hal/AudioControlWrapperV2.java b/service/src/com/android/car/audio/hal/AudioControlWrapperV2.java
index 5b28fd4..15d9daa 100644
--- a/service/src/com/android/car/audio/hal/AudioControlWrapperV2.java
+++ b/service/src/com/android/car/audio/hal/AudioControlWrapperV2.java
@@ -27,6 +27,8 @@
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.car.audio.CarDuckingInfo;
+
 import java.util.NoSuchElementException;
 import java.util.Objects;
 
@@ -69,8 +71,11 @@
     }
 
     @Override
-    public boolean supportsHalAudioFocus() {
-        return true;
+    public boolean supportsFeature(int feature) {
+        if (feature == AUDIOCONTROL_FEATURE_AUDIO_FOCUS) {
+            return true;
+        }
+        return false;
     }
 
     @Override
@@ -103,6 +108,12 @@
         writer.println("*AudioControlWrapperV2*");
         writer.increaseIndent();
         writer.printf("Focus listener registered on HAL? %b\n", (mCloseHandle != null));
+
+        writer.println("Supported Features");
+        writer.increaseIndent();
+        writer.println("- AUDIOCONTROL_FEATURE_AUDIO_FOCUS");
+        writer.decreaseIndent();
+
         writer.decreaseIndent();
     }
 
@@ -125,6 +136,11 @@
     }
 
     @Override
+    public void onDevicesToDuckChange(CarDuckingInfo carDuckingInfo) {
+        throw new UnsupportedOperationException("HAL ducking is unsupported for IAudioControl@2.0");
+    }
+
+    @Override
     public void linkToDeath(@Nullable AudioControlDeathRecipient deathRecipient) {
         try {
             mAudioControlV2.linkToDeath(this::serviceDied, 0);
diff --git a/service/src/com/android/car/garagemode/Controller.java b/service/src/com/android/car/garagemode/Controller.java
index 2568aa9..0689a62 100644
--- a/service/src/com/android/car/garagemode/Controller.java
+++ b/service/src/com/android/car/garagemode/Controller.java
@@ -66,11 +66,13 @@
     public void init() {
         mCarPowerManager = CarLocalServices.createCarPowerManager(mContext);
         mCarPowerManager.setListenerWithCompletion(Controller.this);
+        mGarageMode.init();
     }
 
     /** release */
     public void release() {
         mCarPowerManager.clearListener();
+        mGarageMode.release();
     }
 
     @Override
diff --git a/service/src/com/android/car/garagemode/GarageMode.java b/service/src/com/android/car/garagemode/GarageMode.java
index 627c747..d19772f 100644
--- a/service/src/com/android/car/garagemode/GarageMode.java
+++ b/service/src/com/android/car/garagemode/GarageMode.java
@@ -19,6 +19,9 @@
 import android.app.job.JobInfo;
 import android.app.job.JobScheduler;
 import android.app.job.JobSnapshot;
+import android.car.user.CarUserManager;
+import android.car.user.CarUserManager.UserLifecycleEvent;
+import android.car.user.CarUserManager.UserLifecycleListener;
 import android.content.Intent;
 import android.os.Handler;
 import android.os.UserHandle;
@@ -63,7 +66,7 @@
     static final long JOB_SNAPSHOT_INITIAL_UPDATE_MS = 10_000; // 10 seconds
 
     private static final long JOB_SNAPSHOT_UPDATE_FREQUENCY_MS = 1_000; // 1 second
-    private static final long USER_STOP_CHECK_INTERVAL = 10_000; // 10 secs
+    private static final long USER_STOP_CHECK_INTERVAL_MS = 100; // 100 milliseconds
     private static final int ADDITIONAL_CHECKS_TO_DO = 1;
 
     private final Controller mController;
@@ -124,16 +127,27 @@
         }
     };
 
+    private final Runnable mStartBackgroundUsers = new Runnable() {
+        @Override
+        public void run() {
+            ArrayList<Integer> startedUsers =
+                    CarLocalServices.getService(CarUserService.class).startAllBackgroundUsers();
+            LOG.i("Started background user during garage mode:" + startedUsers);
+            synchronized (mLock) {
+                // Stop stopping background users if there is any users left from last Garage mode,
+                // they would be stopped later.
+                mBackgroundUserStopInProcess = false;
+                mStartedBackgroundUsers.addAll(startedUsers);
+            }
+        }
+    };
+
     private final Runnable mStopUserCheckRunnable = new Runnable() {
         @Override
         public void run() {
             int userToStop = UserHandle.USER_SYSTEM; // BG user never becomes system user.
-            int remainingUsersToStop = 0;
             synchronized (mLock) {
-                remainingUsersToStop = mStartedBackgroundUsers.size();
-                if (remainingUsersToStop < 1) {
-                    return;
-                }
+                if (mStartedBackgroundUsers.isEmpty() || !mBackgroundUserStopInProcess) return;
                 userToStop = mStartedBackgroundUsers.valueAt(0);
             }
             if (numberOfIdleJobsRunning() == 0) { // all jobs done or stopped.
@@ -142,20 +156,20 @@
                     CarLocalServices.getService(CarUserService.class).stopBackgroundUser(
                             userToStop);
                     LOG.i("Stopping background user:" + userToStop + " remaining users:"
-                            + (remainingUsersToStop - 1));
+                            + (mStartedBackgroundUsers.size() - 1));
                 }
                 synchronized (mLock) {
                     mStartedBackgroundUsers.remove(userToStop);
-                    if (mStartedBackgroundUsers.size() == 0) {
+                    if (mStartedBackgroundUsers.isEmpty()) {
                         LOG.i("All background users have stopped");
+                        mBackgroundUserStopInProcess = false;
                         return;
                     }
                 }
             } else {
-                LOG.i("Waiting for jobs to finish, remaining users:" + remainingUsersToStop);
+                // Poll again later
+                mHandler.postDelayed(mStopUserCheckRunnable, USER_STOP_CHECK_INTERVAL_MS);
             }
-            // Poll again
-            mHandler.postDelayed(mStopUserCheckRunnable, USER_STOP_CHECK_INTERVAL);
         }
     };
 
@@ -164,6 +178,16 @@
     @GuardedBy("mLock")
     private ArraySet<Integer> mStartedBackgroundUsers = new ArraySet<>();
 
+    /**
+     * True when stopping of the background users is in process.
+     *
+     * <p> When garage mode exits, all background users started during GarageMode would be stopped
+     * one by one. mBackgroundUserStopInProcess would be true when stopping of the background users
+     * is in process.
+     */
+    @GuardedBy("mLock")
+    private boolean mBackgroundUserStopInProcess;
+
     GarageMode(Controller controller) {
         mGarageModeActive = false;
         mController = controller;
@@ -171,12 +195,49 @@
         mHandler = controller.getHandler();
     }
 
+    void init() {
+        CarLocalServices.getService(CarUserService.class)
+                .addUserLifecycleListener(mUserLifecycleListener);
+    }
+
+    void release() {
+        CarLocalServices.getService(CarUserService.class)
+                .removeUserLifecycleListener(mUserLifecycleListener);
+    }
+
+    /**
+     * When background users are queued to stop, this user lifecycle listener will ensure to stop
+     * them one by one by queuing next user when previous user is stopped.
+     */
+    private final UserLifecycleListener mUserLifecycleListener = new UserLifecycleListener() {
+        @Override
+        public void onEvent(UserLifecycleEvent event) {
+            if (event.getEventType() != CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STOPPED) return;
+
+            synchronized (mLock) {
+                if (mBackgroundUserStopInProcess) {
+                    mHandler.removeCallbacks(mStopUserCheckRunnable);
+                    LOG.i("Background user stopped event received. User Id : "
+                            + event.getUserId() + ". Queueing to stop next background user.");
+                    mHandler.post(mStopUserCheckRunnable);
+                }
+            }
+        }
+    };
+
     boolean isGarageModeActive() {
         synchronized (mLock) {
             return mGarageModeActive;
         }
     }
 
+    @VisibleForTesting
+    ArraySet<Integer> getStartedBackgroundUsers() {
+        synchronized (mLock) {
+            return mStartedBackgroundUsers;
+        }
+    }
+
     void dump(PrintWriter writer) {
         if (!mGarageModeActive) {
             return;
@@ -224,11 +285,7 @@
         broadcastSignalToJobScheduler(true);
         CarStatsLogHelper.logGarageModeStart();
         startMonitoringThread();
-        ArrayList<Integer> startedUsers =
-                CarLocalServices.getService(CarUserService.class).startAllBackgroundUsers();
-        synchronized (mLock) {
-            mStartedBackgroundUsers.addAll(startedUsers);
-        }
+        mHandler.post(mStartBackgroundUsers);
     }
 
     void cancel() {
@@ -281,8 +338,13 @@
     }
 
     private void startBackgroundUserStoppingLocked() {
-        if (mStartedBackgroundUsers.size() > 0) {
-            mHandler.postDelayed(mStopUserCheckRunnable, USER_STOP_CHECK_INTERVAL);
+        synchronized (mLock) {
+            if (!mStartedBackgroundUsers.isEmpty() && !mBackgroundUserStopInProcess) {
+                LOG.i("Stopping of background user queued. Total background users to stop: "
+                        + mStartedBackgroundUsers.size());
+                mHandler.post(mStopUserCheckRunnable);
+                mBackgroundUserStopInProcess = true;
+            }
         }
     }
 
diff --git a/service/src/com/android/car/power/CarPowerManagementService.java b/service/src/com/android/car/power/CarPowerManagementService.java
index 41ceb0e..dece259 100644
--- a/service/src/com/android/car/power/CarPowerManagementService.java
+++ b/service/src/com/android/car/power/CarPowerManagementService.java
@@ -1037,6 +1037,8 @@
     public void applyPowerPolicy(String policyId) {
         ICarImpl.assertPermission(mContext, Car.PERMISSION_CONTROL_CAR_POWER_POLICY);
         Preconditions.checkArgument(policyId != null, "policyId cannot be null");
+        Preconditions.checkArgument(!policyId.startsWith(PolicyReader.SYSTEM_POWER_POLICY_PREFIX),
+                "System power policy cannot be applied by apps");
         String errorMsg = applyPowerPolicy(policyId, true);
         if (errorMsg != null) {
             throw new IllegalArgumentException(errorMsg);
@@ -1165,6 +1167,8 @@
         }
         synchronized (mLock) {
             if (mIsPowerPolicyLocked) {
+                Slog.i(TAG, "Power policy is locked. The request policy(" + policyId
+                        + ") will be applied when power policy becomes unlocked");
                 mPendingPowerPolicy = policyId;
                 return null;
             }
diff --git a/service/src/com/android/car/power/PolicyReader.java b/service/src/com/android/car/power/PolicyReader.java
index 5e050b1..9b01ef8 100644
--- a/service/src/com/android/car/power/PolicyReader.java
+++ b/service/src/com/android/car/power/PolicyReader.java
@@ -179,6 +179,12 @@
     @Nullable
     String definePowerPolicy(String policyId, String[] enabledComponents,
             String[] disabledComponents) {
+        if (policyId == null) {
+            return "policyId cannot be null";
+        }
+        if (policyId.startsWith(SYSTEM_POWER_POLICY_PREFIX)) {
+            return "policyId should not start with " + SYSTEM_POWER_POLICY_PREFIX;
+        }
         if (mRegisteredPowerPolicies.containsKey(policyId)) {
             return policyId + " is already registered";
         }
@@ -605,10 +611,6 @@
                 return VehicleApPowerStateReport.WAIT_FOR_VHAL;
             case POWER_STATE_ON:
                 return VehicleApPowerStateReport.ON;
-            case POWER_STATE_DEEP_SLEEP_ENTRY:
-                return VehicleApPowerStateReport.DEEP_SLEEP_ENTRY;
-            case POWER_STATE_SHUTDOWN_START:
-                return VehicleApPowerStateReport.SHUTDOWN_START;
             default:
                 return INVALID_POWER_STATE;
         }
@@ -620,10 +622,6 @@
                 return POWER_STATE_WAIT_FOR_VHAL;
             case VehicleApPowerStateReport.ON:
                 return POWER_STATE_ON;
-            case VehicleApPowerStateReport.DEEP_SLEEP_ENTRY:
-                return POWER_STATE_DEEP_SLEEP_ENTRY;
-            case VehicleApPowerStateReport.SHUTDOWN_START:
-                return POWER_STATE_SHUTDOWN_START;
             default:
                 return "unknown power state";
         }
diff --git a/tests/carservice_test/src/com/android/car/audio/CarAudioZoneTest.java b/tests/carservice_test/src/com/android/car/audio/CarAudioZoneTest.java
index 0624b21..5d270d3 100644
--- a/tests/carservice_test/src/com/android/car/audio/CarAudioZoneTest.java
+++ b/tests/carservice_test/src/com/android/car/audio/CarAudioZoneTest.java
@@ -48,6 +48,7 @@
 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.ArrayList;
@@ -258,6 +259,49 @@
                         .findActiveContextsFromPlaybackConfigurations(activeConfigurations));
     }
 
+    @Test
+    public void isAudioDeviceInfoValidForZone_withNullAudioDeviceInfo_returnsFalse() {
+        mTestAudioZone.addVolumeGroup(mMockMusicGroup);
+
+        assertThat(mTestAudioZone.isAudioDeviceInfoValidForZone(null)).isFalse();
+    }
+
+    @Test
+    public void isAudioDeviceInfoValidForZone_withNullDeviceAddress_returnsFalse() {
+        mTestAudioZone.addVolumeGroup(mMockMusicGroup);
+        AudioDeviceInfo nullAddressDeviceInfo = Mockito.mock(AudioDeviceInfo.class);
+        when(nullAddressDeviceInfo.getAddress()).thenReturn(null);
+
+        assertThat(mTestAudioZone.isAudioDeviceInfoValidForZone(nullAddressDeviceInfo)).isFalse();
+    }
+
+    @Test
+    public void isAudioDeviceInfoValidForZone_withEmptyDeviceAddress_returnsFalse() {
+        mTestAudioZone.addVolumeGroup(mMockMusicGroup);
+        AudioDeviceInfo nullAddressDeviceInfo = Mockito.mock(AudioDeviceInfo.class);
+        when(nullAddressDeviceInfo.getAddress()).thenReturn("");
+
+        assertThat(mTestAudioZone.isAudioDeviceInfoValidForZone(nullAddressDeviceInfo)).isFalse();
+    }
+
+    @Test
+    public void isAudioDeviceInfoValidForZone_withDeviceAddressNotInZone_returnsFalse() {
+        mTestAudioZone.addVolumeGroup(mMockMusicGroup);
+        AudioDeviceInfo nullAddressDeviceInfo = Mockito.mock(AudioDeviceInfo.class);
+        when(nullAddressDeviceInfo.getAddress()).thenReturn(VOICE_ADDRESS);
+
+        assertThat(mTestAudioZone.isAudioDeviceInfoValidForZone(nullAddressDeviceInfo)).isFalse();
+    }
+
+    @Test
+    public void isAudioDeviceInfoValidForZone_withDeviceAddressInZone_returnsTrue() {
+        mTestAudioZone.addVolumeGroup(mMockMusicGroup);
+        AudioDeviceInfo nullAddressDeviceInfo = Mockito.mock(AudioDeviceInfo.class);
+        when(nullAddressDeviceInfo.getAddress()).thenReturn(MUSIC_ADDRESS);
+
+        assertThat(mTestAudioZone.isAudioDeviceInfoValidForZone(nullAddressDeviceInfo)).isTrue();
+    }
+
     private static class VolumeGroupBuilder {
         private SparseArray<String> mDeviceAddresses = new SparseArray<>();
 
diff --git a/tests/carservice_test/src/com/android/car/garagemode/GarageModeTest.java b/tests/carservice_test/src/com/android/car/garagemode/GarageModeTest.java
new file mode 100644
index 0000000..fd48126
--- /dev/null
+++ b/tests/carservice_test/src/com/android/car/garagemode/GarageModeTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.garagemode;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.job.JobScheduler;
+import android.car.user.CarUserManager;
+import android.car.user.CarUserManager.UserLifecycleEvent;
+import android.car.user.CarUserManager.UserLifecycleListener;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.car.CarLocalServices;
+import com.android.car.user.CarUserService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class GarageModeTest {
+    @Rule
+    public final MockitoRule rule = MockitoJUnit.rule();
+    private GarageMode mGarageMode;
+    @Mock
+    private Controller mController;
+    @Mock
+    private JobScheduler mJobScheduler;
+    @Mock
+    private CarUserService mCarUserService;
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+    @Before
+    public void setUp() {
+        when(mController.getHandler()).thenReturn(mHandler);
+        when(mController.getJobSchedulerService()).thenReturn(mJobScheduler);
+
+        mGarageMode = new GarageMode(mController);
+        CarLocalServices.removeServiceForTest(CarUserService.class);
+        CarLocalServices.addService(CarUserService.class, mCarUserService);
+        mGarageMode.init();
+    }
+
+    @After
+    public void teardown() {
+        CarLocalServices.removeServiceForTest(CarUserService.class);
+    }
+
+    @Test
+    public void test_releaseRemoveListener() {
+        mGarageMode.release();
+
+        verify(mCarUserService).removeUserLifecycleListener(any());
+    }
+
+    @Test
+    public void test_backgroundUsersStopedOnGarageModeCancel() throws Exception {
+        ArrayList<Integer> userToStartInBackground = new ArrayList<>(Arrays.asList(101, 102, 103));
+        when(mCarUserService.startAllBackgroundUsers()).thenReturn(userToStartInBackground);
+        mGarageMode.enterGarageMode(/* future= */ null);
+        CountDownLatch latch = new CountDownLatch(3); // 3 for three users
+        mockCarUserServiceStopUserCall(getEventListener(), latch);
+
+        mGarageMode.cancel();
+
+        // wait for handler thread to finish
+        assertThat(latch.await(100, TimeUnit.MILLISECONDS)).isTrue();
+        verify(mCarUserService).startAllBackgroundUsers();
+        verify(mCarUserService).stopBackgroundUser(101);
+        verify(mCarUserService).stopBackgroundUser(102);
+        verify(mCarUserService).stopBackgroundUser(103);
+    }
+
+    @Test
+    public void test_restartingGarageModeStorePreviouslyStartedUsers() throws Exception {
+        ArrayList<Integer> userToStartInBackground = new ArrayList<>(Arrays.asList(101, 102, 103));
+        CountDownLatch latch = mockCarUserServiceStartUsersCall(userToStartInBackground);
+        mGarageMode.enterGarageMode(/* future= */ null);
+
+        // wait for handler thread to finish
+        assertThat(latch.await(100, TimeUnit.MILLISECONDS)).isTrue();
+        assertThat(mGarageMode.getStartedBackgroundUsers()).containsExactly(101, 102, 103);
+
+        userToStartInBackground = new ArrayList<>(Arrays.asList(103, 104, 105));
+        latch = mockCarUserServiceStartUsersCall(userToStartInBackground);
+        mGarageMode.enterGarageMode(/* future= */ null);
+
+        // wait for handler thread to finish
+        assertThat(latch.await(100, TimeUnit.MILLISECONDS)).isTrue();
+        assertThat(mGarageMode.getStartedBackgroundUsers()).containsExactly(101, 102, 103, 104,
+                105);
+    }
+
+    private CountDownLatch mockCarUserServiceStartUsersCall(
+            ArrayList<Integer> userToStartInBackground) {
+        CountDownLatch latch = new CountDownLatch(1);
+        doAnswer(inv -> {
+            latch.countDown();
+            return userToStartInBackground;
+        }).when(mCarUserService).startAllBackgroundUsers();
+
+        return latch;
+    }
+
+    private UserLifecycleListener getEventListener() {
+        ArgumentCaptor<UserLifecycleListener> listenerCaptor =
+                ArgumentCaptor.forClass(UserLifecycleListener.class);
+        verify(mCarUserService).addUserLifecycleListener(listenerCaptor.capture());
+        UserLifecycleListener listener = listenerCaptor.getValue();
+        return listener;
+    }
+
+    private void mockCarUserServiceStopUserCall(UserLifecycleListener listener,
+            CountDownLatch latch) {
+        doAnswer(inv -> {
+            int userId = (int) inv.getArguments()[0];
+            latch.countDown();
+            listener.onEvent(new UserLifecycleEvent(
+                    CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STOPPED, userId));
+            return null;
+        }).when(mCarUserService).stopBackgroundUser(anyInt());
+    }
+}
diff --git a/tests/carservice_unit_test/res/raw/invalid_power_policy_group_incorrect_state.xml b/tests/carservice_unit_test/res/raw/invalid_power_policy_group_incorrect_state.xml
index 3cbdabe..78292b5 100644
--- a/tests/carservice_unit_test/res/raw/invalid_power_policy_group_incorrect_state.xml
+++ b/tests/carservice_unit_test/res/raw/invalid_power_policy_group_incorrect_state.xml
@@ -20,8 +20,6 @@
         <policyGroup id="mixed_policy_group">
             <defaultPolicy state="WaitForVHAL" id="policy_id_other_on"/>
             <defaultPolicy state="IncorrectState" id="expected_to_be_registered"/>
-            <noDefaultPolicy state="DeepSleepEntry"/>
-            <noDefaultPolicy state="ShutdownStart"/>
         </policyGroup>
     </policyGroups>
 
diff --git a/tests/carservice_unit_test/res/raw/valid_power_policy.xml b/tests/carservice_unit_test/res/raw/valid_power_policy.xml
index 9ced901..c1dc081 100644
--- a/tests/carservice_unit_test/res/raw/valid_power_policy.xml
+++ b/tests/carservice_unit_test/res/raw/valid_power_policy.xml
@@ -20,20 +20,14 @@
         <policyGroup id="basic_policy_group">
             <defaultPolicy state="WaitForVHAL" id="policy_id_other_on"/>
             <defaultPolicy state="On" id="policy_id_other_untouched"/>
-            <defaultPolicy state="DeepSleepEntry" id="policy_id_other_off"/>
-            <defaultPolicy state="ShutdownStart" id="policy_id_other_none"/>
         </policyGroup>
         <policyGroup id="no_default_policy_group">
             <noDefaultPolicy state="WaitForVHAL"/>
             <noDefaultPolicy state="On"/>
-            <noDefaultPolicy state="DeepSleepEntry"/>
-            <noDefaultPolicy state="ShutdownStart"/>
         </policyGroup>
         <policyGroup id="mixed_policy_group">
             <defaultPolicy state="WaitForVHAL" id="policy_id_other_on"/>
-            <defaultPolicy state="On" id="policy_id_other_untouched"/>
-            <noDefaultPolicy state="DeepSleepEntry"/>
-            <noDefaultPolicy state="ShutdownStart"/>
+            <noDefaultPolicy state="On"/>
         </policyGroup>
     </policyGroups>
 
diff --git a/tests/carservice_unit_test/res/raw/valid_power_policy_no_system_power_policy.xml b/tests/carservice_unit_test/res/raw/valid_power_policy_no_system_power_policy.xml
index fc0b72c..4b7e74d 100644
--- a/tests/carservice_unit_test/res/raw/valid_power_policy_no_system_power_policy.xml
+++ b/tests/carservice_unit_test/res/raw/valid_power_policy_no_system_power_policy.xml
@@ -20,8 +20,6 @@
         <policyGroup id="mixed_policy_group">
             <defaultPolicy state="WaitForVHAL" id="policy_id_other_on"/>
             <defaultPolicy state="On" id="policy_id_other_untouched"/>
-            <noDefaultPolicy state="DeepSleepEntry"/>
-            <noDefaultPolicy state="ShutdownStart"/>
         </policyGroup>
     </policyGroups>
 
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPlaybackCallbackTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPlaybackCallbackTest.java
new file mode 100644
index 0000000..926f7d0
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPlaybackCallbackTest.java
@@ -0,0 +1,547 @@
+/*
+ * 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 android.media.AudioAttributes.AttributeUsage;
+import static android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
+import static android.media.AudioAttributes.USAGE_MEDIA;
+
+import static com.android.car.audio.CarAudioContext.AudioContext;
+import static com.android.car.audio.CarAudioContext.MUSIC;
+import static com.android.car.audio.CarAudioContext.NAVIGATION;
+import static com.android.car.audio.CarAudioContext.VOICE_COMMAND;
+import static com.android.car.audio.CarAudioService.SystemClockWrapper;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.expectThrows;
+
+import android.media.AudioAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioPlaybackConfiguration;
+import android.util.SparseArray;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class CarAudioPlaybackCallbackTest {
+
+    private static final int PRIMARY_ZONE_ID = 0;
+    private static final String PRIMARY_MEDIA_ADDRESS = "music_bus0";
+    private static final String PRIMARY_NAVIGATION_ADDRESS = "navigation_bus1";
+    private static final String PRIMARY_VOICE_ADDRESS = "voice_bus3";
+
+    private static final String SECONDARY_MEDIA_ADDRESS = "music_bus100";
+    private static final String SECONDARY_NAVIGATION_ADDRESS = "navigation_bus101";
+
+    private static final long TIMER_START_TIME_MS = 100000;
+    private static final int KEY_EVENT_TIMEOUT_MS = 3000;
+    private static final long TIMER_BEFORE_TIMEOUT_MS =
+            TIMER_START_TIME_MS + KEY_EVENT_TIMEOUT_MS - 1;
+    private static final long TIMER_AFTER_TIMEOUT_MS =
+            TIMER_START_TIME_MS + KEY_EVENT_TIMEOUT_MS + 1;
+
+    @Mock
+    private SystemClockWrapper mClock;
+
+    @Mock
+    private CarVolumeGroup mPrimaryZoneMockMusicGroup;
+    @Mock
+    private CarVolumeGroup mPrimaryZoneMockNavGroup;
+    @Mock
+    private CarVolumeGroup mPrimaryZoneMockVoiceGroup;
+
+    private CarAudioZone mPrimaryZone;
+
+    @Before
+    public void setUp() {
+        mPrimaryZone = generatePrimaryZone();
+        when(mClock.uptimeMillis()).thenReturn(TIMER_START_TIME_MS);
+    }
+
+    @Test
+    public void createCarAudioPlaybackCallback_withNullCarAudioZones_fails() throws Exception {
+        expectThrows(NullPointerException.class, () -> {
+            new CarAudioPlaybackCallback(null, mClock, KEY_EVENT_TIMEOUT_MS);
+        });
+    }
+
+    @Test
+    public void createCarAudioPlaybackCallback_withNullSystemClockWrapper_fails() throws Exception {
+        expectThrows(NullPointerException.class, () -> {
+            new CarAudioPlaybackCallback(mPrimaryZone, null, KEY_EVENT_TIMEOUT_MS);
+        });
+    }
+
+    @Test
+    public void
+            createCarAudioPlaybackCallback_withNegativeKeyEventTimeout_fails() throws Exception {
+        expectThrows(IllegalArgumentException.class, () -> {
+            new CarAudioPlaybackCallback(mPrimaryZone, mClock, -KEY_EVENT_TIMEOUT_MS);
+        });
+    }
+
+    @Test
+    public void
+            getAllActiveContextsForPrimaryZone_withNoOnPlaybackConfigChanged_returnsEmptyList() {
+        CarAudioPlaybackCallback callback =
+                new CarAudioPlaybackCallback(mPrimaryZone, mClock, KEY_EVENT_TIMEOUT_MS);
+
+        List<Integer> activeContexts =
+                callback.getAllActiveContextsForPrimaryZone();
+
+        assertThat(activeContexts).isEmpty();
+    }
+
+    @Test
+    public void
+            getAllActiveContextsForPrimaryZone_withOneMatchingConfiguration_returnsActiveContext() {
+        List<AudioPlaybackConfiguration> activeConfigurations = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(PRIMARY_MEDIA_ADDRESS)
+                        .build()
+        );
+
+        CarAudioPlaybackCallback callback =
+                new CarAudioPlaybackCallback(mPrimaryZone, mClock, KEY_EVENT_TIMEOUT_MS);
+
+        callback.onPlaybackConfigChanged(activeConfigurations);
+
+        List<Integer> activeContexts =
+                callback.getAllActiveContextsForPrimaryZone();
+
+        assertThat(activeContexts).containsExactly(MUSIC);
+    }
+
+    @Test
+    public void
+            getAllActiveContextsForPrimaryZone_withMultipleConfiguration_returnsActiveContexts() {
+        List<AudioPlaybackConfiguration> activeConfigurations = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(PRIMARY_MEDIA_ADDRESS)
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(PRIMARY_NAVIGATION_ADDRESS)
+                        .build()
+        );
+
+        CarAudioPlaybackCallback callback =
+                new CarAudioPlaybackCallback(mPrimaryZone, mClock, KEY_EVENT_TIMEOUT_MS);
+
+        callback.onPlaybackConfigChanged(activeConfigurations);
+
+        List<Integer> activeContexts =
+                callback.getAllActiveContextsForPrimaryZone();
+
+        assertThat(activeContexts).containsExactly(MUSIC, NAVIGATION);
+    }
+
+    @Test
+    public void
+            getAllActiveContextsForPrimaryZone_withInactiveConfigurations_returnsActiveContext() {
+        List<AudioPlaybackConfiguration> configurations = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(PRIMARY_MEDIA_ADDRESS)
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(PRIMARY_NAVIGATION_ADDRESS)
+                        .setInactive()
+                        .build()
+        );
+
+        CarAudioPlaybackCallback callback =
+                new CarAudioPlaybackCallback(mPrimaryZone, mClock, KEY_EVENT_TIMEOUT_MS);
+
+        callback.onPlaybackConfigChanged(configurations);
+
+        List<Integer> activeContexts =
+                callback.getAllActiveContextsForPrimaryZone();
+
+        assertThat(activeContexts).containsExactly(MUSIC);
+    }
+
+    @Test
+    public void
+            getAllActiveContextsForPrimaryZone_withNoActiveConfigurations_returnsEmptyContexts() {
+        List<AudioPlaybackConfiguration> activeConfigurations = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(PRIMARY_MEDIA_ADDRESS)
+                        .setInactive()
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(PRIMARY_NAVIGATION_ADDRESS)
+                        .setInactive()
+                        .build()
+        );
+
+        CarAudioPlaybackCallback callback =
+                new CarAudioPlaybackCallback(mPrimaryZone, mClock, KEY_EVENT_TIMEOUT_MS);
+
+        callback.onPlaybackConfigChanged(activeConfigurations);
+
+        List<Integer> activeContexts =
+                callback.getAllActiveContextsForPrimaryZone();
+
+        assertThat(activeContexts).isEmpty();
+    }
+
+    @Test
+    public void
+            getAllActiveContextsForPrimaryZone_withInactiveConfig_beforeTimeout_returnsContexts() {
+        List<AudioPlaybackConfiguration> activeConfigurations = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(PRIMARY_MEDIA_ADDRESS)
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(PRIMARY_NAVIGATION_ADDRESS)
+                        .build()
+        );
+
+        List<AudioPlaybackConfiguration> configurationsChanged = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(PRIMARY_MEDIA_ADDRESS)
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setInactive()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(PRIMARY_NAVIGATION_ADDRESS)
+                        .build()
+        );
+
+        CarAudioPlaybackCallback callback =
+                new CarAudioPlaybackCallback(mPrimaryZone, mClock, KEY_EVENT_TIMEOUT_MS);
+
+        callback.onPlaybackConfigChanged(activeConfigurations);
+
+        callback.onPlaybackConfigChanged(configurationsChanged);
+
+        when(mClock.uptimeMillis()).thenReturn(TIMER_BEFORE_TIMEOUT_MS);
+
+        List<Integer> activeContexts =
+                callback.getAllActiveContextsForPrimaryZone();
+
+        assertThat(activeContexts).containsExactly(MUSIC, NAVIGATION);
+    }
+
+    @Test
+    public void
+            getAllActiveContextsForPrimaryZone_withInactiveConfigs_beforeTimeout_returnsContexts() {
+        List<AudioPlaybackConfiguration> activeConfigurations = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(PRIMARY_MEDIA_ADDRESS)
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(PRIMARY_NAVIGATION_ADDRESS)
+                        .build()
+        );
+
+        List<AudioPlaybackConfiguration> configurationsChanged = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(PRIMARY_MEDIA_ADDRESS)
+                        .setInactive()
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(PRIMARY_NAVIGATION_ADDRESS)
+                        .setInactive()
+                        .build()
+        );
+
+        CarAudioPlaybackCallback callback =
+                new CarAudioPlaybackCallback(mPrimaryZone, mClock, KEY_EVENT_TIMEOUT_MS);
+
+        callback.onPlaybackConfigChanged(activeConfigurations);
+
+        callback.onPlaybackConfigChanged(configurationsChanged);
+
+        when(mClock.uptimeMillis()).thenReturn(TIMER_BEFORE_TIMEOUT_MS);
+
+        List<Integer> activeContexts =
+                callback.getAllActiveContextsForPrimaryZone();
+
+        assertThat(activeContexts).containsExactly(NAVIGATION, MUSIC);
+    }
+
+    @Test
+    public void
+            getAllActiveContextsForPrimaryZone_withInactiveConfig_afterTimeout_returnsContext() {
+        List<AudioPlaybackConfiguration> activeConfigurations = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(PRIMARY_MEDIA_ADDRESS)
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(PRIMARY_NAVIGATION_ADDRESS)
+                        .build()
+        );
+
+        List<AudioPlaybackConfiguration> configurationsChanged = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(PRIMARY_MEDIA_ADDRESS)
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(PRIMARY_NAVIGATION_ADDRESS)
+                        .setInactive()
+                        .build()
+        );
+
+        CarAudioPlaybackCallback callback =
+                new CarAudioPlaybackCallback(mPrimaryZone, mClock, KEY_EVENT_TIMEOUT_MS);
+
+        callback.onPlaybackConfigChanged(activeConfigurations);
+
+        callback.onPlaybackConfigChanged(configurationsChanged);
+
+        when(mClock.uptimeMillis()).thenReturn(TIMER_AFTER_TIMEOUT_MS);
+
+        List<Integer> activeContexts =
+                callback.getAllActiveContextsForPrimaryZone();
+
+        assertThat(activeContexts).containsExactly(MUSIC);
+    }
+
+    @Test
+    public void getAllActiveContextsForPrimaryZone_withInactiveConfigs_afterTimeout_returnsEmpty() {
+        List<AudioPlaybackConfiguration> activeConfigurations = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(PRIMARY_MEDIA_ADDRESS)
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(PRIMARY_NAVIGATION_ADDRESS)
+                        .build()
+        );
+
+        List<AudioPlaybackConfiguration> configurationsChanged = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(PRIMARY_MEDIA_ADDRESS)
+                        .setInactive()
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(PRIMARY_NAVIGATION_ADDRESS)
+                        .setInactive()
+                        .build()
+        );
+
+        CarAudioPlaybackCallback callback =
+                new CarAudioPlaybackCallback(mPrimaryZone, mClock, KEY_EVENT_TIMEOUT_MS);
+
+        callback.onPlaybackConfigChanged(activeConfigurations);
+
+        callback.onPlaybackConfigChanged(configurationsChanged);
+
+        when(mClock.uptimeMillis()).thenReturn(TIMER_AFTER_TIMEOUT_MS);
+
+        List<Integer> activeContexts =
+                callback.getAllActiveContextsForPrimaryZone();
+
+        assertThat(activeContexts).isEmpty();
+    }
+
+
+    @Test
+    public void
+            getAllActiveContextsForPrimaryZone_withMultiActiveConfigs_forDiffZone_returnsEmpty() {
+        List<AudioPlaybackConfiguration> activeConfigurations = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(SECONDARY_MEDIA_ADDRESS)
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(SECONDARY_NAVIGATION_ADDRESS)
+                        .build()
+        );
+
+        CarAudioPlaybackCallback callback =
+                new CarAudioPlaybackCallback(mPrimaryZone, mClock, KEY_EVENT_TIMEOUT_MS);
+
+        callback.onPlaybackConfigChanged(activeConfigurations);
+
+        List<Integer> activeContexts =
+                callback.getAllActiveContextsForPrimaryZone();
+
+        assertThat(activeContexts).isEmpty();
+    }
+
+    @Test
+    public void
+            getAllActiveContextsForPrimaryZone_withInactiveConfigs_forDifferentZone_returnsEmpty() {
+        List<AudioPlaybackConfiguration> activeConfigurations = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(SECONDARY_MEDIA_ADDRESS)
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(SECONDARY_NAVIGATION_ADDRESS)
+                        .build()
+        );
+
+        List<AudioPlaybackConfiguration> configurationsChanged = ImmutableList.of(
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_MEDIA)
+                        .setDeviceAddress(SECONDARY_MEDIA_ADDRESS)
+                        .setInactive()
+                        .build(),
+                new AudioPlaybackConfigurationBuilder()
+                        .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setDeviceAddress(SECONDARY_NAVIGATION_ADDRESS)
+                        .setInactive()
+                        .build()
+        );
+
+        CarAudioPlaybackCallback callback =
+                new CarAudioPlaybackCallback(mPrimaryZone, mClock, KEY_EVENT_TIMEOUT_MS);
+
+        callback.onPlaybackConfigChanged(activeConfigurations);
+
+        callback.onPlaybackConfigChanged(configurationsChanged);
+
+        List<Integer> activeContexts =
+                callback.getAllActiveContextsForPrimaryZone();
+
+        assertThat(activeContexts).isEmpty();
+    }
+
+    private CarAudioZone generatePrimaryZone() {
+        mPrimaryZoneMockMusicGroup = new VolumeGroupBuilder()
+                .addDeviceAddressAndContexts(MUSIC, PRIMARY_MEDIA_ADDRESS)
+                .build();
+        mPrimaryZoneMockNavGroup = new VolumeGroupBuilder()
+                .addDeviceAddressAndContexts(NAVIGATION, PRIMARY_NAVIGATION_ADDRESS)
+                .build();
+        mPrimaryZoneMockVoiceGroup = new VolumeGroupBuilder()
+                .addDeviceAddressAndContexts(VOICE_COMMAND, PRIMARY_VOICE_ADDRESS)
+                .build();
+        CarAudioZone primaryZone = new CarAudioZone(PRIMARY_ZONE_ID, "Primary zone");
+        primaryZone.addVolumeGroup(mPrimaryZoneMockMusicGroup);
+        primaryZone.addVolumeGroup(mPrimaryZoneMockNavGroup);
+        primaryZone.addVolumeGroup(mPrimaryZoneMockVoiceGroup);
+        return primaryZone;
+    }
+
+    private static class VolumeGroupBuilder {
+        private SparseArray<String> mDeviceAddresses = new SparseArray<>();
+
+        VolumeGroupBuilder addDeviceAddressAndContexts(@AudioContext int context, String address) {
+            mDeviceAddresses.put(context, address);
+            return this;
+        }
+
+        CarVolumeGroup build() {
+            CarVolumeGroup carVolumeGroup = mock(CarVolumeGroup.class);
+            Map<String, ArrayList<Integer>> addressToContexts = new HashMap<>();
+            @AudioContext int[] contexts = new int[mDeviceAddresses.size()];
+
+            for (int index = 0; index < mDeviceAddresses.size(); index++) {
+                @AudioContext int context = mDeviceAddresses.keyAt(index);
+                String address = mDeviceAddresses.get(context);
+                when(carVolumeGroup.getAddressForContext(context)).thenReturn(address);
+                if (!addressToContexts.containsKey(address)) {
+                    addressToContexts.put(address, new ArrayList<>());
+                }
+                addressToContexts.get(address).add(context);
+                contexts[index] = context;
+            }
+
+            when(carVolumeGroup.getContexts()).thenReturn(contexts);
+
+            for (String address : addressToContexts.keySet()) {
+                when(carVolumeGroup.getContextsForAddress(address))
+                        .thenReturn(ImmutableList.copyOf(addressToContexts.get(address)));
+            }
+            when(carVolumeGroup.getAddresses())
+                    .thenReturn(ImmutableList.copyOf(addressToContexts.keySet()));
+            return carVolumeGroup;
+        }
+    }
+
+    private static class AudioPlaybackConfigurationBuilder {
+        private @AttributeUsage int mUsage = USAGE_MEDIA;
+        private boolean mIsActive = true;
+        private String mDeviceAddress = "";
+
+        AudioPlaybackConfigurationBuilder setUsage(@AttributeUsage int usage) {
+            mUsage = usage;
+            return this;
+        }
+
+        AudioPlaybackConfigurationBuilder setDeviceAddress(String deviceAddress) {
+            mDeviceAddress = deviceAddress;
+            return this;
+        }
+
+        AudioPlaybackConfigurationBuilder setInactive() {
+            mIsActive = false;
+            return this;
+        }
+
+        AudioPlaybackConfiguration build() {
+            AudioPlaybackConfiguration configuration = mock(AudioPlaybackConfiguration.class);
+            AudioAttributes attributes = new AudioAttributes.Builder().setUsage(mUsage).build();
+            AudioDeviceInfo outputDevice = generateOutAudioDeviceInfo(mDeviceAddress);
+            when(configuration.getAudioAttributes()).thenReturn(attributes);
+            when(configuration.getAudioDeviceInfo()).thenReturn(outputDevice);
+            when(configuration.isActive()).thenReturn(mIsActive);
+            return configuration;
+        }
+
+        private AudioDeviceInfo generateOutAudioDeviceInfo(String address) {
+            AudioDeviceInfo audioDeviceInfo = mock(AudioDeviceInfo.class);
+            when(audioDeviceInfo.getAddress()).thenReturn(address);
+            when(audioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_BUS);
+            when(audioDeviceInfo.isSource()).thenReturn(false);
+            when(audioDeviceInfo.isSink()).thenReturn(true);
+            when(audioDeviceInfo.getInternalType()).thenReturn(AudioDeviceInfo
+                    .convertDeviceTypeToInternalInputDevice(AudioDeviceInfo.TYPE_BUS));
+            return audioDeviceInfo;
+        }
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioUtilsTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioUtilsTest.java
new file mode 100644
index 0000000..e913b53
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioUtilsTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.android.car.audio.CarAudioUtils.hasExpired;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class CarAudioUtilsTest {
+
+    @Test
+    public void hasExpired_forCurrentTimeBeforeTimeout_returnsFalse() {
+        assertThat(hasExpired(0, 100, 200)).isFalse();
+    }
+
+    @Test
+    public void hasExpired_forCurrentTimeAfterTimeout_returnsFalse() {
+        assertThat(hasExpired(0, 300, 200)).isTrue();
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingInfoTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingInfoTest.java
new file mode 100644
index 0000000..150a48a
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingInfoTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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 android.media.AudioAttributes.USAGE_MEDIA;
+import static android.media.AudioAttributes.USAGE_NOTIFICATION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.audio.policy.configuration.V7_0.AudioUsage;
+import android.hardware.automotive.audiocontrol.DuckingInfo;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class CarDuckingInfoTest {
+    private static final int ZONE_ID = 0;
+    private static final List<String> sAddressesToDuck = List.of("address1", "address2");
+    private static final List<String> sAddressesToUnduck = List.of("address3", "address4");
+    private static final int[] sUsagesHoldingFocus = {USAGE_MEDIA, USAGE_NOTIFICATION};
+
+    @Test
+    public void constructor_nullAddressesToDuck_throws() {
+        assertThrows(NullPointerException.class, () -> new CarDuckingInfo(ZONE_ID, null,
+                sAddressesToUnduck, sUsagesHoldingFocus));
+    }
+
+    @Test
+    public void constructor_nullAddressesToUnduck_throws() {
+        assertThrows(NullPointerException.class, () -> new CarDuckingInfo(ZONE_ID, sAddressesToDuck,
+                null, sUsagesHoldingFocus));
+    }
+
+    @Test
+    public void constructor_nullusagesHoldingFocus_throws() {
+        assertThrows(NullPointerException.class, () -> new CarDuckingInfo(ZONE_ID, sAddressesToDuck,
+                sAddressesToUnduck, null));
+    }
+
+    @Test
+    public void constructor_validInputs_succeeds() {
+        CarDuckingInfo duckingInfo = getCarDuckingInfo();
+
+        assertThat(duckingInfo.mZoneId).isEqualTo(ZONE_ID);
+        assertThat(duckingInfo.mAddressesToDuck).containsExactlyElementsIn(sAddressesToDuck);
+        assertThat(duckingInfo.mAddressesToUnduck).containsExactlyElementsIn(sAddressesToUnduck);
+        assertThat(duckingInfo.mUsagesHoldingFocus).asList()
+                .containsExactly(USAGE_MEDIA, USAGE_NOTIFICATION);
+    }
+
+    @Test
+    public void generateDuckingInfo_includesSameAddressesToDuck() {
+        CarDuckingInfo carDuckingInfo = getCarDuckingInfo();
+
+        DuckingInfo duckingInfo = carDuckingInfo.generateDuckingInfo();
+
+        assertThat(duckingInfo.deviceAddressesToDuck).asList()
+                .containsExactlyElementsIn(carDuckingInfo.mAddressesToDuck);
+    }
+
+    @Test
+    public void generateDuckingInfo_includesSameAddressesToUnduck() {
+        CarDuckingInfo carDuckingInfo = getCarDuckingInfo();
+
+        DuckingInfo duckingInfo = carDuckingInfo.generateDuckingInfo();
+
+        assertThat(duckingInfo.deviceAddressesToUnduck).asList()
+                .containsExactlyElementsIn(carDuckingInfo.mAddressesToUnduck);
+    }
+
+    @Test
+    public void generateDuckingInfo_includesSameUsagesHoldingFocus() {
+        CarDuckingInfo carDuckingInfo = getCarDuckingInfo();
+
+        DuckingInfo duckingInfo = carDuckingInfo.generateDuckingInfo();
+
+        assertThat(duckingInfo.usagesHoldingFocus).asList()
+                .containsExactly(AudioUsage.AUDIO_USAGE_MEDIA.toString(),
+                        AudioUsage.AUDIO_USAGE_NOTIFICATION.toString());
+    }
+
+    private CarDuckingInfo getCarDuckingInfo() {
+        return new CarDuckingInfo(ZONE_ID, sAddressesToDuck, sAddressesToUnduck,
+                sUsagesHoldingFocus);
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingTest.java
index 302669f..25becf8 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingTest.java
@@ -34,8 +34,6 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
-import com.android.car.audio.CarDucking.CarDuckingInfo;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeTest.java
index d282ca7..2bb445a 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeTest.java
@@ -33,38 +33,58 @@
 import static com.android.car.audio.CarAudioContext.NOTIFICATION;
 import static com.android.car.audio.CarAudioContext.VOICE_COMMAND;
 import static com.android.car.audio.CarAudioService.DEFAULT_AUDIO_CONTEXT;
+import static com.android.car.audio.CarAudioService.SystemClockWrapper;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertThrows;
 import static org.testng.Assert.expectThrows;
 
 import android.media.AudioAttributes.AttributeUsage;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
 import com.android.car.audio.CarAudioContext.AudioContext;
 
 import com.google.common.collect.ImmutableList;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
 
 import java.util.ArrayList;
 import java.util.List;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(MockitoJUnitRunner.class)
 public class CarVolumeTest {
 
-    public static final @CarVolume.CarVolumeListVersion int VERSION_ZERO = 0;
-    public static final @CarVolume.CarVolumeListVersion int VERSION_ONE = 1;
-    public static final @CarVolume.CarVolumeListVersion int VERSION_TWO = 2;
-    public static final @CarVolume.CarVolumeListVersion int VERSION_THREE = 3;
+    private static final @CarVolume.CarVolumeListVersion int VERSION_ZERO = 0;
+    private static final @CarVolume.CarVolumeListVersion int VERSION_ONE = 1;
+    private static final @CarVolume.CarVolumeListVersion int VERSION_TWO = 2;
+    private static final @CarVolume.CarVolumeListVersion int VERSION_THREE = 3;
+    private static final long START_TIME = 10000;
+    private static final long START_TIME_ONE_SECOND = 11000;
+    private static final long START_TIME_FOUR_SECOND = 14000;
+    private static final int KEY_EVENT_TIMEOUT_MS = 3000;
+    private static final int TRIAL_COUNTS = 10;
+
+    @Mock
+    private SystemClockWrapper mMockClock;
+
+    private CarVolume mCarVolume;
+
+    @Before
+    public void setUp() throws Exception {
+        when(mMockClock.uptimeMillis()).thenReturn(START_TIME);
+        mCarVolume = new CarVolume(mMockClock, VERSION_TWO, KEY_EVENT_TIMEOUT_MS);
+
+    }
 
     @Test
     public void createCarVolume_withVersionLessThanOne_failsTooLow() {
         IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class, () -> {
-            new CarVolume(VERSION_ZERO);
+            new CarVolume(mMockClock, VERSION_ZERO, KEY_EVENT_TIMEOUT_MS);
         });
 
         assertThat(thrown).hasMessageThat().contains("too low");
@@ -73,33 +93,37 @@
     @Test
     public void createCarVolume_withVersionGreaterThanTwo_failsTooHigh() {
         IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class, () -> {
-            new CarVolume(VERSION_THREE);
+            new CarVolume(mMockClock, VERSION_THREE, KEY_EVENT_TIMEOUT_MS);
         });
 
         assertThat(thrown).hasMessageThat().contains("too high");
     }
 
     @Test
-    public void getSuggestedAudioContext_withNullActivePlayback_fails() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
+    public void createCarVolume_withNullSystemClock_fails() {
+        expectThrows(NullPointerException.class, () -> {
+            new CarVolume(null, VERSION_ONE, KEY_EVENT_TIMEOUT_MS);
+        });
+    }
 
-        assertThrows(NullPointerException.class, () -> carVolume.getSuggestedAudioContext(
+    @Test
+    public void getSuggestedAudioContext_withNullActivePlayback_fails() {
+        assertThrows(NullPointerException.class,
+                () -> mCarVolume.getSuggestedAudioContextAndSaveIfFound(
                 null, CALL_STATE_IDLE, new int[0]));
     }
 
     @Test
     public void getSuggestedAudioContext_withNullHallUsages_fails() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
-
-        assertThrows(NullPointerException.class, () -> carVolume.getSuggestedAudioContext(
+        assertThrows(NullPointerException.class,
+                () -> mCarVolume.getSuggestedAudioContextAndSaveIfFound(
                 new ArrayList<>(), CALL_STATE_IDLE, null));
     }
 
     @Test
     public void getSuggestedAudioContext_withNoActivePlaybackAndIdleTelephony_returnsDefault() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
-
-        @AudioContext int suggestedContext = carVolume.getSuggestedAudioContext(new ArrayList<>(),
+        @AudioContext int suggestedContext =
+                mCarVolume.getSuggestedAudioContextAndSaveIfFound(new ArrayList<>(),
                 CALL_STATE_IDLE, new int[0]);
 
         assertThat(suggestedContext).isEqualTo(CarAudioService.DEFAULT_AUDIO_CONTEXT);
@@ -107,21 +131,20 @@
 
     @Test
     public void getSuggestedAudioContext_withOneConfiguration_returnsAssociatedContext() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
         List<Integer> activePlaybackContexts = ImmutableList.of(VOICE_COMMAND);
 
-        @AudioContext int suggestedContext = carVolume
-                .getSuggestedAudioContext(activePlaybackContexts, CALL_STATE_IDLE, new int[0]);
+        @AudioContext int suggestedContext = mCarVolume
+                .getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_IDLE,
+                        new int[0]);
 
         assertThat(suggestedContext).isEqualTo(VOICE_COMMAND);
     }
 
     @Test
     public void getSuggestedAudioContext_withCallStateOffHook_returnsCallContext() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
-
-        @AudioContext int suggestedContext = carVolume.getSuggestedAudioContext(new ArrayList<>(),
-                CALL_STATE_OFFHOOK, new int[0]);
+        @AudioContext int suggestedContext =
+                mCarVolume.getSuggestedAudioContextAndSaveIfFound(new ArrayList<>(),
+                        CALL_STATE_OFFHOOK, new int[0]);
 
         assertThat(suggestedContext).isEqualTo(CALL);
     }
@@ -129,9 +152,10 @@
     @Test
 
     public void getSuggestedAudioContext_withV1AndCallStateRinging_returnsCallRingContext() {
-        CarVolume carVolume = new CarVolume(VERSION_ONE);
+        CarVolume carVolume = new CarVolume(mMockClock, VERSION_ONE, KEY_EVENT_TIMEOUT_MS);
 
-        @AudioContext int suggestedContext = carVolume.getSuggestedAudioContext(new ArrayList<>(),
+        @AudioContext int suggestedContext =
+                carVolume.getSuggestedAudioContextAndSaveIfFound(new ArrayList<>(),
                 CALL_STATE_RINGING, new int[0]);
 
         assertThat(suggestedContext).isEqualTo(CALL_RING);
@@ -139,65 +163,66 @@
 
     @Test
     public void getSuggestedAudioContext_withActivePlayback_returnsHighestPriorityContext() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
         List<Integer> activePlaybackContexts = ImmutableList.of(ALARM, CALL, NOTIFICATION);
 
-        @AudioContext int suggestedContext = carVolume
-                .getSuggestedAudioContext(activePlaybackContexts, CALL_STATE_IDLE, new int[0]);
+        @AudioContext int suggestedContext = mCarVolume
+                .getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_IDLE,
+                        new int[0]);
 
         assertThat(suggestedContext).isEqualTo(CALL);
     }
 
     @Test
     public void getSuggestedAudioContext_withLowerPriorityActivePlaybackAndCall_returnsCall() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
         List<Integer> activePlaybackContexts = ImmutableList.of(ALARM, NOTIFICATION);
 
-        @AudioContext int suggestedContext = carVolume
-                .getSuggestedAudioContext(activePlaybackContexts, CALL_STATE_OFFHOOK, new int[0]);
+        @AudioContext int suggestedContext = mCarVolume
+                .getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_OFFHOOK,
+                        new int[0]);
 
         assertThat(suggestedContext).isEqualTo(CALL);
     }
 
     @Test
     public void getSuggestedAudioContext_withV1AndNavigationConfigurationAndCall_returnsNav() {
-        CarVolume carVolume = new CarVolume(VERSION_ONE);
+        CarVolume carVolume = new CarVolume(mMockClock, VERSION_ONE, KEY_EVENT_TIMEOUT_MS);
         List<Integer> activePlaybackContexts = ImmutableList.of(NAVIGATION);
 
         @AudioContext int suggestedContext = carVolume
-                .getSuggestedAudioContext(activePlaybackContexts, CALL_STATE_OFFHOOK, new int[0]);
+                .getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_OFFHOOK,
+                        new int[0]);
 
         assertThat(suggestedContext).isEqualTo(NAVIGATION);
     }
 
     @Test
     public void getSuggestedAudioContext_withV2AndNavigationConfigurationAndCall_returnsCall() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
         List<Integer> activePlaybackContexts = ImmutableList.of(NAVIGATION);
 
-        @AudioContext int suggestedContext = carVolume
-                .getSuggestedAudioContext(activePlaybackContexts, CALL_STATE_OFFHOOK, new int[0]);
+        @AudioContext int suggestedContext = mCarVolume
+                .getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_OFFHOOK,
+                        new int[0]);
 
         assertThat(suggestedContext).isEqualTo(CALL);
     }
 
     @Test
     public void getSuggestedAudioContext_withUnprioritizedUsage_returnsDefault() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
         List<Integer> activePlaybackContexts = ImmutableList.of(INVALID);
 
-        @AudioContext int suggestedContext = carVolume
-                .getSuggestedAudioContext(activePlaybackContexts, CALL_STATE_IDLE, new int[0]);
+        @AudioContext int suggestedContext = mCarVolume
+                .getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_IDLE,
+                        new int[0]);
 
         assertThat(suggestedContext).isEqualTo(DEFAULT_AUDIO_CONTEXT);
     }
 
     @Test
     public void getSuggestedAudioContext_withHalActiveUsage_returnsHalActive() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
         int[] activeHalUsages = new int[] {USAGE_ASSISTANT};
 
-        @AudioContext int suggestedContext = carVolume.getSuggestedAudioContext(new ArrayList<>(),
+        @AudioContext int suggestedContext =
+                mCarVolume.getSuggestedAudioContextAndSaveIfFound(new ArrayList<>(),
                 CALL_STATE_IDLE, activeHalUsages);
 
         assertThat(suggestedContext).isEqualTo(VOICE_COMMAND);
@@ -205,10 +230,10 @@
 
     @Test
     public void getSuggestedAudioContext_withHalUnprioritizedUsage_returnsDefault() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
         int[] activeHalUsages = new int[] {USAGE_VIRTUAL_SOURCE};
 
-        @AudioContext int suggestedContext = carVolume.getSuggestedAudioContext(new ArrayList<>(),
+        @AudioContext int suggestedContext =
+                mCarVolume.getSuggestedAudioContextAndSaveIfFound(new ArrayList<>(),
                 CALL_STATE_IDLE, activeHalUsages);
 
         assertThat(suggestedContext).isEqualTo(DEFAULT_AUDIO_CONTEXT);
@@ -216,35 +241,34 @@
 
     @Test
     public void getSuggestedAudioContext_withConfigAndHalActiveUsage_returnsConfigActive() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
         int[] activeHalUsages = new int[] {USAGE_ASSISTANT};
         List<Integer> activePlaybackContexts = ImmutableList.of(MUSIC);
 
-        @AudioContext int suggestedContext = carVolume
-                .getSuggestedAudioContext(activePlaybackContexts, CALL_STATE_IDLE, activeHalUsages);
+        @AudioContext int suggestedContext = mCarVolume
+                .getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_IDLE,
+                        activeHalUsages);
 
         assertThat(suggestedContext).isEqualTo(MUSIC);
     }
 
     @Test
     public void getSuggestedAudioContext_withConfigAndHalActiveUsage_returnsHalActive() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
         int[] activeHalUsages = new int[] {USAGE_MEDIA};
         List<Integer> activePlaybackContexts = ImmutableList.of(VOICE_COMMAND);
 
-        @AudioContext int suggestedContext = carVolume
-                .getSuggestedAudioContext(activePlaybackContexts, CALL_STATE_IDLE, activeHalUsages);
+        @AudioContext int suggestedContext = mCarVolume
+                .getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_IDLE,
+                        activeHalUsages);
 
         assertThat(suggestedContext).isEqualTo(MUSIC);
     }
 
     @Test
     public void getSuggestedAudioContext_withHalActiveUsageAndActiveCall_returnsCall() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
         int[] activeHalUsages = new int[] {USAGE_MEDIA};
         List<Integer> activePlaybackContexts = new ArrayList<>();
 
-        @AudioContext int suggestedContext = carVolume.getSuggestedAudioContext(
+        @AudioContext int suggestedContext = mCarVolume.getSuggestedAudioContextAndSaveIfFound(
                 activePlaybackContexts, CALL_STATE_OFFHOOK, activeHalUsages);
 
         assertThat(suggestedContext).isEqualTo(CALL);
@@ -252,16 +276,130 @@
 
     @Test
     public void getSuggestedAudioContext_withMultipleHalActiveUsages_returnsMusic() {
-        CarVolume carVolume = new CarVolume(VERSION_TWO);
         int[] activeHalUsages = new int[] {USAGE_MEDIA, USAGE_ANNOUNCEMENT, USAGE_ASSISTANT};
         List<Integer> activePlaybackContexts = new ArrayList<>();
 
-        @AudioContext int suggestedContext = carVolume
-                .getSuggestedAudioContext(activePlaybackContexts, CALL_STATE_IDLE, activeHalUsages);
+        @AudioContext int suggestedContext = mCarVolume
+                .getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_IDLE,
+                        activeHalUsages);
 
         assertThat(suggestedContext).isEqualTo(MUSIC);
     }
 
+    @Test
+    public void getSuggestedAudioContext_withStillActiveContext_returnsPrevActiveContext() {
+        List<Integer> activePlaybackContexts = ImmutableList.of(VOICE_COMMAND);
+
+        mCarVolume.getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_IDLE,
+                new int[0]);
+
+        when(mMockClock.uptimeMillis()).thenReturn(START_TIME_ONE_SECOND);
+
+        @AudioContext int suggestedContext =
+                mCarVolume.getSuggestedAudioContextAndSaveIfFound(new ArrayList<>(),
+                CALL_STATE_IDLE, new int[0]);
+
+        assertThat(suggestedContext).isEqualTo(VOICE_COMMAND);
+    }
+
+    @Test
+    public void
+            getSuggestedAudioContext_withStillActiveContext_returnPrevActiveContextMultipleTimes() {
+        List<Integer> activePlaybackContexts = ImmutableList.of(VOICE_COMMAND);
+
+        mCarVolume.getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_IDLE,
+                new int[0]);
+
+        long deltaTime = KEY_EVENT_TIMEOUT_MS - 1;
+        for (int volumeCounter = 1; volumeCounter < TRIAL_COUNTS; volumeCounter++) {
+            when(mMockClock.uptimeMillis())
+                    .thenReturn(START_TIME + (volumeCounter * deltaTime));
+
+            @AudioContext int suggestedContext =
+                    mCarVolume.getSuggestedAudioContextAndSaveIfFound(new ArrayList<>(),
+                            CALL_STATE_IDLE, new int[0]);
+            assertThat(suggestedContext).isEqualTo(VOICE_COMMAND);
+        }
+    }
+
+    @Test
+    public void
+            getSuggestedAudioContext_withActContextAndNewHigherPrioContext_returnPrevActContext() {
+        List<Integer> activePlaybackContexts = ImmutableList.of(VOICE_COMMAND);
+
+        mCarVolume.getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_IDLE,
+                new int[0]);
+
+        when(mMockClock.uptimeMillis()).thenReturn(START_TIME_ONE_SECOND);
+
+        @AudioContext int suggestedContext =
+                mCarVolume.getSuggestedAudioContextAndSaveIfFound(new ArrayList<>(),
+                CALL_STATE_OFFHOOK, new int[0]);
+
+        assertThat(suggestedContext).isEqualTo(VOICE_COMMAND);
+    }
+
+    @Test
+    public void getSuggestedAudioContext_afterActiveContextTimeout_returnsDefaultContext() {
+        List<Integer> activePlaybackContexts = ImmutableList.of(VOICE_COMMAND);
+
+        mCarVolume.getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_IDLE,
+                        new int[0]);
+
+        when(mMockClock.uptimeMillis()).thenReturn(START_TIME_FOUR_SECOND);
+
+        @AudioContext int suggestedContext =
+                mCarVolume.getSuggestedAudioContextAndSaveIfFound(new ArrayList<>(),
+                CALL_STATE_IDLE, new int[0]);
+
+        assertThat(suggestedContext).isEqualTo(DEFAULT_AUDIO_CONTEXT);
+    }
+
+    @Test
+    public void
+            getSuggestedAudioContext_afterActiveContextTimeoutAndNewContext_returnsNewContext() {
+        List<Integer> activePlaybackContexts = ImmutableList.of(VOICE_COMMAND);
+
+        mCarVolume.getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_IDLE,
+                new int[0]);
+
+        when(mMockClock.uptimeMillis()).thenReturn(START_TIME_FOUR_SECOND);
+
+        @AudioContext int suggestedContext =
+                mCarVolume.getSuggestedAudioContextAndSaveIfFound(new ArrayList<>(),
+                CALL_STATE_OFFHOOK, new int[0]);
+
+        assertThat(suggestedContext).isEqualTo(CALL);
+    }
+
+    @Test
+    public void
+            getSuggestedAudioContext_afterMultipleQueriesAndNewContextCall_returnsNewContext() {
+        List<Integer> activePlaybackContexts = ImmutableList.of(VOICE_COMMAND);
+
+        mCarVolume.getSuggestedAudioContextAndSaveIfFound(activePlaybackContexts, CALL_STATE_IDLE,
+                new int[0]);
+
+
+        long deltaTime = KEY_EVENT_TIMEOUT_MS - 1;
+
+        for (int volumeCounter = 1; volumeCounter < TRIAL_COUNTS; volumeCounter++) {
+            when(mMockClock.uptimeMillis()).thenReturn(START_TIME + volumeCounter * deltaTime);
+
+            mCarVolume.getSuggestedAudioContextAndSaveIfFound(new ArrayList<>(), CALL_STATE_IDLE,
+                            new int[0]);
+        }
+
+        when(mMockClock.uptimeMillis())
+                .thenReturn(START_TIME + (TRIAL_COUNTS * deltaTime) + KEY_EVENT_TIMEOUT_MS);
+
+        @AudioContext int newContext =
+                mCarVolume.getSuggestedAudioContextAndSaveIfFound(new ArrayList<>(),
+                CALL_STATE_OFFHOOK, new int[0]);
+
+        assertThat(newContext).isEqualTo(CALL);
+    }
+
 
     @Test
     public void isAnyContextActive_withOneConfigurationAndMatchedContext_returnsTrue() {
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/hal/AudioControlWrapperAidlTest.java b/tests/carservice_unit_test/src/com/android/car/audio/hal/AudioControlWrapperAidlTest.java
index 3613ad0..8e67c32 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/hal/AudioControlWrapperAidlTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/hal/AudioControlWrapperAidlTest.java
@@ -16,8 +16,12 @@
 
 package com.android.car.audio.hal;
 
+import static android.media.AudioAttributes.USAGE_MEDIA;
+import static android.media.AudioAttributes.USAGE_NOTIFICATION;
 import static android.os.IBinder.DeathRecipient;
 
+import static com.android.car.audio.hal.AudioControlWrapper.AUDIOCONTROL_FEATURE_AUDIO_DUCKING;
+import static com.android.car.audio.hal.AudioControlWrapper.AUDIOCONTROL_FEATURE_AUDIO_FOCUS;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -29,17 +33,19 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
 
 import android.audio.policy.configuration.V7_0.AudioUsage;
 import android.car.test.mocks.AbstractExtendedMockitoTestCase;
+import android.hardware.automotive.audiocontrol.DuckingInfo;
 import android.hardware.automotive.audiocontrol.IAudioControl;
 import android.hardware.automotive.audiocontrol.IFocusListener;
-import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.os.IBinder;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
+import com.android.car.audio.CarDuckingInfo;
 import com.android.car.audio.hal.AudioControlWrapper.AudioControlDeathRecipient;
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 
@@ -49,12 +55,14 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+
 @RunWith(AndroidJUnit4.class)
 public final class AudioControlWrapperAidlTest extends AbstractExtendedMockitoTestCase {
-    private static final String TAG = AudioControlWrapperAidlTest.class.getSimpleName();
     private static final float FADE_VALUE = 5;
     private static final float BALANCE_VALUE = 6;
-    private static final int USAGE = AudioAttributes.USAGE_MEDIA;
+    private static final int USAGE = USAGE_MEDIA;
     private static final String USAGE_NAME = AudioUsage.AUDIO_USAGE_MEDIA.toString();
     private static final int ZONE_ID = 2;
     private static final int FOCUS_GAIN = AudioManager.AUDIOFOCUS_GAIN;
@@ -97,8 +105,20 @@
     }
 
     @Test
-    public void supportsHalAudioFocus_returnsTrue() {
-        assertThat(mAudioControlWrapperAidl.supportsHalAudioFocus()).isTrue();
+    public void supportsFeature_forAudioFocus_returnsTrue() {
+        assertThat(mAudioControlWrapperAidl.supportsFeature(AUDIOCONTROL_FEATURE_AUDIO_FOCUS))
+                .isTrue();
+    }
+
+    @Test
+    public void supportsFeature_forAudioDucking_returnsTrue() {
+        assertThat(mAudioControlWrapperAidl.supportsFeature(AUDIOCONTROL_FEATURE_AUDIO_DUCKING))
+                .isTrue();
+    }
+
+    @Test
+    public void supportsFeature_forUnknownFeature_returnsFalse() {
+        assertThat(mAudioControlWrapperAidl.supportsFeature(-1)).isFalse();
     }
 
     @Test
@@ -117,6 +137,85 @@
     }
 
     @Test
+    public void onDevicesToDuckChange_withNullDuckingInfo_throws() {
+        assertThrows(NullPointerException.class,
+                () -> mAudioControlWrapperAidl.onDevicesToDuckChange(null));
+    }
+
+    @Test
+    public void onDevicesToDuckChange_callsHalWithDuckingInfo() throws Exception {
+        CarDuckingInfo carDuckingInfo = new CarDuckingInfo(ZONE_ID, new ArrayList<>(),
+                new ArrayList<>(), new int[0]);
+
+        mAudioControlWrapperAidl.onDevicesToDuckChange(carDuckingInfo);
+
+        ArgumentCaptor<DuckingInfo[]> captor = ArgumentCaptor.forClass(DuckingInfo[].class);
+        verify(mAudioControl).onDevicesToDuckChange(captor.capture());
+        DuckingInfo[] duckingInfos = captor.getValue();
+        assertThat(duckingInfos).hasLength(1);
+    }
+
+    @Test
+    public void onDevicesToDuckChange_convertsUsagesToXsdStrings() throws Exception {
+        CarDuckingInfo carDuckingInfo = new CarDuckingInfo(ZONE_ID, new ArrayList<>(),
+                new ArrayList<>(), new int[]{USAGE_MEDIA, USAGE_NOTIFICATION});
+
+        mAudioControlWrapperAidl.onDevicesToDuckChange(carDuckingInfo);
+
+        ArgumentCaptor<DuckingInfo[]> captor = ArgumentCaptor.forClass(DuckingInfo[].class);
+        verify(mAudioControl).onDevicesToDuckChange(captor.capture());
+        DuckingInfo duckingInfo = captor.getValue()[0];
+        assertThat(duckingInfo.usagesHoldingFocus).asList()
+                .containsExactly(AudioUsage.AUDIO_USAGE_MEDIA.toString(),
+                        AudioUsage.AUDIO_USAGE_NOTIFICATION.toString());
+    }
+
+    @Test
+    public void onDevicesToDuckChange_passesAlongAddressesToDuck() throws Exception {
+        String mediaAddress = "media_bus";
+        String navigationAddress = "navigation_bus";
+        CarDuckingInfo carDuckingInfo = new CarDuckingInfo(ZONE_ID,
+                Arrays.asList(mediaAddress, navigationAddress), new ArrayList<>(), new int[0]);
+
+        mAudioControlWrapperAidl.onDevicesToDuckChange(carDuckingInfo);
+
+        ArgumentCaptor<DuckingInfo[]> captor = ArgumentCaptor.forClass(DuckingInfo[].class);
+        verify(mAudioControl).onDevicesToDuckChange(captor.capture());
+        DuckingInfo duckingInfo = captor.getValue()[0];
+        assertThat(duckingInfo.deviceAddressesToDuck).asList()
+                .containsExactly(mediaAddress, navigationAddress);
+    }
+
+    @Test
+    public void onDevicesToDuckChange_passesAlongAddressesToUnduck() throws Exception {
+        String notificationAddress = "notification_bus";
+        String callAddress = "call_address";
+        CarDuckingInfo carDuckingInfo = new CarDuckingInfo(ZONE_ID, new ArrayList<>(),
+                Arrays.asList(notificationAddress, callAddress), new int[0]);
+
+        mAudioControlWrapperAidl.onDevicesToDuckChange(carDuckingInfo);
+
+        ArgumentCaptor<DuckingInfo[]> captor = ArgumentCaptor.forClass(DuckingInfo[].class);
+        verify(mAudioControl).onDevicesToDuckChange(captor.capture());
+        DuckingInfo duckingInfo = captor.getValue()[0];
+        assertThat(duckingInfo.deviceAddressesToUnduck).asList()
+                .containsExactly(notificationAddress, callAddress);
+    }
+
+    @Test
+    public void onDevicesToDuckChange_passesAlongZoneId() throws Exception {
+        CarDuckingInfo carDuckingInfo = new CarDuckingInfo(ZONE_ID, new ArrayList<>(),
+                new ArrayList<>(), new int[0]);
+
+        mAudioControlWrapperAidl.onDevicesToDuckChange(carDuckingInfo);
+
+        ArgumentCaptor<DuckingInfo[]> captor = ArgumentCaptor.forClass(DuckingInfo[].class);
+        verify(mAudioControl).onDevicesToDuckChange(captor.capture());
+        DuckingInfo duckingInfo = captor.getValue()[0];
+        assertThat(duckingInfo.zoneId).isEqualTo(ZONE_ID);
+    }
+
+    @Test
     public void linkToDeath_callsBinder() throws Exception {
         mAudioControlWrapperAidl.linkToDeath(null);
 
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/hal/AudioControlWrapperV1Test.java b/tests/carservice_unit_test/src/com/android/car/audio/hal/AudioControlWrapperV1Test.java
index 1c87103..e65625e 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/hal/AudioControlWrapperV1Test.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/hal/AudioControlWrapperV1Test.java
@@ -16,6 +16,9 @@
 
 package com.android.car.audio.hal;
 
+import static com.android.car.audio.hal.AudioControlWrapper.AUDIOCONTROL_FEATURE_AUDIO_DUCKING;
+import static com.android.car.audio.hal.AudioControlWrapper.AUDIOCONTROL_FEATURE_AUDIO_FOCUS;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.mock;
@@ -78,10 +81,26 @@
     }
 
     @Test
-    public void supportsHalAudioFocus_returnsFalse() {
+    public void supportsFeature_withAudioFocus_returnsFalse() {
         AudioControlWrapperV1 audioControlWrapperV1 = new AudioControlWrapperV1(mAudioControlV1);
 
-        assertThat(audioControlWrapperV1.supportsHalAudioFocus()).isFalse();
+        assertThat(audioControlWrapperV1.supportsFeature(AUDIOCONTROL_FEATURE_AUDIO_FOCUS))
+                .isFalse();
+    }
+
+    @Test
+    public void supportsFeature_withAudioDucking_returnsFalse() {
+        AudioControlWrapperV1 audioControlWrapperV1 = new AudioControlWrapperV1(mAudioControlV1);
+
+        assertThat(audioControlWrapperV1.supportsFeature(AUDIOCONTROL_FEATURE_AUDIO_DUCKING))
+                .isFalse();
+    }
+
+    @Test
+    public void supportsFeature_withUnknownFeature_returnsFalse() {
+        AudioControlWrapperV1 audioControlWrapperV1 = new AudioControlWrapperV1(mAudioControlV1);
+
+        assertThat(audioControlWrapperV1.supportsFeature(-1)).isFalse();
     }
 
     @Test
@@ -109,4 +128,12 @@
         assertThrows(UnsupportedOperationException.class,
                 () -> audioControlWrapperV1.onAudioFocusChange(USAGE, ZONE_ID, FOCUS_GAIN));
     }
+
+    @Test
+    public void onDevicesToDuckChange_throws() {
+        AudioControlWrapperV1 audioControlWrapperV1 = new AudioControlWrapperV1(mAudioControlV1);
+
+        assertThrows(UnsupportedOperationException.class,
+                () -> audioControlWrapperV1.onDevicesToDuckChange(null));
+    }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/hal/AudioControlWrapperV2Test.java b/tests/carservice_unit_test/src/com/android/car/audio/hal/AudioControlWrapperV2Test.java
index 80d566a..5fb4674 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/hal/AudioControlWrapperV2Test.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/hal/AudioControlWrapperV2Test.java
@@ -16,12 +16,16 @@
 
 package com.android.car.audio.hal;
 
+import static com.android.car.audio.hal.AudioControlWrapper.AUDIOCONTROL_FEATURE_AUDIO_DUCKING;
+import static com.android.car.audio.hal.AudioControlWrapper.AUDIOCONTROL_FEATURE_AUDIO_FOCUS;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
 
 import android.hardware.automotive.audiocontrol.V2_0.IAudioControl;
 import android.hardware.automotive.audiocontrol.V2_0.ICloseHandle;
@@ -69,10 +73,26 @@
     }
 
     @Test
-    public void supportsHalAudioFocus_returnsTrue() {
+    public void supportsFeature_audioFocus_returnsTrue() {
         AudioControlWrapperV2 audioControlWrapperV2 = new AudioControlWrapperV2(mAudioControlV2);
 
-        assertThat(audioControlWrapperV2.supportsHalAudioFocus()).isTrue();
+        assertThat(audioControlWrapperV2.supportsFeature(AUDIOCONTROL_FEATURE_AUDIO_FOCUS))
+                .isTrue();
+    }
+
+    @Test
+    public void supportsFeature_withUnknownFeature_returnsFalse() {
+        AudioControlWrapperV2 audioControlWrapperV2 = new AudioControlWrapperV2(mAudioControlV2);
+
+        assertThat(audioControlWrapperV2.supportsFeature(-1)).isFalse();
+    }
+
+    @Test
+    public void supportsFeature_withAudioDucking_returnsFalse() {
+        AudioControlWrapperV2 audioControlWrapperV2 = new AudioControlWrapperV2(mAudioControlV2);
+
+        assertThat(audioControlWrapperV2.supportsFeature(AUDIOCONTROL_FEATURE_AUDIO_DUCKING))
+                .isFalse();
     }
 
     @Test
@@ -107,4 +127,12 @@
 
         verify(mAudioControlV2).onAudioFocusChange(USAGE, ZONE_ID, FOCUS_GAIN);
     }
+
+    @Test
+    public void onDevicesToDuckChange_throws() {
+        AudioControlWrapperV2 audioControlWrapperV2 = new AudioControlWrapperV2(mAudioControlV2);
+
+        assertThrows(UnsupportedOperationException.class,
+                () -> audioControlWrapperV2.onDevicesToDuckChange(null));
+    }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/power/CarPowerManagementServiceUnitTest.java b/tests/carservice_unit_test/src/com/android/car/power/CarPowerManagementServiceUnitTest.java
index bed39f9..3f18b01 100644
--- a/tests/carservice_unit_test/src/com/android/car/power/CarPowerManagementServiceUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/power/CarPowerManagementServiceUnitTest.java
@@ -417,6 +417,14 @@
     }
 
     @Test
+    public void testApplySystemPowerPolicyFromApps() {
+        grantPowerPolicyPermission();
+        String policyId = "system_power_policy_no_user_interaction";
+
+        assertThrows(IllegalArgumentException.class, () -> mService.applyPowerPolicy(policyId));
+    }
+
+    @Test
     public void testRegisterPowerPolicyChangeListener() throws Exception {
         grantPowerPolicyPermission();
         String policyId = "policy_id_enable_audio_disable_wifi";