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;
+ }
+}