diff options
45 files changed, 1886 insertions, 163 deletions
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 000000000000..d7563e085e61 --- /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 000000000000..438c1ea01e29 --- /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 000000000000..1a1d4c19358b --- /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 000000000000..a884ffb93a6d --- /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 751f2db99528..157057d0e4b7 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 @@ final class Constants { 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 @@ final class Constants { 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 26a161323e4f..fb2d2ee08cbd 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 @@ abstract class HdmiCecLocalDevice { 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 @@ abstract class HdmiCecLocalDevice { 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 @@ abstract class HdmiCecLocalDevice { 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 @@ abstract class HdmiCecLocalDevice { */ 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 90b4f76fec10..c0c02027a7a1 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 @@ abstract class HdmiCecLocalDeviceSource extends HdmiCecLocalDevice { 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 9212fb604721..1ea1457439ec 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 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { 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 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { 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 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { removeAction(OneTouchRecordAction.class); removeAction(TimerRecordingAction.class); removeAction(NewDeviceAction.class); + removeAction(AbsoluteVolumeAudioStatusAction.class); disableSystemAudioIfExist(); disableArcIfExist(); @@ -1318,7 +1339,6 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { removeAction(SystemAudioActionFromAvr.class); removeAction(SystemAudioActionFromTv.class); removeAction(SystemAudioAutoInitiationAction.class); - removeAction(SystemAudioStatusAction.class); removeAction(VolumeControlAction.class); if (!mService.isControlEnabled()) { @@ -1589,6 +1609,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { 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 290cae50f2cb..2b84225e7c55 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 @@ public class HdmiCecMessage { 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 8b6d16a171f5..caaf80073de1 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 @@ public class HdmiCecNetwork { // 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 @@ public class HdmiCecNetwork { 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 @@ public class HdmiCecNetwork { .build(); updateCecDevice(newDeviceInfo); + + mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState(); } @ServiceThreadOnly @@ -617,6 +620,8 @@ public class HdmiCecNetwork { ) .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 12380abd0d38..9824b4e6c43a 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.hdmi.Constants.OPTION_MHL_SERVICE_CONTROL; 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.Intent; 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.IHdmiSystemAudioModeChangeListener; 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.ArrayMap; 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.io.PrintWriter; 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 class HdmiControlService extends SystemService { 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 @@ public class HdmiControlService extends SystemService { } }; + Executor getServiceThreadExecutor() { + return mServiceThreadExecutor; + } + // Logical address of the active source. @GuardedBy("mLock") protected final ActiveSource mActiveSource = new ActiveSource(); @@ -222,6 +256,13 @@ public class HdmiControlService extends SystemService { @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 @@ public class HdmiControlService extends SystemService { private PowerManagerInternalWrapper mPowerManagerInternal; @Nullable + private AudioManager mAudioManager; + + @Nullable + private AudioDeviceVolumeManagerWrapperInterface mAudioDeviceVolumeManager; + + @Nullable private Looper mIoLooper; @Nullable @@ -439,11 +486,21 @@ public class HdmiControlService extends SystemService { 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 @@ public class HdmiControlService extends SystemService { 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 @@ public class HdmiControlService extends SystemService { 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 @@ public class HdmiControlService extends SystemService { @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 @@ public class HdmiControlService extends SystemService { 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 @@ public class HdmiControlService extends SystemService { 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 @@ public class HdmiControlService extends SystemService { synchronized (mLock) { mSystemAudioActivated = on; } + runOnServiceThread(this::checkAndUpdateAbsoluteVolumeControlState); } @ServiceThreadOnly @@ -3599,6 +3681,8 @@ public class HdmiControlService extends SystemService { 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 @@ public class HdmiControlService extends SystemService { 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 adcef667545f..7daeaf19c657 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 @@ final class SendKeyAction extends HdmiCecFeatureAction { } 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 96fd003c72b4..eb3b33d8ca22 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 @@ public class SetAudioVolumeLevelDiscoveryAction extends HdmiCecFeatureAction { 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 978c25d0a8c6..e7a3db75c9d0 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 @@ abstract class SystemAudioAction extends HdmiCecFeatureAction { 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 @@ abstract class SystemAudioAction extends HdmiCecFeatureAction { } } - 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 6ddff91a70f7..99148c4ea114 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 @@ final class SystemAudioActionFromAvr extends SystemAudioAction { 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 b4af540b96f5..000000000000 --- 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 18f264277b41..112db7612f29 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 class ActiveSourceActionTest { 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 @@ public class ActiveSourceActionTest { 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 @@ public class ActiveSourceActionTest { 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 e4c5ad6769d7..e4eecc6f6fa0 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 @@ public class ArcInitiationActionFromAvrTest { 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 d73cdb5f53b0..5b114661fae8 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 @@ public class ArcTerminationActionFromAvrTest { 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 000000000000..e06877f9144e --- /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 5cec8ad1e63d..28ba4bb503f9 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 @@ public class DetectTvSystemAudioModeSupportActionTest { 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 52a0b6cdc2be..545f3183ee26 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 @@ public class DevicePowerStatusActionTest { 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 @@ public class DevicePowerStatusActionTest { 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 35432edfcf16..d7fef90456ab 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_ON; 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 @@ public class DeviceSelectActionFromPlaybackTest { mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), - Collections.emptyList()) { + Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) { @Override boolean isControlEnabled() { return true; @@ -136,6 +137,7 @@ public class DeviceSelectActionFromPlaybackTest { 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 e77cd91b46d8..72d36b00d73d 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_ON; 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 @@ public class DeviceSelectActionFromTvTest { mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), - Collections.emptyList()) { + Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) { @Override boolean isControlEnabled() { return true; @@ -145,6 +146,7 @@ public class DeviceSelectActionFromTvTest { 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 000000000000..d33ef9bc8879 --- /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 30bcc7e8afa1..9f744f9373ed 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 @@ public class HdmiCecAtomLoggingTest { 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 2dcc449e36a5..0cba10669c85 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 @@ public class HdmiCecControllerTest { 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 70bc460411c8..91d265c81083 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 @@ public class HdmiCecLocalDeviceAudioSystemTest { 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 86130daf4aac..484b5a8dae1a 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 @@ public class HdmiCecLocalDevicePlaybackTest { mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), - Collections.emptyList()) { + Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) { @Override void wakeUp() { mWokenUp = true; @@ -151,6 +152,7 @@ public class HdmiCecLocalDevicePlaybackTest { 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 fb8baa30e6b4..48e70fe4a60f 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 @@ package com.android.server.hdmi; 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 @@ public class HdmiCecLocalDeviceTest { 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 @@ public class HdmiCecLocalDeviceTest { 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 df4aa5dac9df..f27b8c2f4b3a 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 com.google.common.truth.Truth.assertThat; 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 @@ public class HdmiCecLocalDeviceTvTest { mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), - Collections.emptyList()) { + Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) { @Override void wakeUp() { mWokenUp = true; @@ -179,6 +182,7 @@ public class HdmiCecLocalDeviceTvTest { 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 @@ public class HdmiCecLocalDeviceTvTest { // 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 @@ public class HdmiCecLocalDeviceTvTest { 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 50c9f70ccb03..a446e109c921 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 @@ public class HdmiCecMessageValidatorTest { @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 03532ae1cb1f..b8a1ba363373 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 @@ public class HdmiCecNetworkTest { @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 7a68285bc003..b94deeddb8af 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 @@ public class HdmiCecPowerStatusControllerTest { 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 @@ public class HdmiCecPowerStatusControllerTest { 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 3987c32277cd..6266571d33d4 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 @@ public class HdmiControlServiceTest { 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 561e6a5fec41..46a4e862c300 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 @@ public class OneTouchPlayActionTest { 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 @@ public class OneTouchPlayActionTest { 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 000000000000..64186028e6ed --- /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 000000000000..504c3bc2626a --- /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 c878f99f7912..e5058bea8d1b 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 @@ public class PowerStatusMonitorActionTest { 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 @@ public class PowerStatusMonitorActionTest { 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 6184c2116e1d..f7983ca21816 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 @@ public class RequestSadActionTest { 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 @@ public class RequestSadActionTest { 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 0587864eeb20..566a7e0fecce 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 @@ public class RoutingControlActionTest { mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), - Collections.emptyList()) { + Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) { @Override boolean isControlEnabled() { return true; @@ -186,6 +187,7 @@ public class RoutingControlActionTest { 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 a34b55c00308..087e407e314c 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_NOT_SUPPORTED; 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 @@ public class SetAudioVolumeLevelDiscoveryActionTest { 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 @@ public class SetAudioVolumeLevelDiscoveryActionTest { 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 9d143418fd4c..1644252e5739 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 @@ public class SystemAudioAutoInitiationActionTest { 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 @@ public class SystemAudioAutoInitiationActionTest { 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 095c69c776a2..c2f706ad1220 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 @@ public class SystemAudioInitiationActionFromAvrTest { 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 000000000000..41c0e0d29879 --- /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; + } +} |