Merge changes from topics "presubmit-am-7ed5c317d7b347f1a59eb332f20e8522", "presubmit-am-86ce18b053454a4db98a349f8b39985e", "presubmit-am-94d3142a4e1c418fadfd6eca6208920a", "presubmit-am-a466ed1382304d549cb550a5cae804e6" into tm-dev

* changes:
  Add Absolute Volume Control support for a TV as the System Audio device
  Implement support for Absolute Volume Control
  Add call to onBootPhase to tests that use HdmiControlService
  Add wrapper for AudioDeviceVolumeManager in the HDMI framework
diff --git a/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java
new file mode 100644
index 0000000..d7563e0
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 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.server.hdmi;
+
+/**
+ * Action to query and track the audio status of the System Audio device when enabling or using
+ * Absolute Volume Control. Must be removed when AVC is disabled. Performs two main functions:
+ * 1. When enabling AVC: queries the starting audio status of the System Audio device and
+ *    enables the feature upon receiving a response.
+ * 2. While AVC is enabled: monitors <Report Audio Status> messages from the System Audio device and
+ *    notifies AudioService if the audio status changes.
+ */
+final class AbsoluteVolumeAudioStatusAction extends HdmiCecFeatureAction {
+    private static final String TAG = "AbsoluteVolumeAudioStatusAction";
+
+    private int mInitialAudioStatusRetriesLeft = 2;
+
+    private static final int STATE_WAIT_FOR_INITIAL_AUDIO_STATUS = 1;
+    private static final int STATE_MONITOR_AUDIO_STATUS = 2;
+
+    private final int mTargetAddress;
+
+    private AudioStatus mLastAudioStatus;
+
+    AbsoluteVolumeAudioStatusAction(HdmiCecLocalDevice source, int targetAddress) {
+        super(source);
+        mTargetAddress = targetAddress;
+    }
+
+    @Override
+    boolean start() {
+        mState = STATE_WAIT_FOR_INITIAL_AUDIO_STATUS;
+        sendGiveAudioStatus();
+        return true;
+    }
+
+    void updateVolume(int volumeIndex) {
+        mLastAudioStatus = new AudioStatus(volumeIndex, mLastAudioStatus.getMute());
+    }
+
+    private void sendGiveAudioStatus() {
+        addTimer(mState, HdmiConfig.TIMEOUT_MS);
+        sendCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(getSourceAddress(), mTargetAddress));
+    }
+
+    @Override
+    boolean processCommand(HdmiCecMessage cmd) {
+        switch (cmd.getOpcode()) {
+            case Constants.MESSAGE_REPORT_AUDIO_STATUS:
+                return handleReportAudioStatus(cmd);
+        }
+
+        return false;
+    }
+
+    private boolean handleReportAudioStatus(HdmiCecMessage cmd) {
+        if (mTargetAddress != cmd.getSource() || cmd.getParams().length == 0) {
+            return false;
+        }
+
+        boolean mute = HdmiUtils.isAudioStatusMute(cmd);
+        int volume = HdmiUtils.getAudioStatusVolume(cmd);
+        AudioStatus audioStatus = new AudioStatus(volume, mute);
+        if (mState == STATE_WAIT_FOR_INITIAL_AUDIO_STATUS) {
+            localDevice().getService().enableAbsoluteVolumeControl(audioStatus);
+            mState = STATE_MONITOR_AUDIO_STATUS;
+        } else if (mState == STATE_MONITOR_AUDIO_STATUS) {
+            if (audioStatus.getVolume() != mLastAudioStatus.getVolume()) {
+                localDevice().getService().notifyAvcVolumeChange(audioStatus.getVolume());
+            }
+            if (audioStatus.getMute() != mLastAudioStatus.getMute()) {
+                localDevice().getService().notifyAvcMuteChange(audioStatus.getMute());
+            }
+        }
+        mLastAudioStatus = audioStatus;
+
+        return true;
+    }
+
+    @Override
+    void handleTimerEvent(int state) {
+        if (mState != state) {
+            return;
+        } else if (mInitialAudioStatusRetriesLeft > 0) {
+            mInitialAudioStatusRetriesLeft--;
+            sendGiveAudioStatus();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java
new file mode 100644
index 0000000..438c1ea
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 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.server.hdmi;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceVolumeManager;
+import android.media.VolumeInfo;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Wrapper for {@link AudioDeviceVolumeManager}. Creates an instance of the class and directly
+ * passes method calls to that instance.
+ */
+public class AudioDeviceVolumeManagerWrapper
+        implements AudioDeviceVolumeManagerWrapperInterface {
+
+    private static final String TAG = "AudioDeviceVolumeManagerWrapper";
+
+    private final AudioDeviceVolumeManager mAudioDeviceVolumeManager;
+
+    public AudioDeviceVolumeManagerWrapper(Context context) {
+        mAudioDeviceVolumeManager = new AudioDeviceVolumeManager(context);
+    }
+
+    @Override
+    public void addOnDeviceVolumeBehaviorChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener)
+            throws SecurityException {
+        mAudioDeviceVolumeManager.addOnDeviceVolumeBehaviorChangedListener(executor, listener);
+    }
+
+    @Override
+    public void removeOnDeviceVolumeBehaviorChangedListener(
+            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener) {
+        mAudioDeviceVolumeManager.removeOnDeviceVolumeBehaviorChangedListener(listener);
+    }
+
+    @Override
+    public void setDeviceAbsoluteVolumeBehavior(
+            @NonNull AudioDeviceAttributes device,
+            @NonNull VolumeInfo volume,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
+            boolean handlesVolumeAdjustment) {
+        mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume, executor,
+                vclistener, handlesVolumeAdjustment);
+    }
+}
diff --git a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapperInterface.java b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapperInterface.java
new file mode 100644
index 0000000..1a1d4c1
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapperInterface.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 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.server.hdmi;
+
+import static android.media.AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener;
+import static android.media.AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceVolumeManager;
+import android.media.VolumeInfo;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Interface with the methods from {@link AudioDeviceVolumeManager} used by the HDMI framework.
+ * Allows the class to be faked for tests.
+ */
+public interface AudioDeviceVolumeManagerWrapperInterface {
+
+    /**
+     * Wrapper for {@link AudioDeviceVolumeManager#addOnDeviceVolumeBehaviorChangedListener(
+     * Executor, OnDeviceVolumeBehaviorChangedListener)}
+     */
+    void addOnDeviceVolumeBehaviorChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener);
+
+    /**
+     * Wrapper for {@link AudioDeviceVolumeManager#removeOnDeviceVolumeBehaviorChangedListener(
+     * OnDeviceVolumeBehaviorChangedListener)}
+     */
+    void removeOnDeviceVolumeBehaviorChangedListener(
+            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener);
+
+    /**
+     * Wrapper for {@link AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior(
+     * AudioDeviceAttributes, VolumeInfo, Executor, OnAudioDeviceVolumeChangedListener, boolean)}
+     */
+    void setDeviceAbsoluteVolumeBehavior(
+            @NonNull AudioDeviceAttributes device,
+            @NonNull VolumeInfo volume,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
+            boolean handlesVolumeAdjustment);
+}
diff --git a/services/core/java/com/android/server/hdmi/AudioStatus.java b/services/core/java/com/android/server/hdmi/AudioStatus.java
new file mode 100644
index 0000000..a884ffb
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/AudioStatus.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 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.server.hdmi;
+
+import android.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Immutable representation of the information in the [Audio Status] operand:
+ * volume status (0 <= N <= 100) and mute status (muted or unmuted).
+ */
+public class AudioStatus {
+    public static final int MAX_VOLUME = 100;
+    public static final int MIN_VOLUME = 0;
+
+    int mVolume;
+    boolean mMute;
+
+    public AudioStatus(int volume, boolean mute) {
+        mVolume = volume;
+        mMute = mute;
+    }
+
+    public int getVolume() {
+        return mVolume;
+    }
+
+    public boolean getMute() {
+        return mMute;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof AudioStatus)) {
+            return false;
+        }
+
+        AudioStatus other = (AudioStatus) obj;
+        return mVolume == other.mVolume
+                && mMute == other.mMute;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mVolume, mMute);
+    }
+
+    @Override
+    public String toString() {
+        return "AudioStatus mVolume:" + mVolume + " mMute:" + mMute;
+    }
+}
diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java
index 751f2db..157057d 100644
--- a/services/core/java/com/android/server/hdmi/Constants.java
+++ b/services/core/java/com/android/server/hdmi/Constants.java
@@ -118,6 +118,7 @@
             MESSAGE_SYSTEM_AUDIO_MODE_REQUEST,
             MESSAGE_GIVE_AUDIO_STATUS,
             MESSAGE_SET_SYSTEM_AUDIO_MODE,
+            MESSAGE_SET_AUDIO_VOLUME_LEVEL,
             MESSAGE_REPORT_AUDIO_STATUS,
             MESSAGE_GIVE_SYSTEM_AUDIO_MODE_STATUS,
             MESSAGE_SYSTEM_AUDIO_MODE_STATUS,
@@ -197,9 +198,9 @@
     static final int MESSAGE_SYSTEM_AUDIO_MODE_REQUEST = 0x70;
     static final int MESSAGE_GIVE_AUDIO_STATUS = 0x71;
     static final int MESSAGE_SET_SYSTEM_AUDIO_MODE = 0x72;
+    static final int MESSAGE_SET_AUDIO_VOLUME_LEVEL = 0x73;
     static final int MESSAGE_REPORT_AUDIO_STATUS = 0x7A;
     static final int MESSAGE_GIVE_SYSTEM_AUDIO_MODE_STATUS = 0x7D;
-    static final int MESSAGE_SET_AUDIO_VOLUME_LEVEL = 0x73;
     static final int MESSAGE_SYSTEM_AUDIO_MODE_STATUS = 0x7E;
     static final int MESSAGE_ROUTING_CHANGE = 0x80;
     static final int MESSAGE_ROUTING_INFORMATION = 0x81;
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index 26a1613..fb2d2ee 100755
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -303,6 +303,13 @@
         if (dispatchMessageToAction(message)) {
             return Constants.HANDLED;
         }
+
+        // If a message type has its own class, all valid messages of that type
+        // will be represented by an instance of that class.
+        if (message instanceof SetAudioVolumeLevelMessage) {
+            return handleSetAudioVolumeLevel((SetAudioVolumeLevelMessage) message);
+        }
+
         switch (message.getOpcode()) {
             case Constants.MESSAGE_ACTIVE_SOURCE:
                 return handleActiveSource(message);
@@ -637,6 +644,11 @@
         return Constants.NOT_HANDLED;
     }
 
+    @Constants.HandleMessageResult
+    protected int handleSetAudioVolumeLevel(SetAudioVolumeLevelMessage message) {
+        return Constants.NOT_HANDLED;
+    }
+
     @Constants.RcProfile
     protected abstract int getRcProfile();
 
@@ -1002,6 +1014,57 @@
         action.start();
     }
 
+    void addAvcAudioStatusAction(int targetAddress) {
+        if (!hasAction(AbsoluteVolumeAudioStatusAction.class)) {
+            addAndStartAction(new AbsoluteVolumeAudioStatusAction(this, targetAddress));
+        }
+    }
+
+    void removeAvcAudioStatusAction() {
+        removeAction(AbsoluteVolumeAudioStatusAction.class);
+    }
+
+    void updateAvcVolume(int volumeIndex) {
+        for (AbsoluteVolumeAudioStatusAction action :
+                getActions(AbsoluteVolumeAudioStatusAction.class)) {
+            action.updateVolume(volumeIndex);
+        }
+    }
+
+    /**
+     * Determines whether {@code targetAddress} supports <Set Audio Volume Level>. Does two things
+     * in parallel: send <Give Features> (to get <Report Features> in response),
+     * and send <Set Audio Volume Level> (to see if it gets a <Feature Abort> in response).
+     */
+    @ServiceThreadOnly
+    void queryAvcSupport(int targetAddress) {
+        assertRunOnServiceThread();
+
+        // Send <Give Features> if using CEC 2.0 or above.
+        if (mService.getCecVersion() >= HdmiControlManager.HDMI_CEC_VERSION_2_0) {
+            synchronized (mLock) {
+                mService.sendCecCommand(HdmiCecMessageBuilder.buildGiveFeatures(
+                        getDeviceInfo().getLogicalAddress(), targetAddress));
+            }
+        }
+
+        // If we don't already have a {@link SetAudioVolumeLevelDiscoveryAction} for the target
+        // device, start one.
+        List<SetAudioVolumeLevelDiscoveryAction> savlDiscoveryActions =
+                getActions(SetAudioVolumeLevelDiscoveryAction.class);
+        if (savlDiscoveryActions.stream().noneMatch(a -> a.getTargetAddress() == targetAddress)) {
+            addAndStartAction(new SetAudioVolumeLevelDiscoveryAction(this, targetAddress,
+                    new IHdmiControlCallback.Stub() {
+                            @Override
+                            public void onComplete(int result) {
+                                if (result == HdmiControlManager.RESULT_SUCCESS) {
+                                    getService().checkAndUpdateAbsoluteVolumeControlState();
+                                }
+                            }
+                        }));
+        }
+    }
+
     @ServiceThreadOnly
     void startQueuedActions() {
         assertRunOnServiceThread();
@@ -1205,6 +1268,9 @@
      */
     protected void disableDevice(
             boolean initiatedByCec, final PendingActionClearedCallback originalCallback) {
+        removeAction(AbsoluteVolumeAudioStatusAction.class);
+        removeAction(SetAudioVolumeLevelDiscoveryAction.class);
+
         mPendingActionClearedCallback =
                 new PendingActionClearedCallback() {
                     @Override
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java
index 90b4f76..c0c0202 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java
@@ -307,6 +307,7 @@
     protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
         removeAction(OneTouchPlayAction.class);
         removeAction(DevicePowerStatusAction.class);
+        removeAction(AbsoluteVolumeAudioStatusAction.class);
 
         super.disableDevice(initiatedByCec, callback);
     }
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 9212fb6..1ea1457 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -1166,6 +1166,19 @@
         return Constants.HANDLED;
     }
 
+    @Override
+    @Constants.HandleMessageResult
+    protected int handleSetAudioVolumeLevel(SetAudioVolumeLevelMessage message) {
+        // <Set Audio Volume Level> should only be sent to the System Audio device, so we don't
+        // handle it when System Audio Mode is enabled.
+        if (mService.isSystemAudioActivated()) {
+            return Constants.ABORT_NOT_IN_CORRECT_MODE;
+        } else {
+            mService.setStreamMusicVolume(message.getAudioVolumeLevel(), 0);
+            return Constants.HANDLED;
+        }
+    }
+
     void announceOneTouchRecordResult(int recorderAddress, int result) {
         mService.invokeOneTouchRecordResult(recorderAddress, result);
     }
@@ -1202,6 +1215,13 @@
         return mService.getHdmiCecNetwork().getSafeCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
     }
 
+    /**
+     * Returns the audio output device used for System Audio Mode.
+     */
+    AudioDeviceAttributes getSystemAudioOutputDevice() {
+        return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI_ARC;
+    }
+
 
     @ServiceThreadOnly
     void handleRemoveActiveRoutingPath(int path) {
@@ -1296,6 +1316,7 @@
         removeAction(OneTouchRecordAction.class);
         removeAction(TimerRecordingAction.class);
         removeAction(NewDeviceAction.class);
+        removeAction(AbsoluteVolumeAudioStatusAction.class);
 
         disableSystemAudioIfExist();
         disableArcIfExist();
@@ -1318,7 +1339,6 @@
         removeAction(SystemAudioActionFromAvr.class);
         removeAction(SystemAudioActionFromTv.class);
         removeAction(SystemAudioAutoInitiationAction.class);
-        removeAction(SystemAudioStatusAction.class);
         removeAction(VolumeControlAction.class);
 
         if (!mService.isControlEnabled()) {
@@ -1589,6 +1609,7 @@
         return DeviceFeatures.NO_FEATURES_SUPPORTED.toBuilder()
                 .setRecordTvScreenSupport(FEATURE_SUPPORTED)
                 .setArcTxSupport(hasArcPort ? FEATURE_SUPPORTED : FEATURE_NOT_SUPPORTED)
+                .setSetAudioVolumeLevelSupport(FEATURE_SUPPORTED)
                 .build();
     }
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessage.java b/services/core/java/com/android/server/hdmi/HdmiCecMessage.java
index 290cae5..2b84225 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessage.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessage.java
@@ -262,6 +262,8 @@
                 return "Give Audio Status";
             case Constants.MESSAGE_SET_SYSTEM_AUDIO_MODE:
                 return "Set System Audio Mode";
+            case Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL:
+                return "Set Audio Volume Level";
             case Constants.MESSAGE_REPORT_AUDIO_STATUS:
                 return "Report Audio Status";
             case Constants.MESSAGE_GIVE_SYSTEM_AUDIO_MODE_STATUS:
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
index 8b6d16a..caaf800 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
@@ -259,6 +259,7 @@
             // The addition of a local device should not notify listeners
             return;
         }
+        mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState();
         if (info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) {
             // Don't notify listeners of devices that haven't reported their physical address yet
             return;
@@ -383,7 +384,7 @@
     final void removeCecDevice(HdmiCecLocalDevice localDevice, int address) {
         assertRunOnServiceThread();
         HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address));
-
+        mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState();
         localDevice.mCecMessageCache.flushMessagesFrom(address);
         if (info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) {
             // Don't notify listeners of devices that haven't reported their physical address yet
@@ -586,6 +587,8 @@
                 .build();
 
         updateCecDevice(newDeviceInfo);
+
+        mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState();
     }
 
     @ServiceThreadOnly
@@ -617,6 +620,8 @@
                     )
                     .build();
             updateCecDevice(newDeviceInfo);
+
+            mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState();
         }
     }
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 12380ab..9824b4e 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -30,6 +30,7 @@
 import static com.android.server.power.ShutdownThread.SHUTDOWN_ACTION_PROPERTY;
 
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
@@ -38,6 +39,7 @@
 import android.content.IntentFilter;
 import android.database.ContentObserver;
 import android.hardware.display.DisplayManager;
+import android.hardware.hdmi.DeviceFeatures;
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiHotplugEvent;
@@ -56,7 +58,12 @@
 import android.hardware.hdmi.IHdmiVendorCommandListener;
 import android.hardware.tv.cec.V1_0.OptionKey;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioDeviceVolumeManager;
 import android.media.AudioManager;
+import android.media.VolumeInfo;
 import android.media.session.MediaController;
 import android.media.session.MediaSessionManager;
 import android.media.tv.TvInputManager;
@@ -83,6 +90,7 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
+import android.view.KeyEvent;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -101,6 +109,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -202,6 +211,27 @@
     public @interface WakeReason {
     }
 
+    @VisibleForTesting
+    static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_HDMI = new AudioDeviceAttributes(
+            AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI, "");
+    @VisibleForTesting
+    static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_HDMI_ARC =
+            new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
+                    AudioDeviceInfo.TYPE_HDMI_ARC, "");
+    static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_HDMI_EARC =
+            new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
+                    AudioDeviceInfo.TYPE_HDMI_EARC, "");
+
+    // Audio output devices used for Absolute Volume Control
+    private static final List<AudioDeviceAttributes> AVC_AUDIO_OUTPUT_DEVICES =
+            Collections.unmodifiableList(Arrays.asList(AUDIO_OUTPUT_DEVICE_HDMI,
+                    AUDIO_OUTPUT_DEVICE_HDMI_ARC, AUDIO_OUTPUT_DEVICE_HDMI_EARC));
+
+    // AudioAttributes for STREAM_MUSIC
+    @VisibleForTesting
+    static final AudioAttributes STREAM_MUSIC_ATTRIBUTES =
+            new AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build();
+
     private final Executor mServiceThreadExecutor = new Executor() {
         @Override
         public void execute(Runnable r) {
@@ -209,6 +239,10 @@
         }
     };
 
+    Executor getServiceThreadExecutor() {
+        return mServiceThreadExecutor;
+    }
+
     // Logical address of the active source.
     @GuardedBy("mLock")
     protected final ActiveSource mActiveSource = new ActiveSource();
@@ -222,6 +256,13 @@
     @HdmiControlManager.VolumeControl
     private int mHdmiCecVolumeControl;
 
+    // Caches the volume behaviors of all audio output devices in AVC_AUDIO_OUTPUT_DEVICES.
+    @GuardedBy("mLock")
+    private Map<AudioDeviceAttributes, Integer> mAudioDeviceVolumeBehaviors = new HashMap<>();
+
+    // Maximum volume of AudioManager.STREAM_MUSIC. Set upon gaining access to system services.
+    private int mStreamMusicMaxVolume;
+
     // Make sure HdmiCecConfig is instantiated and the XMLs are read.
     private HdmiCecConfig mHdmiCecConfig;
 
@@ -411,6 +452,12 @@
     private PowerManagerInternalWrapper mPowerManagerInternal;
 
     @Nullable
+    private AudioManager mAudioManager;
+
+    @Nullable
+    private AudioDeviceVolumeManagerWrapperInterface mAudioDeviceVolumeManager;
+
+    @Nullable
     private Looper mIoLooper;
 
     @Nullable
@@ -439,11 +486,21 @@
 
     private final SelectRequestBuffer mSelectRequestBuffer = new SelectRequestBuffer();
 
-    @VisibleForTesting HdmiControlService(Context context, List<Integer> deviceTypes) {
+    /**
+     * Constructor for testing.
+     *
+     * It's critical to use a fake AudioDeviceVolumeManager because a normally instantiated
+     * AudioDeviceVolumeManager can access the "real" AudioService on the DUT.
+     *
+     * @see FakeAudioDeviceVolumeManagerWrapper
+     */
+    @VisibleForTesting HdmiControlService(Context context, List<Integer> deviceTypes,
+            AudioDeviceVolumeManagerWrapperInterface audioDeviceVolumeManager) {
         super(context);
         mLocalDevices = deviceTypes;
         mSettingsObserver = new SettingsObserver(mHandler);
         mHdmiCecConfig = new HdmiCecConfig(context);
+        mAudioDeviceVolumeManager = audioDeviceVolumeManager;
     }
 
     public HdmiControlService(Context context) {
@@ -744,6 +801,14 @@
                     Context.TV_INPUT_SERVICE);
             mPowerManager = new PowerManagerWrapper(getContext());
             mPowerManagerInternal = new PowerManagerInternalWrapper();
+            mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
+            mStreamMusicMaxVolume = getAudioManager().getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+            if (mAudioDeviceVolumeManager == null) {
+                mAudioDeviceVolumeManager =
+                        new AudioDeviceVolumeManagerWrapper(getContext());
+            }
+            getAudioDeviceVolumeManager().addOnDeviceVolumeBehaviorChangedListener(
+                    mServiceThreadExecutor, this::onDeviceVolumeBehaviorChanged);
         } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
             runOnServiceThread(this::bootCompleted);
         }
@@ -2419,6 +2484,7 @@
             pw.println("mPowerStatus: " + mPowerStatusController.getPowerStatus());
             pw.println("mIsCecAvailable: " + mIsCecAvailable);
             pw.println("mCecVersion: " + mCecVersion);
+            pw.println("mIsAbsoluteVolumeControlEnabled: " + isAbsoluteVolumeControlEnabled());
 
             // System settings
             pw.println("System_settings:");
@@ -2578,6 +2644,7 @@
             @HdmiControlManager.VolumeControl int hdmiCecVolumeControl) {
         mHdmiCecVolumeControl = hdmiCecVolumeControl;
         announceHdmiCecVolumeControlFeatureChange(hdmiCecVolumeControl);
+        runOnServiceThread(this::checkAndUpdateAbsoluteVolumeControlState);
     }
 
     // Get the source address to send out commands to devices connected to the current device
@@ -3086,15 +3153,17 @@
     private void announceHdmiCecVolumeControlFeatureChange(
             @HdmiControlManager.VolumeControl int hdmiCecVolumeControl) {
         assertRunOnServiceThread();
-        mHdmiCecVolumeControlFeatureListenerRecords.broadcast(listener -> {
-            try {
-                listener.onHdmiCecVolumeControlFeature(hdmiCecVolumeControl);
-            } catch (RemoteException e) {
-                Slog.e(TAG,
-                        "Failed to report HdmiControlVolumeControlStatusChange: "
-                                + hdmiCecVolumeControl);
-            }
-        });
+        synchronized (mLock) {
+            mHdmiCecVolumeControlFeatureListenerRecords.broadcast(listener -> {
+                try {
+                    listener.onHdmiCecVolumeControlFeature(hdmiCecVolumeControl);
+                } catch (RemoteException e) {
+                    Slog.e(TAG,
+                            "Failed to report HdmiControlVolumeControlStatusChange: "
+                                    + hdmiCecVolumeControl);
+                }
+            });
+        }
     }
 
     public HdmiCecLocalDeviceTv tv() {
@@ -3131,8 +3200,20 @@
                 HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
     }
 
+    /**
+     * Returns null before the boot phase {@link SystemService#PHASE_SYSTEM_SERVICES_READY}.
+     */
+    @Nullable
     AudioManager getAudioManager() {
-        return (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
+        return mAudioManager;
+    }
+
+    /**
+     * Returns null before the boot phase {@link SystemService#PHASE_SYSTEM_SERVICES_READY}.
+     */
+    @Nullable
+    private AudioDeviceVolumeManagerWrapperInterface getAudioDeviceVolumeManager() {
+        return mAudioDeviceVolumeManager;
     }
 
     boolean isControlEnabled() {
@@ -3486,6 +3567,7 @@
         synchronized (mLock) {
             mSystemAudioActivated = on;
         }
+        runOnServiceThread(this::checkAndUpdateAbsoluteVolumeControlState);
     }
 
     @ServiceThreadOnly
@@ -3599,6 +3681,8 @@
             device.addActiveSourceHistoryItem(new ActiveSource(logicalAddress, physicalAddress),
                     deviceIsActiveSource, caller);
         }
+
+        runOnServiceThread(this::checkAndUpdateAbsoluteVolumeControlState);
     }
 
     // This method should only be called when the device can be the active source
@@ -3791,4 +3875,332 @@
             Slog.e(TAG, "Failed to report setting change", e);
         }
     }
+
+    /**
+     * Listener for changes to the volume behavior of an audio output device. Caches the
+     * volume behavior of devices used for Absolute Volume Control.
+     */
+    @VisibleForTesting
+    @ServiceThreadOnly
+    void onDeviceVolumeBehaviorChanged(AudioDeviceAttributes device, int volumeBehavior) {
+        assertRunOnServiceThread();
+        if (AVC_AUDIO_OUTPUT_DEVICES.contains(device)) {
+            synchronized (mLock) {
+                mAudioDeviceVolumeBehaviors.put(device, volumeBehavior);
+            }
+            checkAndUpdateAbsoluteVolumeControlState();
+        }
+    }
+
+    /**
+     * Wrapper for {@link AudioManager#getDeviceVolumeBehavior} that takes advantage of cached
+     * results for the volume behaviors of HDMI audio devices.
+     */
+    @AudioManager.DeviceVolumeBehavior
+    private int getDeviceVolumeBehavior(AudioDeviceAttributes device) {
+        if (AVC_AUDIO_OUTPUT_DEVICES.contains(device)) {
+            synchronized (mLock) {
+                if (mAudioDeviceVolumeBehaviors.containsKey(device)) {
+                    return mAudioDeviceVolumeBehaviors.get(device);
+                }
+            }
+        }
+        return getAudioManager().getDeviceVolumeBehavior(device);
+    }
+
+    /**
+     * Returns whether Absolute Volume Control is enabled or not. This is determined by the
+     * volume behavior of the relevant HDMI audio output device(s) for this device's type.
+     */
+    public boolean isAbsoluteVolumeControlEnabled() {
+        if (!isTvDevice() && !isPlaybackDevice()) {
+            return false;
+        }
+        AudioDeviceAttributes avcAudioOutputDevice = getAvcAudioOutputDevice();
+        if (avcAudioOutputDevice == null) {
+            return false;
+        }
+        return getDeviceVolumeBehavior(avcAudioOutputDevice)
+                    == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE;
+    }
+
+    private AudioDeviceAttributes getAvcAudioOutputDevice() {
+        if (isTvDevice()) {
+            return tv().getSystemAudioOutputDevice();
+        } else if (isPlaybackDevice()) {
+            return AUDIO_OUTPUT_DEVICE_HDMI;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Checks the conditions for Absolute Volume Control (AVC), and enables or disables the feature
+     * if necessary. AVC is enabled precisely when a specific audio output device
+     * (HDMI for playback devices, and HDMI_ARC or HDMI_EARC for TVs) is using absolute volume
+     * behavior.
+     *
+     * AVC must be enabled on a Playback device or TV precisely when it is playing
+     * audio on an external device (the System Audio device) that supports the feature.
+     * This reduces to these conditions:
+     *
+     * 1. If the System Audio Device is an Audio System: System Audio Mode is active
+     * 2. Our HDMI audio output device is using full volume behavior
+     * 3. CEC volume is enabled
+     * 4. The System Audio device supports AVC (i.e. it supports <Set Audio Volume Level>)
+     *
+     * If not all of these conditions are met, this method disables AVC if necessary.
+     *
+     * If all of these conditions are met, this method starts an action to query the System Audio
+     * device's audio status, which enables AVC upon obtaining the audio status.
+     */
+    @ServiceThreadOnly
+    void checkAndUpdateAbsoluteVolumeControlState() {
+        assertRunOnServiceThread();
+
+        // Can't enable or disable AVC before we have access to system services
+        if (getAudioManager() == null) {
+            return;
+        }
+
+        HdmiCecLocalDevice localCecDevice;
+        if (isTvDevice() && tv() != null) {
+            localCecDevice = tv();
+            // Condition 1: TVs need System Audio Mode to be active
+            // (Doesn't apply to Playback Devices, where if SAM isn't active, we assume the
+            // TV is the System Audio Device instead.)
+            if (!isSystemAudioActivated()) {
+                disableAbsoluteVolumeControl();
+                return;
+            }
+        } else if (isPlaybackDevice() && playback() != null) {
+            localCecDevice = playback();
+        } else {
+            // Either this device type doesn't support AVC, or it hasn't fully initialized yet
+            return;
+        }
+
+        HdmiDeviceInfo systemAudioDeviceInfo = getHdmiCecNetwork().getSafeCecDeviceInfo(
+                localCecDevice.findAudioReceiverAddress());
+        @AudioManager.DeviceVolumeBehavior int currentVolumeBehavior =
+                        getDeviceVolumeBehavior(getAvcAudioOutputDevice());
+
+        // Condition 2: Already using full or absolute volume behavior
+        boolean alreadyUsingFullOrAbsoluteVolume =
+                currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL
+                        || currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE;
+        // Condition 3: CEC volume is enabled
+        boolean cecVolumeEnabled =
+                getHdmiCecVolumeControl() == HdmiControlManager.VOLUME_CONTROL_ENABLED;
+
+        if (!cecVolumeEnabled || !alreadyUsingFullOrAbsoluteVolume) {
+            disableAbsoluteVolumeControl();
+            return;
+        }
+
+        // Check for safety: if the System Audio device is a candidate for AVC, we should already
+        // have received messages from it to trigger the other conditions.
+        if (systemAudioDeviceInfo == null) {
+            disableAbsoluteVolumeControl();
+            return;
+        }
+        // Condition 4: The System Audio device supports AVC (i.e. <Set Audio Volume Level>).
+        switch (systemAudioDeviceInfo.getDeviceFeatures().getSetAudioVolumeLevelSupport()) {
+            case DeviceFeatures.FEATURE_SUPPORTED:
+                if (!isAbsoluteVolumeControlEnabled()) {
+                    // Start an action that will call {@link #enableAbsoluteVolumeControl}
+                    // once the System Audio device sends <Report Audio Status>
+                    localCecDevice.addAvcAudioStatusAction(
+                            systemAudioDeviceInfo.getLogicalAddress());
+                }
+                return;
+            case DeviceFeatures.FEATURE_NOT_SUPPORTED:
+                disableAbsoluteVolumeControl();
+                return;
+            case DeviceFeatures.FEATURE_SUPPORT_UNKNOWN:
+                disableAbsoluteVolumeControl();
+                localCecDevice.queryAvcSupport(systemAudioDeviceInfo.getLogicalAddress());
+                return;
+            default:
+                return;
+        }
+    }
+
+    private void disableAbsoluteVolumeControl() {
+        if (isPlaybackDevice()) {
+            playback().removeAvcAudioStatusAction();
+        } else if (isTvDevice()) {
+            tv().removeAvcAudioStatusAction();
+        }
+        AudioDeviceAttributes device = getAvcAudioOutputDevice();
+        if (getDeviceVolumeBehavior(device) == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE) {
+            getAudioManager().setDeviceVolumeBehavior(device,
+                    AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        }
+    }
+
+    /**
+     * Enables Absolute Volume Control. Should only be called when all the conditions for
+     * AVC are met (see {@link #checkAndUpdateAbsoluteVolumeControlState}).
+     * @param audioStatus The initial audio status to set the audio output device to
+     */
+    void enableAbsoluteVolumeControl(AudioStatus audioStatus) {
+        HdmiCecLocalDevice localDevice = isPlaybackDevice() ? playback() : tv();
+        HdmiDeviceInfo systemAudioDevice = getHdmiCecNetwork().getDeviceInfo(
+                localDevice.findAudioReceiverAddress());
+        VolumeInfo volumeInfo = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
+                .setMuted(audioStatus.getMute())
+                .setVolumeIndex(audioStatus.getVolume())
+                .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
+                .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
+                .build();
+        mAbsoluteVolumeChangedListener = new AbsoluteVolumeChangedListener(
+                localDevice, systemAudioDevice);
+
+        // AudioService sets the volume of the stream and device based on the input VolumeInfo
+        // when enabling absolute volume behavior, but not the mute state
+        notifyAvcMuteChange(audioStatus.getMute());
+        getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeBehavior(
+                getAvcAudioOutputDevice(), volumeInfo, mServiceThreadExecutor,
+                mAbsoluteVolumeChangedListener, true);
+    }
+
+    private AbsoluteVolumeChangedListener mAbsoluteVolumeChangedListener;
+
+    @VisibleForTesting
+    AbsoluteVolumeChangedListener getAbsoluteVolumeChangedListener() {
+        return mAbsoluteVolumeChangedListener;
+    }
+
+    /**
+     * Listeners for changes reported by AudioService to the state of an audio output device using
+     * absolute volume behavior.
+     */
+    @VisibleForTesting
+    class AbsoluteVolumeChangedListener implements
+            AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener {
+        private HdmiCecLocalDevice mLocalDevice;
+        private HdmiDeviceInfo mSystemAudioDevice;
+
+        private AbsoluteVolumeChangedListener(HdmiCecLocalDevice localDevice,
+                HdmiDeviceInfo systemAudioDevice) {
+            mLocalDevice = localDevice;
+            mSystemAudioDevice = systemAudioDevice;
+        }
+
+        /**
+         * Called when AudioService sets the volume level of an absolute volume audio output device
+         * to a numeric value.
+         */
+        @Override
+        public void onAudioDeviceVolumeChanged(
+                @NonNull AudioDeviceAttributes audioDevice,
+                @NonNull VolumeInfo volumeInfo) {
+            int localDeviceAddress;
+            synchronized (mLocalDevice.mLock) {
+                localDeviceAddress = mLocalDevice.getDeviceInfo().getLogicalAddress();
+            }
+            sendCecCommand(SetAudioVolumeLevelMessage.build(
+                            localDeviceAddress,
+                            mSystemAudioDevice.getLogicalAddress(),
+                            volumeInfo.getVolumeIndex()),
+                    // If sending the message fails, ask the System Audio device for its
+                    // audio status so that we can update AudioService
+                    (int errorCode) -> {
+                        if (errorCode == SendMessageResult.SUCCESS) {
+                            // Update the volume tracked in our AbsoluteVolumeAudioStatusAction
+                            // so it correctly processes incoming <Report Audio Status> messages
+                            HdmiCecLocalDevice avcDevice = isTvDevice() ? tv() : playback();
+                            avcDevice.updateAvcVolume(volumeInfo.getVolumeIndex());
+                        } else {
+                            sendCecCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(
+                                    localDeviceAddress,
+                                    mSystemAudioDevice.getLogicalAddress()
+                            ));
+                        }
+                    });
+        }
+
+        /**
+         * Called when AudioService adjusts the volume or mute state of an absolute volume
+         * audio output device
+         */
+        @Override
+        public void onAudioDeviceVolumeAdjusted(
+                @NonNull AudioDeviceAttributes audioDevice,
+                @NonNull VolumeInfo volumeInfo,
+                @AudioManager.VolumeAdjustment int direction,
+                @AudioDeviceVolumeManager.VolumeAdjustmentMode int mode
+        ) {
+            int keyCode;
+            switch (direction) {
+                case AudioManager.ADJUST_RAISE:
+                    keyCode = KeyEvent.KEYCODE_VOLUME_UP;
+                    break;
+                case AudioManager.ADJUST_LOWER:
+                    keyCode = KeyEvent.KEYCODE_VOLUME_DOWN;
+                    break;
+                case AudioManager.ADJUST_TOGGLE_MUTE:
+                case AudioManager.ADJUST_MUTE:
+                case AudioManager.ADJUST_UNMUTE:
+                    // Many CEC devices only support toggle mute. Therefore, we send the
+                    // same keycode for all three mute options.
+                    keyCode = KeyEvent.KEYCODE_VOLUME_MUTE;
+                    break;
+                default:
+                    return;
+            }
+            switch (mode) {
+                case AudioDeviceVolumeManager.ADJUST_MODE_NORMAL:
+                    mLocalDevice.sendVolumeKeyEvent(keyCode, true);
+                    mLocalDevice.sendVolumeKeyEvent(keyCode, false);
+                    break;
+                case AudioDeviceVolumeManager.ADJUST_MODE_START:
+                    mLocalDevice.sendVolumeKeyEvent(keyCode, true);
+                    break;
+                case AudioDeviceVolumeManager.ADJUST_MODE_END:
+                    mLocalDevice.sendVolumeKeyEvent(keyCode, false);
+                    break;
+                default:
+                    return;
+            }
+        }
+    }
+
+    /**
+     * Notifies AudioService of a change in the volume of the System Audio device. Has no effect if
+     * AVC is disabled, or the audio output device for AVC is not playing for STREAM_MUSIC
+     */
+    void notifyAvcVolumeChange(int volume) {
+        if (!isAbsoluteVolumeControlEnabled()) return;
+        List<AudioDeviceAttributes> streamMusicDevices =
+                getAudioManager().getDevicesForAttributes(STREAM_MUSIC_ATTRIBUTES);
+        if (streamMusicDevices.contains(getAvcAudioOutputDevice())) {
+            setStreamMusicVolume(volume, AudioManager.FLAG_ABSOLUTE_VOLUME);
+        }
+    }
+
+    /**
+     * Notifies AudioService of a change in the mute status of the System Audio device. Has no
+     * effect if AVC is disabled, or the audio output device for AVC is not playing for STREAM_MUSIC
+     */
+    void notifyAvcMuteChange(boolean mute) {
+        if (!isAbsoluteVolumeControlEnabled()) return;
+        List<AudioDeviceAttributes> streamMusicDevices =
+                getAudioManager().getDevicesForAttributes(STREAM_MUSIC_ATTRIBUTES);
+        if (streamMusicDevices.contains(getAvcAudioOutputDevice())) {
+            int direction = mute ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE;
+            getAudioManager().adjustStreamVolume(AudioManager.STREAM_MUSIC, direction,
+                    AudioManager.FLAG_ABSOLUTE_VOLUME);
+        }
+    }
+
+    /**
+     * Sets the volume index of {@link AudioManager#STREAM_MUSIC}. Rescales the input volume index
+     * from HDMI-CEC volume range to STREAM_MUSIC's.
+     */
+    void setStreamMusicVolume(int volume, int flags) {
+        getAudioManager().setStreamVolume(AudioManager.STREAM_MUSIC,
+                volume * mStreamMusicMaxVolume / AudioStatus.MAX_VOLUME, flags);
+    }
 }
diff --git a/services/core/java/com/android/server/hdmi/SendKeyAction.java b/services/core/java/com/android/server/hdmi/SendKeyAction.java
index adcef66..7daeaf1 100644
--- a/services/core/java/com/android/server/hdmi/SendKeyAction.java
+++ b/services/core/java/com/android/server/hdmi/SendKeyAction.java
@@ -172,8 +172,19 @@
     }
 
     private void sendKeyUp() {
-        sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(getSourceAddress(),
-                mTargetAddress));
+        // When using Absolute Volume Control, query audio status after a volume key is released.
+        // This allows us to notify AudioService of the resulting volume or mute status changes.
+        if (HdmiCecKeycode.isVolumeKeycode(mLastKeycode)
+                && localDevice().getService().isAbsoluteVolumeControlEnabled()) {
+            sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(getSourceAddress(),
+                    mTargetAddress),
+                    __ -> sendCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(
+                            getSourceAddress(),
+                            localDevice().findAudioReceiverAddress())));
+        } else {
+            sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(getSourceAddress(),
+                    mTargetAddress));
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java b/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java
index 96fd003..eb3b33d 100644
--- a/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java
+++ b/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java
@@ -122,4 +122,11 @@
             return true;
         }
     }
+
+    /**
+     * Returns the logical address of this action's target device.
+     */
+    public int getTargetAddress() {
+        return mTargetAddress;
+    }
 }
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioAction.java b/services/core/java/com/android/server/hdmi/SystemAudioAction.java
index 978c25d..e7a3db7 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioAction.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioAction.java
@@ -153,7 +153,7 @@
                 boolean receivedStatus = HdmiUtils.parseCommandParamSystemAudioStatus(cmd);
                 if (receivedStatus == mTargetAudioStatus) {
                     setSystemAudioMode(receivedStatus);
-                    startAudioStatusAction();
+                    finish();
                     return true;
                 } else {
                     HdmiLogger.debug("Unexpected system audio mode request:" + receivedStatus);
@@ -168,11 +168,6 @@
         }
     }
 
-    protected void startAudioStatusAction() {
-        addAndStartAction(new SystemAudioStatusAction(tv(), mAvrLogicalAddress, mCallbacks));
-        finish();
-    }
-
     protected void removeSystemAudioActionInProgress() {
         removeActionExcept(SystemAudioActionFromTv.class, this);
         removeActionExcept(SystemAudioActionFromAvr.class, this);
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java b/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java
index 6ddff91..99148c4 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java
@@ -16,8 +16,8 @@
 
 package com.android.server.hdmi;
 
-import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.IHdmiControlCallback;
 
 /**
@@ -65,7 +65,7 @@
 
         if (mTargetAudioStatus) {
             setSystemAudioMode(true);
-            startAudioStatusAction();
+            finish();
         } else {
             setSystemAudioMode(false);
             finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java b/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java
deleted file mode 100644
index b4af540..0000000
--- a/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2014 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.server.hdmi;
-
-import android.hardware.hdmi.HdmiControlManager;
-import android.hardware.hdmi.IHdmiControlCallback;
-import android.hardware.tv.cec.V1_0.SendMessageResult;
-
-import com.android.server.hdmi.HdmiControlService.SendMessageCallback;
-
-import java.util.List;
-
-/**
- * Action to update audio status (volume or mute) of audio amplifier
- */
-final class SystemAudioStatusAction extends HdmiCecFeatureAction {
-    private static final String TAG = "SystemAudioStatusAction";
-
-    // State that waits for <ReportAudioStatus>.
-    private static final int STATE_WAIT_FOR_REPORT_AUDIO_STATUS = 1;
-
-    private final int mAvrAddress;
-
-    SystemAudioStatusAction(
-            HdmiCecLocalDevice source, int avrAddress, List<IHdmiControlCallback> callbacks) {
-        super(source, callbacks);
-        mAvrAddress = avrAddress;
-    }
-
-    SystemAudioStatusAction(HdmiCecLocalDevice source, int avrAddress,
-            IHdmiControlCallback callback) {
-        super(source, callback);
-        mAvrAddress = avrAddress;
-    }
-
-    @Override
-    boolean start() {
-        mState = STATE_WAIT_FOR_REPORT_AUDIO_STATUS;
-        addTimer(mState, HdmiConfig.TIMEOUT_MS);
-        sendGiveAudioStatus();
-        return true;
-    }
-
-    private void sendGiveAudioStatus() {
-        sendCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(getSourceAddress(), mAvrAddress),
-                new SendMessageCallback() {
-            @Override
-            public void onSendCompleted(int error) {
-                if (error != SendMessageResult.SUCCESS) {
-                    handleSendGiveAudioStatusFailure();
-                }
-            }
-        });
-    }
-
-    private void handleSendGiveAudioStatusFailure() {
-
-        // Still return SUCCESS to callback.
-        finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
-    }
-
-    @Override
-    boolean processCommand(HdmiCecMessage cmd) {
-        if (mState != STATE_WAIT_FOR_REPORT_AUDIO_STATUS || mAvrAddress != cmd.getSource()) {
-            return false;
-        }
-
-        switch (cmd.getOpcode()) {
-            case Constants.MESSAGE_REPORT_AUDIO_STATUS:
-                handleReportAudioStatus(cmd);
-                return true;
-        }
-
-        return false;
-    }
-
-    private void handleReportAudioStatus(HdmiCecMessage cmd) {
-        byte[] params = cmd.getParams();
-        boolean mute = HdmiUtils.isAudioStatusMute(cmd);
-        int volume = HdmiUtils.getAudioStatusVolume(cmd);
-        tv().setAudioStatus(mute, volume);
-
-        if (!(tv().isSystemAudioActivated() ^ mute)) {
-            // Toggle AVR's mute status to match with the system audio status.
-            sendUserControlPressedAndReleased(mAvrAddress, HdmiCecKeycode.CEC_KEYCODE_MUTE);
-        }
-        finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
-    }
-
-    @Override
-    void handleTimerEvent(int state) {
-        if (mState != state) {
-            return;
-        }
-
-        handleSendGiveAudioStatusFailure();
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
index 18f2642..112db76 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
@@ -61,7 +61,8 @@
     public void setUp() throws Exception {
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList()) {
+        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
                 return new AudioManager() {
@@ -93,6 +94,7 @@
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mPhysicalAddress = 0x2000;
@@ -153,7 +155,6 @@
                 mHdmiControlService);
         audioDevice.init();
         mLocalDevices.add(audioDevice);
-        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
index e4c5ad67..e4eecc6 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
@@ -68,7 +68,8 @@
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
         HdmiControlService hdmiControlService =
-                new HdmiControlService(mContextSpy, Collections.emptyList()) {
+                new HdmiControlService(mContextSpy, Collections.emptyList(),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isPowerStandby() {
                         return false;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
index d73cdb5..5b11466 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
@@ -68,7 +68,8 @@
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
         HdmiControlService hdmiControlService =
-                new HdmiControlService(mContextSpy, Collections.emptyList()) {
+                new HdmiControlService(mContextSpy, Collections.emptyList(),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     AudioManager getAudioManager() {
                         return mAudioManager;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java
new file mode 100644
index 0000000..e06877f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java
@@ -0,0 +1,570 @@
+/*
+ * 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.server.hdmi;
+
+import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM;
+
+import static com.android.server.hdmi.HdmiCecKeycode.CEC_KEYCODE_VOLUME_UP;
+import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_BOOT_UP;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import static org.mockito.AdditionalMatchers.not;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.hardware.hdmi.DeviceFeatures;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.hardware.hdmi.HdmiPortInfo;
+import android.hardware.tv.cec.V1_0.SendMessageResult;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceVolumeManager;
+import android.media.AudioManager;
+import android.media.VolumeInfo;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.server.SystemService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Tests that Absolute Volume Control (AVC) is enabled and disabled correctly, and that
+ * the device responds correctly to incoming <Report Audio Status> messages and API calls
+ * from AudioService when AVC is active.
+ *
+ * This is an abstract base class. Concrete subclasses specify the type of the local device, and the
+ * type of the System Audio device. This allows each test to be run for multiple setups.
+ *
+ * We test the following pairs of (local device, System Audio device):
+ * (Playback, TV): {@link PlaybackDeviceToTvAvcTest}
+ * (Playback, Audio System): {@link PlaybackDeviceToAudioSystemAvcTest}
+ * (TV, Audio System): {@link TvToAudioSystemAvcTest}
+ */
+public abstract class BaseAbsoluteVolumeControlTest {
+    private HdmiControlService mHdmiControlService;
+    private HdmiCecController mHdmiCecController;
+    private HdmiCecLocalDevice mHdmiCecLocalDevice;
+    private FakeHdmiCecConfig mHdmiCecConfig;
+    private FakePowerManagerWrapper mPowerManager;
+    private Looper mLooper;
+    private Context mContextSpy;
+    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
+
+    @Mock protected AudioManager mAudioManager;
+    protected FakeAudioDeviceVolumeManagerWrapper mAudioDeviceVolumeManager;
+
+    protected TestLooper mTestLooper = new TestLooper();
+    protected FakeNativeWrapper mNativeWrapper;
+
+    // Audio Status given by the System Audio device in its initial <Report Audio Status> that
+    // triggers AVC being enabled
+    private static final AudioStatus INITIAL_SYSTEM_AUDIO_DEVICE_STATUS =
+            new AudioStatus(50, false);
+
+    // VolumeInfo passed to AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior to enable AVC
+    private static final VolumeInfo ENABLE_AVC_VOLUME_INFO =
+            new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
+                    .setMuted(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute())
+                    .setVolumeIndex(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume())
+                    .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
+                    .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
+                    .build();
+
+    protected abstract HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService);
+
+    protected abstract int getPhysicalAddress();
+    protected abstract int getDeviceType();
+    protected abstract AudioDeviceAttributes getAudioOutputDevice();
+
+    protected abstract int getSystemAudioDeviceLogicalAddress();
+    protected abstract int getSystemAudioDeviceType();
+
+    @Before
+    public void setUp() throws RemoteException {
+        MockitoAnnotations.initMocks(this);
+
+        mContextSpy = spy(new ContextWrapper(
+                InstrumentationRegistry.getInstrumentation().getTargetContext()));
+
+        mAudioDeviceVolumeManager = spy(new FakeAudioDeviceVolumeManagerWrapper());
+
+        mHdmiControlService =
+                new HdmiControlService(InstrumentationRegistry.getTargetContext(),
+                        Collections.singletonList(getDeviceType()),
+                        mAudioDeviceVolumeManager) {
+                    @Override
+                    AudioManager getAudioManager() {
+                        return mAudioManager;
+                    }
+
+                    @Override
+                    protected void writeStringSystemProperty(String key, String value) {
+                        // do nothing
+                    }
+                };
+
+        mLooper = mTestLooper.getLooper();
+        mHdmiControlService.setIoLooper(mLooper);
+
+        mHdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
+        mHdmiCecConfig.setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
+                HdmiControlManager.HDMI_CEC_VERSION_2_0);
+        mHdmiCecConfig.setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE,
+                HdmiControlManager.VOLUME_CONTROL_DISABLED);
+        mHdmiControlService.setHdmiCecConfig(mHdmiCecConfig);
+
+        mNativeWrapper = new FakeNativeWrapper();
+        mNativeWrapper.setPhysicalAddress(getPhysicalAddress());
+        mNativeWrapper.setPollAddressResponse(Constants.ADDR_TV, SendMessageResult.SUCCESS);
+
+        mHdmiCecController = HdmiCecController.createWithNativeWrapper(
+                mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
+        mHdmiControlService.setCecController(mHdmiCecController);
+        mHdmiControlService.setHdmiMhlController(
+                HdmiMhlControllerStub.create(mHdmiControlService));
+        mHdmiControlService.initService();
+        mPowerManager = new FakePowerManagerWrapper(mContextSpy);
+        mHdmiControlService.setPowerManager(mPowerManager);
+
+        mHdmiCecLocalDevice = createLocalDevice(mHdmiControlService);
+        mHdmiCecLocalDevice.init();
+        mLocalDevices.add(mHdmiCecLocalDevice);
+
+        HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1];
+        hdmiPortInfos[0] =
+                new HdmiPortInfo(1, HdmiPortInfo.PORT_OUTPUT, 0x0000, true, false, false);
+        mNativeWrapper.setPortInfo(hdmiPortInfos);
+        mNativeWrapper.setPortConnectionStatus(1, true);
+
+        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_BOOT_UP);
+        mTestLooper.dispatchAll();
+
+        // Simulate AudioManager's behavior and response when setDeviceVolumeBehavior is called
+        doAnswer(invocation -> {
+            setDeviceVolumeBehavior(invocation.getArgument(0), invocation.getArgument(1));
+            return null;
+        }).when(mAudioManager).setDeviceVolumeBehavior(any(), anyInt());
+
+        // Set starting volume behavior
+        doReturn(AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE)
+                .when(mAudioManager).getDeviceVolumeBehavior(eq(getAudioOutputDevice()));
+
+        // Audio service always plays STREAM_MUSIC on the device we need
+        doReturn(Collections.singletonList(getAudioOutputDevice())).when(mAudioManager)
+                .getDevicesForAttributes(HdmiControlService.STREAM_MUSIC_ATTRIBUTES);
+
+        // Max volume of STREAM_MUSIC
+        doReturn(25).when(mAudioManager).getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+
+        // Receive messages from devices to make sure they're registered in HdmiCecNetwork
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
+                Constants.ADDR_TV, getLogicalAddress()));
+        if (getSystemAudioDeviceType() == DEVICE_AUDIO_SYSTEM) {
+            mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
+                    Constants.ADDR_AUDIO_SYSTEM, getLogicalAddress()));
+        }
+
+        mHdmiControlService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
+        mHdmiControlService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+        mTestLooper.dispatchAll();
+    }
+
+    protected int getLogicalAddress() {
+        synchronized (mHdmiCecLocalDevice.mLock) {
+            return mHdmiCecLocalDevice.getDeviceInfo().getLogicalAddress();
+        }
+    }
+
+    /**
+     * Simulates the volume behavior of {@code device} being set to {@code behavior}.
+     */
+    protected void setDeviceVolumeBehavior(AudioDeviceAttributes device,
+            @AudioManager.DeviceVolumeBehavior int behavior) {
+        doReturn(behavior).when(mAudioManager).getDeviceVolumeBehavior(eq(device));
+        mHdmiControlService.onDeviceVolumeBehaviorChanged(device, behavior);
+        mTestLooper.dispatchAll();
+    }
+
+    /**
+     * Changes the setting for CEC volume.
+     */
+    protected void setCecVolumeControlSetting(@HdmiControlManager.VolumeControl int setting) {
+        mHdmiControlService.getHdmiCecConfig().setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE, setting);
+        mTestLooper.dispatchAll();
+    }
+
+    /**
+     * Has the device receive a <Report Features> message from the System Audio device specifying
+     * whether <Set Audio Volume Level> is supported or not.
+     */
+    protected void receiveSetAudioVolumeLevelSupport(
+            @DeviceFeatures.FeatureSupportStatus int featureSupportStatus) {
+        // <Report Features> can't specify an unknown feature support status
+        if (featureSupportStatus != DeviceFeatures.FEATURE_SUPPORT_UNKNOWN) {
+            mNativeWrapper.onCecMessage(ReportFeaturesMessage.build(
+                    getSystemAudioDeviceLogicalAddress(), HdmiControlManager.HDMI_CEC_VERSION_2_0,
+                    Arrays.asList(getSystemAudioDeviceType()), Constants.RC_PROFILE_SOURCE,
+                    Collections.emptyList(),
+                    DeviceFeatures.NO_FEATURES_SUPPORTED.toBuilder()
+                            .setSetAudioVolumeLevelSupport(featureSupportStatus)
+                            .build()));
+            mTestLooper.dispatchAll();
+        }
+    }
+
+    /**
+     * Enables System Audio mode if the System Audio device is an Audio System.
+     */
+    protected void enableSystemAudioModeIfNeeded() {
+        if (getSystemAudioDeviceType() == DEVICE_AUDIO_SYSTEM) {
+            receiveSetSystemAudioMode(true);
+        }
+    }
+
+    /**
+     * Sets System Audio Mode by having the device receive <Set System Audio Mode>
+     * from the Audio System.
+     */
+    protected void receiveSetSystemAudioMode(boolean status) {
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildSetSystemAudioMode(
+                Constants.ADDR_AUDIO_SYSTEM, Constants.ADDR_BROADCAST, status));
+        mTestLooper.dispatchAll();
+    }
+
+    /**
+     * Has the device receive a <Report Audio Status> reporting the status in
+     * {@link #INITIAL_SYSTEM_AUDIO_DEVICE_STATUS}
+     */
+    protected void receiveInitialReportAudioStatus() {
+        receiveReportAudioStatus(
+                INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume(),
+                INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute());
+    }
+
+    /**
+     * Has the device receive a <Report Audio Status> message from the System Audio Device.
+     */
+    protected void receiveReportAudioStatus(int volume, boolean mute) {
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportAudioStatus(
+                getSystemAudioDeviceLogicalAddress(),
+                getLogicalAddress(),
+                volume,
+                mute));
+        mTestLooper.dispatchAll();
+    }
+
+    /**
+     * Triggers all the conditions required to enable Absolute Volume Control.
+     */
+    protected void enableAbsoluteVolumeControl() {
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
+        enableSystemAudioModeIfNeeded();
+        receiveInitialReportAudioStatus();
+
+        verifyAbsoluteVolumeEnabled();
+    }
+
+    /**
+     * Verifies that AVC was enabled - that is the audio output device's volume behavior was last
+     * set to absolute volume behavior.
+     */
+    protected void verifyAbsoluteVolumeEnabled() {
+        InOrder inOrder = inOrder(mAudioManager, mAudioDeviceVolumeManager);
+        inOrder.verify(mAudioDeviceVolumeManager, atLeastOnce()).setDeviceAbsoluteVolumeBehavior(
+                eq(getAudioOutputDevice()), any(), any(), any(), anyBoolean());
+        inOrder.verify(mAudioManager, never()).setDeviceVolumeBehavior(
+                eq(getAudioOutputDevice()), not(eq(AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE)));
+    }
+
+    /**
+     * Verifies that AVC was disabled - that is, the audio output device's volume behavior was
+     * last set to something other than absolute volume behavior.
+     */
+    protected void verifyAbsoluteVolumeDisabled() {
+        InOrder inOrder = inOrder(mAudioManager, mAudioDeviceVolumeManager);
+        inOrder.verify(mAudioManager, atLeastOnce()).setDeviceVolumeBehavior(
+                eq(getAudioOutputDevice()), not(eq(AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE)));
+        inOrder.verify(mAudioDeviceVolumeManager, never()).setDeviceAbsoluteVolumeBehavior(
+                eq(getAudioOutputDevice()), any(), any(), any(), anyBoolean());
+    }
+
+    protected void verifyGiveAudioStatusNeverSent() {
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(
+                HdmiCecMessageBuilder.buildGiveAudioStatus(
+                        getLogicalAddress(), getSystemAudioDeviceLogicalAddress()));
+    }
+
+    protected void verifyGiveAudioStatusSent() {
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildGiveAudioStatus(
+                        getLogicalAddress(), getSystemAudioDeviceLogicalAddress()));
+    }
+
+    @Test
+    public void allConditionsExceptSavlSupportMet_sendsSetAudioVolumeLevelAndGiveFeatures() {
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                SetAudioVolumeLevelMessage.build(
+                        getLogicalAddress(), getSystemAudioDeviceLogicalAddress(),
+                        Constants.AUDIO_VOLUME_STATUS_UNKNOWN));
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildGiveFeatures(
+                        getLogicalAddress(), getSystemAudioDeviceLogicalAddress()));
+    }
+
+    @Test
+    public void allConditionsMet_savlSupportLast_reportFeatures_giveAudioStatusSent() {
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+        verifyGiveAudioStatusNeverSent();
+
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
+        verifyGiveAudioStatusSent();
+    }
+
+    @Test
+    public void allConditionsMet_savlSupportLast_noFeatureAbort_giveAudioStatusSent() {
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+        verifyGiveAudioStatusNeverSent();
+
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        verifyGiveAudioStatusSent();
+    }
+
+    @Test
+    public void allConditionsMet_cecVolumeEnabledLast_giveAudioStatusSent() {
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        enableSystemAudioModeIfNeeded();
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
+        verifyGiveAudioStatusNeverSent();
+
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        verifyGiveAudioStatusSent();
+    }
+
+    @Test
+    public void allConditionsMet_fullVolumeBehaviorLast_giveAudioStatusSent() {
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
+        verifyGiveAudioStatusNeverSent();
+
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        verifyGiveAudioStatusSent();
+    }
+
+    @Test
+    public void allConditionsMet_systemAudioModeEnabledLast_giveAudioStatusSent() {
+        // Only run when the System Audio device is an Audio System.
+        assume().that(getSystemAudioDeviceType()).isEqualTo(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
+        verifyGiveAudioStatusNeverSent();
+
+        receiveSetSystemAudioMode(true);
+        verifyGiveAudioStatusSent();
+    }
+
+    @Test
+    public void giveAudioStatusSent_systemAudioDeviceSendsReportAudioStatus_avcEnabled() {
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+
+        // Verify that AVC was never enabled
+        verify(mAudioDeviceVolumeManager, never()).setDeviceAbsoluteVolumeBehavior(
+                eq(getAudioOutputDevice()), any(), any(), any(), anyBoolean());
+        receiveInitialReportAudioStatus();
+
+        verifyAbsoluteVolumeEnabled();
+    }
+
+    @Test
+    public void avcEnabled_cecVolumeDisabled_absoluteVolumeDisabled() {
+        enableAbsoluteVolumeControl();
+
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_DISABLED);
+        verifyAbsoluteVolumeDisabled();
+    }
+
+    @Test
+    public void avcEnabled_setAudioVolumeLevelNotSupported_absoluteVolumeDisabled() {
+        enableAbsoluteVolumeControl();
+
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED);
+        verifyAbsoluteVolumeDisabled();
+    }
+
+    @Test
+    public void avcEnabled_setAudioVolumeLevelFeatureAborted_absoluteVolumeDisabled() {
+        enableAbsoluteVolumeControl();
+
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildFeatureAbortCommand(
+                getSystemAudioDeviceLogicalAddress(), getLogicalAddress(),
+                Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL, Constants.ABORT_UNRECOGNIZED_OPCODE));
+        mTestLooper.dispatchAll();
+        verifyAbsoluteVolumeDisabled();
+    }
+
+    @Test
+    public void avcEnabled_systemAudioModeDisabled_absoluteVolumeDisabled() {
+        // Only run when the System Audio device is an Audio System.
+        assume().that(getSystemAudioDeviceType()).isEqualTo(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+
+        enableAbsoluteVolumeControl();
+
+        receiveSetSystemAudioMode(false);
+        verifyAbsoluteVolumeDisabled();
+    }
+
+    @Test
+    public void avcEnabled_receiveReportAudioStatus_notifiesVolumeOrMuteChanges() {
+        // Initial <Report Audio Status> has volume=50 and mute=false
+        enableAbsoluteVolumeControl();
+
+        // New volume and mute status: sets both
+        receiveReportAudioStatus(20, true);
+        verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(5),
+                anyInt());
+        verify(mAudioManager).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_MUTE), anyInt());
+        clearInvocations(mAudioManager);
+
+        // New volume only: sets volume only
+        receiveReportAudioStatus(32, true);
+        verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
+                anyInt());
+        verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_MUTE), anyInt());
+        clearInvocations(mAudioManager);
+
+        // New mute status only: sets mute only
+        receiveReportAudioStatus(32, false);
+        verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
+                anyInt());
+        verify(mAudioManager).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_UNMUTE), anyInt());
+        clearInvocations(mAudioManager);
+
+        // Repeat of earlier message: sets neither volume nor mute
+        receiveReportAudioStatus(32, false);
+        verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
+                anyInt());
+        verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_UNMUTE), anyInt());
+        clearInvocations(mAudioManager);
+
+        // If AudioService causes us to send <Set Audio Volume Level>, the System Audio device's
+        // volume changes. Afterward, a duplicate of an earlier <Report Audio Status> should
+        // still cause us to call setStreamVolume()
+        mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeChanged(
+                getAudioOutputDevice(),
+                new VolumeInfo.Builder(ENABLE_AVC_VOLUME_INFO)
+                        .setVolumeIndex(20)
+                        .build()
+        );
+        mTestLooper.dispatchAll();
+        receiveReportAudioStatus(32, false);
+        verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
+                anyInt());
+        verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_UNMUTE), anyInt());
+    }
+
+    @Test
+    public void avcEnabled_audioDeviceVolumeAdjusted_sendsUserControlPressedAndGiveAudioStatus() {
+        enableAbsoluteVolumeControl();
+        mNativeWrapper.clearResultMessages();
+
+        mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeAdjusted(
+                getAudioOutputDevice(),
+                ENABLE_AVC_VOLUME_INFO,
+                AudioManager.ADJUST_RAISE,
+                AudioDeviceVolumeManager.ADJUST_MODE_NORMAL
+        );
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildUserControlPressed(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress(), CEC_KEYCODE_VOLUME_UP));
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildUserControlReleased(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress()));
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildGiveAudioStatus(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress()));
+    }
+
+    @Test
+    public void avcEnabled_audioDeviceVolumeChanged_sendsSetAudioVolumeLevel() {
+        enableAbsoluteVolumeControl();
+        mNativeWrapper.clearResultMessages();
+
+        mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeChanged(
+                getAudioOutputDevice(),
+                new VolumeInfo.Builder(ENABLE_AVC_VOLUME_INFO)
+                        .setVolumeIndex(20)
+                        .build()
+        );
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                SetAudioVolumeLevelMessage.build(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress(), 20));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java
index 5cec8ad..28ba4bb 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java
@@ -56,7 +56,7 @@
         mDeviceInfoForTests = HdmiDeviceInfo.hardwarePort(1001, 1234);
         HdmiControlService hdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList()) {
+                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
 
                     @Override
                     void sendCecCommand(
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
index 52a0b6c..545f318 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
 import static com.android.server.hdmi.Constants.ADDR_TV;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
@@ -78,7 +79,8 @@
 
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList()) {
+        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
                 return new AudioManager() {
@@ -110,6 +112,7 @@
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mPhysicalAddress = 0x2000;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
index 35432ed..d7fef90 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
@@ -20,6 +20,7 @@
 import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_STANDBY;
 import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_3;
@@ -100,7 +101,7 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList()) {
+                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -136,6 +137,7 @@
 
         mLocalDevices.add(mHdmiCecLocalDevicePlayback);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x0000);
         mPowerManager = new FakePowerManagerWrapper(context);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
index e77cd91..72d36b0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
@@ -20,6 +20,7 @@
 import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_STANDBY;
 import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2;
 import static com.android.server.hdmi.Constants.ADDR_TV;
@@ -109,7 +110,7 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList()) {
+                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -145,6 +146,7 @@
                                  true, false, false);
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioDeviceVolumeManagerWrapper.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioDeviceVolumeManagerWrapper.java
new file mode 100644
index 0000000..d33ef9b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioDeviceVolumeManagerWrapper.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2022 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.server.hdmi;
+
+import static android.media.AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener;
+import static android.media.AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceVolumeManager;
+import android.media.AudioManager;
+import android.media.VolumeInfo;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * Wrapper for {@link AudioDeviceVolumeManager} that stubs its methods. Useful for testing.
+ */
+public class FakeAudioDeviceVolumeManagerWrapper implements
+        AudioDeviceVolumeManagerWrapperInterface {
+
+    private final Set<OnDeviceVolumeBehaviorChangedListener> mVolumeBehaviorListeners;
+
+    public FakeAudioDeviceVolumeManagerWrapper() {
+        mVolumeBehaviorListeners = new HashSet<>();
+    }
+
+    @Override
+    public void addOnDeviceVolumeBehaviorChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnDeviceVolumeBehaviorChangedListener listener)
+            throws SecurityException {
+        mVolumeBehaviorListeners.add(listener);
+    }
+
+    @Override
+    public void removeOnDeviceVolumeBehaviorChangedListener(
+            @NonNull OnDeviceVolumeBehaviorChangedListener listener) {
+        mVolumeBehaviorListeners.remove(listener);
+    }
+
+    @Override
+    public void setDeviceAbsoluteVolumeBehavior(
+            @NonNull AudioDeviceAttributes device,
+            @NonNull VolumeInfo volume,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnAudioDeviceVolumeChangedListener vclistener,
+            boolean handlesVolumeAdjustment) {
+        // Notify all volume behavior listeners that the device adopted absolute volume behavior
+        for (OnDeviceVolumeBehaviorChangedListener listener : mVolumeBehaviorListeners) {
+            listener.onDeviceVolumeBehaviorChanged(device,
+                    AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
index 30bcc7e..9f744f9 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
@@ -89,7 +89,8 @@
         mContextSpy = spy(new ContextWrapper(
                 InstrumentationRegistry.getInstrumentation().getTargetContext()));
 
-        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList()));
+        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
         doReturn(mHdmiCecAtomWriterSpy).when(mHdmiControlServiceSpy).getAtomWriter();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
index 2dcc449..0cba106 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
@@ -101,7 +101,8 @@
         mMyLooper = mTestLooper.getLooper();
 
         mHdmiControlServiceSpy = spy(new HdmiControlService(
-                InstrumentationRegistry.getTargetContext(), Collections.emptyList()));
+                InstrumentationRegistry.getTargetContext(), Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()));
         doReturn(mMyLooper).when(mHdmiControlServiceSpy).getIoLooper();
         doReturn(mMyLooper).when(mHdmiControlServiceSpy).getServiceLooper();
         doAnswer(__ -> mCecVersion).when(mHdmiControlServiceSpy).getCecVersion();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index 70bc460..91d265c 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -88,7 +88,7 @@
 
         mHdmiControlService =
             new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                    Collections.emptyList()) {
+                    Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                 @Override
                 AudioManager getAudioManager() {
                     return new AudioManager() {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index 86130da..484b5a8 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ABORT_UNRECOGNIZED_OPCODE;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
@@ -92,7 +93,7 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList()) {
+                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     void wakeUp() {
                         mWokenUp = true;
@@ -151,6 +152,7 @@
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mNativeWrapper.setPortConnectionStatus(1, true);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.setPowerManagerInternal(mPowerManagerInternal);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
index fb8baa3..48e70fe 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
@@ -17,6 +17,7 @@
 
 import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_TV;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
 import static com.android.server.hdmi.Constants.ADDR_TV;
@@ -137,7 +138,8 @@
         Context context = InstrumentationRegistry.getTargetContext();
 
         mHdmiControlService =
-                new HdmiControlService(context, Collections.emptyList()) {
+                new HdmiControlService(context, Collections.emptyList(),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return isControlEnabled;
@@ -190,6 +192,7 @@
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mNativeWrapper.setPortConnectionStatus(1, true);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x2000);
         mTestLooper.dispatchAll();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index df4aa5d..f27b8c2 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ABORT_UNRECOGNIZED_OPCODE;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
@@ -29,8 +30,10 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.hardware.hdmi.HdmiControlManager;
@@ -125,7 +128,7 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList()) {
+                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     void wakeUp() {
                         mWokenUp = true;
@@ -179,6 +182,7 @@
                 new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true);
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
@@ -768,7 +772,7 @@
         // When the device reports its physical address, the listener eventually is invoked.
         HdmiCecMessage reportPhysicalAddress =
                 HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
-                ADDR_PLAYBACK_2, 0x1000, HdmiDeviceInfo.DEVICE_PLAYBACK);
+                        ADDR_PLAYBACK_2, 0x1000, HdmiDeviceInfo.DEVICE_PLAYBACK);
         mNativeWrapper.onCecMessage(reportPhysicalAddress);
         mTestLooper.dispatchAll();
 
@@ -777,6 +781,54 @@
         assertThat(mDeviceEventListeners.size()).isEqualTo(1);
         assertThat(mDeviceEventListeners.get(0).getStatus())
                 .isEqualTo(HdmiControlManager.DEVICE_EVENT_ADD_DEVICE);
+    }
 
+    @Test
+    public void receiveSetAudioVolumeLevel_samNotActivated_noFeatureAbort_volumeChanges() {
+        when(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)).thenReturn(25);
+
+        // Max volume of STREAM_MUSIC is retrieved on boot
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
+        mTestLooper.dispatchAll();
+
+        mNativeWrapper.onCecMessage(SetAudioVolumeLevelMessage.build(
+                ADDR_PLAYBACK_1,
+                ADDR_TV,
+                20));
+        mTestLooper.dispatchAll();
+
+        // <Feature Abort>[Not in correct mode] not sent
+        HdmiCecMessage featureAbortMessage = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+                ADDR_TV,
+                ADDR_PLAYBACK_1,
+                Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
+                Constants.ABORT_NOT_IN_CORRECT_MODE);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(featureAbortMessage);
+
+        // <Set Audio Volume Level> uses volume range [0, 100]; STREAM_MUSIC uses range [0, 25]
+        verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(5), anyInt());
+    }
+
+    @Test
+    public void receiveSetAudioVolumeLevel_samActivated_respondsFeatureAbort_noVolumeChange() {
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildSetSystemAudioMode(
+                ADDR_AUDIO_SYSTEM, ADDR_TV, true));
+        mTestLooper.dispatchAll();
+
+        mNativeWrapper.onCecMessage(SetAudioVolumeLevelMessage.build(
+                ADDR_PLAYBACK_1, ADDR_TV, 50));
+        mTestLooper.dispatchAll();
+
+        // <Feature Abort>[Not in correct mode] sent
+        HdmiCecMessage featureAbortMessage = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+                ADDR_TV,
+                ADDR_PLAYBACK_1,
+                Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
+                Constants.ABORT_NOT_IN_CORRECT_MODE);
+        assertThat(mNativeWrapper.getResultMessages()).contains(featureAbortMessage);
+
+        // AudioManager not notified of volume change
+        verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(),
+                anyInt());
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
index 50c9f70..a446e10 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
@@ -51,7 +51,8 @@
     @Before
     public void setUp() throws Exception {
         HdmiControlService mHdmiControlService = new HdmiControlService(
-                InstrumentationRegistry.getTargetContext(), Collections.emptyList());
+                InstrumentationRegistry.getTargetContext(), Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper());
 
         mHdmiControlService.setIoLooper(mTestLooper.getLooper());
     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
index 03532ae..b8a1ba3 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
@@ -67,7 +67,8 @@
     @Before
     public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getTargetContext();
-        mHdmiControlService = new HdmiControlService(mContext, Collections.emptyList()) {
+        mHdmiControlService = new HdmiControlService(mContext, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             void invokeDeviceEventListeners(HdmiDeviceInfo device, int status) {
                 mDeviceEventListenerStatuses.add(status);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
index 7a68285..b94deed 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -65,7 +66,8 @@
         Context contextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
         Looper myLooper = mTestLooper.getLooper();
 
-        mHdmiControlService = new HdmiControlService(contextSpy, Collections.emptyList()) {
+        mHdmiControlService = new HdmiControlService(contextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             boolean isControlEnabled() {
                 return true;
@@ -105,6 +107,7 @@
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mNativeWrapper.setPortConnectionStatus(1, true);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(contextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.getHdmiCecNetwork().initPortInfo();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 3987c32..6266571 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -91,7 +91,8 @@
 
         HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
 
-        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList()));
+        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
index 561e6a5..46a4e86 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_TV;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 import static com.android.server.hdmi.OneTouchPlayAction.STATE_WAITING_FOR_REPORT_POWER_STATUS;
@@ -87,7 +88,8 @@
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
         mHdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList()) {
+        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
                 return new AudioManager() {
@@ -120,6 +122,7 @@
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mPhysicalAddress = 0x2000;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvcTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvcTest.java
new file mode 100644
index 0000000..6418602
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvcTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 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.server.hdmi;
+
+import android.hardware.hdmi.DeviceFeatures;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.media.AudioDeviceAttributes;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import com.google.android.collect.Lists;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+/**
+ * Tests for Absolute Volume Control where the local device is a Playback device and the
+ * System Audio device is an Audio System.
+ */
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class PlaybackDeviceToAudioSystemAvcTest extends BaseAbsoluteVolumeControlTest {
+
+    @Override
+    protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) {
+        return new HdmiCecLocalDevicePlayback(hdmiControlService);
+    }
+
+    @Override
+    protected int getPhysicalAddress() {
+        return 0x1100;
+    }
+
+    @Override
+    protected int getDeviceType() {
+        return HdmiDeviceInfo.DEVICE_PLAYBACK;
+    }
+
+    @Override
+    protected AudioDeviceAttributes getAudioOutputDevice() {
+        return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceLogicalAddress() {
+        return Constants.ADDR_AUDIO_SYSTEM;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceType() {
+        return HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM;
+    }
+
+    /**
+     * AVC is disabled if the Audio System disables System Audio mode, and the TV has unknown
+     * support for <Set Audio Volume Level>. It is enabled once the TV confirms support for
+     * <Set Audio Volume Level> and sends <Report Audio Status>.
+     */
+    @Test
+    public void switchToTv_absoluteVolumeControlDisabledUntilAllConditionsMet() {
+        enableAbsoluteVolumeControl();
+
+        // Audio System disables System Audio Mode. AVC should be disabled.
+        receiveSetSystemAudioMode(false);
+        verifyAbsoluteVolumeDisabled();
+
+        // TV reports support for <Set Audio Volume Level>
+        mNativeWrapper.onCecMessage(ReportFeaturesMessage.build(
+                Constants.ADDR_TV, HdmiControlManager.HDMI_CEC_VERSION_2_0,
+                Arrays.asList(HdmiDeviceInfo.DEVICE_TV), Constants.RC_PROFILE_TV,
+                Lists.newArrayList(Constants.RC_PROFILE_TV_NONE),
+                DeviceFeatures.NO_FEATURES_SUPPORTED.toBuilder()
+                        .setSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED)
+                        .build()));
+        mTestLooper.dispatchAll();
+
+        // TV reports its initial audio status
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportAudioStatus(
+                Constants.ADDR_TV,
+                getLogicalAddress(),
+                30,
+                false));
+        mTestLooper.dispatchAll();
+
+        verifyAbsoluteVolumeEnabled();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvcTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvcTest.java
new file mode 100644
index 0000000..504c3bc
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvcTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2022 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.server.hdmi;
+
+import static org.mockito.Mockito.clearInvocations;
+
+import android.hardware.hdmi.DeviceFeatures;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.media.AudioDeviceAttributes;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Tests for Absolute Volume Control where the local device is a Playback device and the
+ * System Audio device is a TV.
+ */
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class PlaybackDeviceToTvAvcTest extends BaseAbsoluteVolumeControlTest {
+
+    @Override
+    protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) {
+        return new HdmiCecLocalDevicePlayback(hdmiControlService);
+    }
+
+    @Override
+    protected int getPhysicalAddress() {
+        return 0x1100;
+    }
+
+    @Override
+    protected int getDeviceType() {
+        return HdmiDeviceInfo.DEVICE_PLAYBACK;
+    }
+
+    @Override
+    protected AudioDeviceAttributes getAudioOutputDevice() {
+        return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceLogicalAddress() {
+        return Constants.ADDR_TV;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceType() {
+        return HdmiDeviceInfo.DEVICE_TV;
+    }
+
+    /**
+     * AVC is disabled when an Audio System with unknown support for <Set Audio Volume Level>
+     * becomes the System Audio device. It is enabled once the Audio System reports that it
+     * supports <Set Audio Volume Level> and sends <Report Audio Status>.
+     */
+    @Test
+    public void switchToAudioSystem_absoluteVolumeControlDisabledUntilAllConditionsMet() {
+        enableAbsoluteVolumeControl();
+
+        // Audio System enables System Audio Mode. AVC should be disabled.
+        receiveSetSystemAudioMode(true);
+        verifyAbsoluteVolumeDisabled();
+
+        clearInvocations(mAudioManager, mAudioDeviceVolumeManager);
+
+        // Audio System reports support for <Set Audio Volume Level>
+        mNativeWrapper.onCecMessage(ReportFeaturesMessage.build(
+                Constants.ADDR_AUDIO_SYSTEM, HdmiControlManager.HDMI_CEC_VERSION_2_0,
+                Arrays.asList(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM), Constants.RC_PROFILE_SOURCE,
+                Collections.emptyList(),
+                DeviceFeatures.NO_FEATURES_SUPPORTED.toBuilder()
+                        .setSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED)
+                        .build()));
+        mTestLooper.dispatchAll();
+
+        // Audio system reports its initial audio status
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportAudioStatus(
+                Constants.ADDR_AUDIO_SYSTEM,
+                getLogicalAddress(),
+                30,
+                false));
+        mTestLooper.dispatchAll();
+
+        verifyAbsoluteVolumeEnabled();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
index c878f99..e5058be 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2;
@@ -68,7 +69,8 @@
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
         mHdmiControlService = new HdmiControlService(mContextSpy,
-                Collections.singletonList(HdmiDeviceInfo.DEVICE_TV)) {
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
                 return new AudioManager() {
@@ -110,6 +112,7 @@
                 new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, false);
         mNativeWrapper.setPortInfo(hdmiPortInfo);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mPhysicalAddress = 0x0000;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
index 6184c21..f7983ca 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
@@ -96,8 +97,8 @@
         mMyLooper = mTestLooper.getLooper();
 
         mHdmiControlService =
-                new HdmiControlService(context,
-                        Collections.emptyList()) {
+                new HdmiControlService(context, Collections.emptyList(),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -125,6 +126,7 @@
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         mLocalDevices.add(mHdmiCecLocalDeviceTv);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
index 0587864..566a7e0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
@@ -149,7 +150,7 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList()) {
+                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -186,6 +187,7 @@
                         true, false, false);
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
index a34b55c..087e407 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
@@ -20,6 +20,7 @@
 import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORTED;
 import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORT_UNKNOWN;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -81,7 +82,8 @@
         mContextSpy = spy(new ContextWrapper(
                 InstrumentationRegistry.getInstrumentation().getTargetContext()));
 
-        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList()));
+        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
 
@@ -98,6 +100,7 @@
         mHdmiControlServiceSpy.setHdmiMhlController(
                 HdmiMhlControllerStub.create(mHdmiControlServiceSpy));
         mHdmiControlServiceSpy.initService();
+        mHdmiControlServiceSpy.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlServiceSpy.setPowerManager(mPowerManager);
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
index 9d14341..1644252 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
@@ -17,6 +17,7 @@
 package com.android.server.hdmi;
 
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 import static com.android.server.hdmi.SystemAudioAutoInitiationAction.RETRIES_ON_TIMEOUT;
@@ -69,7 +70,8 @@
 
         Looper myLooper = mTestLooper.getLooper();
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList()) {
+        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
                 return new AudioManager() {
@@ -108,6 +110,7 @@
                 new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true);
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
index 095c69c..c2f706a 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
@@ -69,7 +69,7 @@
         Context context = InstrumentationRegistry.getTargetContext();
 
         HdmiControlService hdmiControlService = new HdmiControlService(context,
-                Collections.emptyList()) {
+                Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     void sendCecCommand(
                             HdmiCecMessage command, @Nullable SendMessageCallback callback) {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvcTest.java b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvcTest.java
new file mode 100644
index 0000000..41c0e0d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvcTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 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.server.hdmi;
+
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.media.AudioDeviceAttributes;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Absolute Volume Control where the local device is a TV and the System Audio device
+ * is an Audio System. Assumes that the TV uses ARC (rather than eARC).
+ */
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class TvToAudioSystemAvcTest extends BaseAbsoluteVolumeControlTest {
+
+    @Override
+    protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) {
+        return new HdmiCecLocalDeviceTv(hdmiControlService);
+    }
+
+    @Override
+    protected int getPhysicalAddress() {
+        return 0x0000;
+    }
+
+    @Override
+    protected int getDeviceType() {
+        return HdmiDeviceInfo.DEVICE_TV;
+    }
+
+    @Override
+    protected AudioDeviceAttributes getAudioOutputDevice() {
+        return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI_ARC;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceLogicalAddress() {
+        return Constants.ADDR_AUDIO_SYSTEM;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceType() {
+        return HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM;
+    }
+}