From fd133451c234358bfa5fb8f230a05a626a014f98 Mon Sep 17 00:00:00 2001 From: Nathalie Le Clair Date: Thu, 4 Aug 2022 14:41:15 +0200 Subject: [PATCH 1/7] Create and remove HdmiEarcLocalDeviceTx Create when HdmiControlService is started, or eARC gets enabled. Remove when eARC gets disabled. Test: atest Bug: 240388108 Change-Id: I1c563de3022ab3b614b6b71fc36250daced9b06b --- .../server/hdmi/HdmiControlService.java | 165 ++++++++++++++++-- .../server/hdmi/HdmiEarcLocalDevice.java | 32 ++++ .../server/hdmi/HdmiEarcLocalDeviceTx.java | 32 ++++ .../server/hdmi/HdmiCecNetworkTest.java | 32 ++++ .../server/hdmi/HdmiControlServiceTest.java | 49 ++++++ 5 files changed, 300 insertions(+), 10 deletions(-) create mode 100644 services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java create mode 100644 services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 2f15e579b2149..820700b0af236 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -18,6 +18,7 @@ package com.android.server.hdmi; import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_ADD_DEVICE; import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE; +import static android.hardware.hdmi.HdmiControlManager.EARC_FEATURE_ENABLED; import static android.hardware.hdmi.HdmiControlManager.HDMI_CEC_CONTROL_ENABLED; import static android.hardware.hdmi.HdmiControlManager.SOUNDBAR_MODE_DISABLED; import static android.hardware.hdmi.HdmiControlManager.SOUNDBAR_MODE_ENABLED; @@ -184,13 +185,14 @@ public class HdmiControlService extends SystemService { static final String PERMISSION = "android.permission.HDMI_CEC"; - // The reason code to initiate initializeCec(). + // The reason code to initiate initializeCec() and initializeEarc(). static final int INITIATED_BY_ENABLE_CEC = 0; static final int INITIATED_BY_BOOT_UP = 1; static final int INITIATED_BY_SCREEN_ON = 2; static final int INITIATED_BY_WAKE_UP_MESSAGE = 3; static final int INITIATED_BY_HOTPLUG = 4; static final int INITIATED_BY_SOUNDBAR_MODE = 5; + static final int INITIATED_BY_ENABLE_EARC = 6; // The reason code representing the intent action that drives the standby // procedure. The procedure starts either by Intent.ACTION_SCREEN_OFF or @@ -384,6 +386,15 @@ public class HdmiControlService extends SystemService { @HdmiControlManager.HdmiCecControl private int mHdmiControlEnabled; + // Set to true while the eARC feature is supported by the hardware and the eARC HAL is present. + @GuardedBy("mLock") + @VisibleForTesting + protected boolean mEarcSupported; + + // Set to true while the eARC feature is enabled. + @GuardedBy("mLock") + private boolean mEarcEnabled; + // Set to true while the service is in normal mode. While set to false, no input change is // allowed. Used for situations where input change can confuse users such as channel auto-scan, // system upgrade, etc., a.k.a. "prohibit mode". @@ -417,6 +428,9 @@ public class HdmiControlService extends SystemService { private HdmiCecPowerStatusController mPowerStatusController; + @Nullable + private HdmiEarcLocalDevice mEarcLocalDevice; + @ServiceThreadOnly private String mMenuLanguage = localeToMenuLanguage(Locale.getDefault()); @@ -631,9 +645,13 @@ public class HdmiControlService extends SystemService { mPowerStatusController = new HdmiCecPowerStatusController(this); } mPowerStatusController.setPowerStatus(getInitialPowerStatus()); - mProhibitMode = false; + setProhibitMode(false); mHdmiControlEnabled = mHdmiCecConfig.getIntValue( HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED); + synchronized (mLock) { + mEarcEnabled = (mHdmiCecConfig.getIntValue( + HdmiControlManager.SETTING_NAME_EARC_ENABLED) == EARC_FEATURE_ENABLED); + } setHdmiCecVolumeControlEnabledInternal(getHdmiCecConfig().getIntValue( HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE)); mMhlInputChangeEnabled = readBooleanSetting(Global.MHL_INPUT_SWITCHING_ENABLED, true); @@ -655,11 +673,16 @@ public class HdmiControlService extends SystemService { Slog.i(TAG, "Device does not support MHL-control."); } mHdmiCecNetwork = new HdmiCecNetwork(this, mCecController, mMhlController); - if (mHdmiControlEnabled == HdmiControlManager.HDMI_CEC_CONTROL_ENABLED) { + if (isCecControlEnabled()) { initializeCec(INITIATED_BY_BOOT_UP); } else { mCecController.enableCec(false); } + synchronized (mLock) { + if (mEarcSupported && mEarcEnabled) { + initializeEarc(INITIATED_BY_BOOT_UP); + } + } mMhlDevices = Collections.emptyList(); mHdmiCecNetwork.initPortInfo(); @@ -743,8 +766,16 @@ public class HdmiControlService extends SystemService { mCecController.enableWakeupByOtp(tv().getAutoWakeup()); } } - }, - mServiceThreadExecutor); + }, mServiceThreadExecutor); + mHdmiCecConfig.registerChangeListener(HdmiControlManager.SETTING_NAME_EARC_ENABLED, + new HdmiCecConfig.SettingChangeListener() { + @Override + public void onChange(String setting) { + @HdmiControlManager.HdmiCecControl int enabled = mHdmiCecConfig.getIntValue( + HdmiControlManager.SETTING_NAME_EARC_ENABLED); + setEarcEnabled(enabled); + } + }, mServiceThreadExecutor); } /** Returns true if the device screen is off */ @@ -1083,7 +1114,8 @@ public class HdmiControlService extends SystemService { } @ServiceThreadOnly - private void initializeCecLocalDevices(final int initiatedBy) { + @VisibleForTesting + protected void initializeCecLocalDevices(final int initiatedBy) { assertRunOnServiceThread(); // A container for [Device type, Local device info]. ArrayList localDevices = new ArrayList<>(); @@ -2568,7 +2600,9 @@ public class HdmiControlService extends SystemService { if (!DumpUtils.checkDumpPermission(getContext(), TAG, writer)) return; final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); - pw.println("mProhibitMode: " + mProhibitMode); + synchronized (mLock) { + pw.println("mProhibitMode: " + mProhibitMode); + } pw.println("mPowerStatus: " + mPowerStatusController.getPowerStatus()); pw.println("mIsCecAvailable: " + mIsCecAvailable); pw.println("mCecVersion: " + mCecVersion); @@ -2577,9 +2611,9 @@ public class HdmiControlService extends SystemService { // System settings pw.println("System_settings:"); pw.increaseIndent(); - pw.println("mMhlInputChangeEnabled: " + mMhlInputChangeEnabled); + pw.println("mMhlInputChangeEnabled: " + isMhlInputChangeEnabled()); pw.println("mSystemAudioActivated: " + isSystemAudioActivated()); - pw.println("mHdmiCecVolumeControlEnabled: " + mHdmiCecVolumeControl); + pw.println("mHdmiCecVolumeControlEnabled: " + getHdmiCecVolumeControl()); pw.decreaseIndent(); // CEC settings @@ -2605,6 +2639,14 @@ public class HdmiControlService extends SystemService { pw.increaseIndent(); mMhlController.dump(pw); pw.decreaseIndent(); + pw.print("eARC local device: "); + pw.increaseIndent(); + if (mEarcLocalDevice == null) { + pw.println("None. eARC is either disabled or not available."); + } else { + mEarcLocalDevice.dump(pw); + } + pw.decreaseIndent(); mHdmiCecNetwork.dump(pw); if (mCecController != null) { pw.println("mCecController: "); @@ -3313,6 +3355,12 @@ public class HdmiControlService extends SystemService { } } + private boolean isEarcSupportedAndEnabled() { + synchronized (mLock) { + return mEarcSupported && mEarcEnabled; + } + } + @ServiceThreadOnly int getPowerStatus() { assertRunOnServiceThread(); @@ -3384,7 +3432,7 @@ public class HdmiControlService extends SystemService { mPowerStatusController.setPowerStatus(HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON, false); if (mCecController != null) { - if (mHdmiControlEnabled == HDMI_CEC_CONTROL_ENABLED) { + if (isCecControlEnabled()) { int startReason = -1; switch (wakeUpAction) { case WAKE_UP_SCREEN_ON: @@ -3406,6 +3454,22 @@ public class HdmiControlService extends SystemService { } else { Slog.i(TAG, "Device does not support HDMI-CEC."); } + if (isEarcSupportedAndEnabled()) { + int startReason = -1; + switch (wakeUpAction) { + case WAKE_UP_SCREEN_ON: + startReason = INITIATED_BY_SCREEN_ON; + break; + case WAKE_UP_BOOT_UP: + startReason = INITIATED_BY_BOOT_UP; + break; + default: + Slog.e(TAG, "wakeUpAction " + wakeUpAction + " not defined."); + return; + + } + initializeEarc(startReason); + } // TODO: Initialize MHL local devices. } @@ -4296,4 +4360,85 @@ public class HdmiControlService extends SystemService { getAudioManager().setStreamVolume(AudioManager.STREAM_MUSIC, volume * mStreamMusicMaxVolume / AudioStatus.MAX_VOLUME, flags); } + + private void initializeEarc(int initiatedBy) { + Slog.i(TAG, "eARC initialized, reason = " + initiatedBy); + initializeEarcLocalDevice(initiatedBy); + } + + @ServiceThreadOnly + @VisibleForTesting + protected void initializeEarcLocalDevice(final int initiatedBy) { + // TODO remove initiatedBy argument if it stays unused + assertRunOnServiceThread(); + if (isTvDevice() && mEarcLocalDevice == null) { + mEarcLocalDevice = new HdmiEarcLocalDeviceTx(); + } + // TODO create HdmiEarcLocalDeviceRx if we're an audio system device. + } + + @ServiceThreadOnly + @VisibleForTesting + protected void setEarcEnabled(@HdmiControlManager.EarcFeature int enabled) { + assertRunOnServiceThread(); + synchronized (mLock) { + mEarcEnabled = (enabled == EARC_FEATURE_ENABLED); + + if (!mEarcSupported) { + Slog.i(TAG, "Enabled/disabled eARC setting, but the hardware doesn´t support eARC." + + "This settings change doesn´t have an effect."); + return; + } + + if (mEarcEnabled) { + onEnableEarc(); + return; + } + } + runOnServiceThread(new Runnable() { + @Override + public void run() { + onDisableEarc(); + } + }); + } + + @VisibleForTesting + protected void setEarcSupported(boolean supported) { + synchronized (mLock) { + mEarcSupported = supported; + } + } + + @ServiceThreadOnly + private void onEnableEarc() { + initializeEarc(INITIATED_BY_ENABLE_EARC); + } + + @ServiceThreadOnly + private void onDisableEarc() { + clearEarcLocalDevice(); + } + + @ServiceThreadOnly + @VisibleForTesting + protected void clearEarcLocalDevice() { + assertRunOnServiceThread(); + mEarcLocalDevice = null; + } + + @ServiceThreadOnly + @VisibleForTesting + protected void addEarcLocalDevice(HdmiEarcLocalDevice localDevice) { + assertRunOnServiceThread(); + mEarcLocalDevice = localDevice; + } + + @ServiceThreadOnly + @VisibleForTesting + protected HdmiEarcLocalDevice getEarcLocalDevice() { + assertRunOnServiceThread(); + return mEarcLocalDevice; + } + } diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java new file mode 100644 index 0000000000000..1e9d338fc2d68 --- /dev/null +++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java @@ -0,0 +1,32 @@ +/* + * 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.util.IndentingPrintWriter; + +/** + * Class that models a local eARC device hosted in this system. + * The class contains methods that are common between eARC TX and eARC RX devices. + */ +public class HdmiEarcLocalDevice { + private static final String TAG = "HdmiEarcLocalDevice"; + + /** Dump internal status of HdmiEarcLocalDevice object */ + protected void dump(final IndentingPrintWriter pw) { + // Should be overridden in the more specific classes + } +} diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java new file mode 100644 index 0000000000000..c1c1e7794676d --- /dev/null +++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java @@ -0,0 +1,32 @@ +/* + * 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.util.IndentingPrintWriter; + +/** + * Represents a local eARC device of type TX residing in the Android system. + * Only TV panel devices can have a local eARC TX device. + */ +public class HdmiEarcLocalDeviceTx extends HdmiEarcLocalDevice { + private static final String TAG = "HdmiEarcLocalDeviceTx"; + + /** Dump internal status of HdmiEarcLocalDeviceTx object */ + protected void dump(final IndentingPrintWriter pw) { + pw.println("TX"); + } +} 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 b8a1ba3633735..a82a79f06f456 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java @@ -605,4 +605,36 @@ public class HdmiCecNetworkTest { assertThat(mHdmiCecNetwork.getSafeCecDevicesLocked()).hasSize(1); } + + @Test + public void disableCec_clearCecLocalDevices() { + mHdmiCecNetwork.clearLocalDevices(); + mHdmiCecNetwork.addLocalDevice(HdmiDeviceInfo.DEVICE_TV, + new HdmiCecLocalDeviceTv(mHdmiControlService)); + + assertThat(mHdmiCecNetwork.getLocalDeviceList()).hasSize(1); + assertThat(mHdmiCecNetwork.getLocalDeviceList().get(0)).isInstanceOf( + HdmiCecLocalDeviceTv.class); + mHdmiControlService.setCecEnabled(HdmiControlManager.HDMI_CEC_CONTROL_DISABLED); + mTestLooper.dispatchAll(); + + assertThat(mHdmiCecNetwork.getLocalDeviceList()).hasSize(0); + } + + @Test + public void disableEarc_doNotClearCecLocalDevices() { + mHdmiCecNetwork.clearLocalDevices(); + mHdmiCecNetwork.addLocalDevice(HdmiDeviceInfo.DEVICE_TV, + new HdmiCecLocalDeviceTv(mHdmiControlService)); + + assertThat(mHdmiCecNetwork.getLocalDeviceList()).hasSize(1); + assertThat(mHdmiCecNetwork.getLocalDeviceList().get(0)).isInstanceOf( + HdmiCecLocalDeviceTv.class); + mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_DISABLED); + mTestLooper.dispatchAll(); + + assertThat(mHdmiCecNetwork.getLocalDeviceList()).hasSize(1); + assertThat(mHdmiCecNetwork.getLocalDeviceList().get(0)).isInstanceOf( + HdmiCecLocalDeviceTv.class); + } } 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 49a0a9a52c47d..f07ee6c2be4e7 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java @@ -29,6 +29,7 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static junit.framework.TestCase.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; @@ -1082,6 +1083,54 @@ public class HdmiControlServiceTest { assertThat(mHdmiControlServiceSpy.audioSystem()).isNull(); } + @Test + public void disableEarc_clearEarcLocalDevice() { + mHdmiControlServiceSpy.setEarcSupported(true); + mHdmiControlServiceSpy.clearEarcLocalDevice(); + mHdmiControlServiceSpy.addEarcLocalDevice(new HdmiEarcLocalDeviceTx()); + assertThat(mHdmiControlServiceSpy.getEarcLocalDevice()).isNotNull(); + + mHdmiControlServiceSpy.setEarcEnabled(HdmiControlManager.EARC_FEATURE_DISABLED); + mTestLooper.dispatchAll(); + assertThat(mHdmiControlServiceSpy.getEarcLocalDevice()).isNull(); + } + + @Test + public void disableCec_doNotClearEarcLocalDevice() { + mHdmiControlServiceSpy.setEarcSupported(true); + mHdmiControlServiceSpy.clearEarcLocalDevice(); + mHdmiControlServiceSpy.addEarcLocalDevice(new HdmiEarcLocalDeviceTx()); + assertThat(mHdmiControlServiceSpy.getEarcLocalDevice()).isNotNull(); + + mHdmiControlServiceSpy.setCecEnabled(HdmiControlManager.HDMI_CEC_CONTROL_DISABLED); + mTestLooper.dispatchAll(); + assertThat(mHdmiControlServiceSpy.getEarcLocalDevice()).isNotNull(); + } + + @Test + public void enableCec_initializeCecLocalDevices() { + mHdmiControlServiceSpy.setEarcSupported(true); + Mockito.clearInvocations(mHdmiControlServiceSpy); + mHdmiControlServiceSpy.setCecEnabled(HdmiControlManager.HDMI_CEC_CONTROL_DISABLED); + mTestLooper.dispatchAll(); + mHdmiControlServiceSpy.setCecEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED); + mTestLooper.dispatchAll(); + verify(mHdmiControlServiceSpy, times(1)).initializeCecLocalDevices(anyInt()); + verify(mHdmiControlServiceSpy, times(0)).initializeEarcLocalDevice(anyInt()); + } + + @Test + public void enableEarc_initializeEarcLocalDevices() { + mHdmiControlServiceSpy.setEarcSupported(true); + Mockito.clearInvocations(mHdmiControlServiceSpy); + mHdmiControlServiceSpy.setEarcEnabled(HdmiControlManager.EARC_FEATURE_DISABLED); + mTestLooper.dispatchAll(); + mHdmiControlServiceSpy.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED); + mTestLooper.dispatchAll(); + verify(mHdmiControlServiceSpy, times(0)).initializeCecLocalDevices(anyInt()); + verify(mHdmiControlServiceSpy, times(1)).initializeEarcLocalDevice(anyInt()); + } + protected static class MockPlaybackDevice extends HdmiCecLocalDevicePlayback { private boolean mCanGoToStandby; From f4e0589b9709ad5c0502aab214ca0cb1ff30b28b Mon Sep 17 00:00:00 2001 From: Nathalie Le Clair Date: Thu, 27 Oct 2022 11:23:07 +0200 Subject: [PATCH 2/7] Add eARC info to HdmiPortInfo Add a stub for the earcSupported parameter to HdmiCecController. The HDMI HAL methods (such as getPortInfo, the method that reports this parameter) currently enter the framework through HdmiCecController, but may be moved out into a separate controller in the future. Test: make Bug: 240388203 Change-Id: I231cd4ed97490d6770c45b0fd5631274c421b672 --- core/api/system-current.txt | 2 + .../android/hardware/hdmi/HdmiPortInfo.java | 40 ++++++++++++++++--- .../hardware/hdmi/HdmiPortInfoTest.java | 28 +++++++++---- .../server/hdmi/HdmiCecController.java | 15 +++++-- .../android/server/hdmi/HdmiCecNetwork.java | 3 +- 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 8528c95101a75..dd8734668c9da 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -4520,12 +4520,14 @@ package android.hardware.hdmi { public final class HdmiPortInfo implements android.os.Parcelable { ctor public HdmiPortInfo(int, int, int, boolean, boolean, boolean); + ctor public HdmiPortInfo(int, int, int, boolean, boolean, boolean, boolean); method public int describeContents(); method public int getAddress(); method public int getId(); method public int getType(); method public boolean isArcSupported(); method public boolean isCecSupported(); + method public boolean isEarcSupported(); method public boolean isMhlSupported(); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator CREATOR; diff --git a/core/java/android/hardware/hdmi/HdmiPortInfo.java b/core/java/android/hardware/hdmi/HdmiPortInfo.java index 625a6a53d9ce4..5bb1f035f8487 100644 --- a/core/java/android/hardware/hdmi/HdmiPortInfo.java +++ b/core/java/android/hardware/hdmi/HdmiPortInfo.java @@ -23,7 +23,7 @@ import android.os.Parcelable; /** * A class to encapsulate HDMI port information. Contains the capability of the ports such as - * HDMI-CEC, MHL, ARC(Audio Return Channel), and physical address assigned to each port. + * HDMI-CEC, MHL, ARC(Audio Return Channel), eARC and physical address assigned to each port. * * @hide */ @@ -40,6 +40,7 @@ public final class HdmiPortInfo implements Parcelable { private final int mAddress; private final boolean mCecSupported; private final boolean mArcSupported; + private final boolean mEarcSupported; private final boolean mMhlSupported; /** @@ -53,11 +54,28 @@ public final class HdmiPortInfo implements Parcelable { * @param arc {@code true} if audio return channel is supported on the port */ public HdmiPortInfo(int id, int type, int address, boolean cec, boolean mhl, boolean arc) { + this(id, type, address, cec, mhl, arc, false); + } + + /** + * Constructor. + * + * @param id identifier assigned to each port. 1 for HDMI port 1 + * @param type HDMI port input/output type + * @param address physical address of the port + * @param cec {@code true} if HDMI-CEC is supported on the port + * @param mhl {@code true} if MHL is supported on the port + * @param arc {@code true} if audio return channel is supported on the port + * @param earc {@code true} if eARC is supported on the port + */ + public HdmiPortInfo(int id, int type, int address, + boolean cec, boolean mhl, boolean arc, boolean earc) { mId = id; mType = type; mAddress = address; mCecSupported = cec; mArcSupported = arc; + mEarcSupported = earc; mMhlSupported = mhl; } @@ -115,6 +133,15 @@ public final class HdmiPortInfo implements Parcelable { return mArcSupported; } + /** + * Returns {@code true} if the port supports eARC. + * + * @return {@code true} if the port supports eARC. + */ + public boolean isEarcSupported() { + return mEarcSupported; + } + /** * Describes the kinds of special objects contained in this Parcelable's * marshalled representation. @@ -138,7 +165,8 @@ public final class HdmiPortInfo implements Parcelable { boolean cec = (source.readInt() == 1); boolean arc = (source.readInt() == 1); boolean mhl = (source.readInt() == 1); - return new HdmiPortInfo(id, type, address, cec, mhl, arc); + boolean earc = (source.readInt() == 1); + return new HdmiPortInfo(id, type, address, cec, mhl, arc, earc); } @Override @@ -164,6 +192,7 @@ public final class HdmiPortInfo implements Parcelable { dest.writeInt(mCecSupported ? 1 : 0); dest.writeInt(mArcSupported ? 1 : 0); dest.writeInt(mMhlSupported ? 1 : 0); + dest.writeInt(mEarcSupported ? 1 : 0); } @NonNull @@ -175,7 +204,8 @@ public final class HdmiPortInfo implements Parcelable { s.append("address: ").append(String.format("0x%04x", mAddress)).append(", "); s.append("cec: ").append(mCecSupported).append(", "); s.append("arc: ").append(mArcSupported).append(", "); - s.append("mhl: ").append(mMhlSupported); + s.append("mhl: ").append(mMhlSupported).append(", "); + s.append("earc: ").append(mEarcSupported); return s.toString(); } @@ -187,12 +217,12 @@ public final class HdmiPortInfo implements Parcelable { final HdmiPortInfo other = (HdmiPortInfo) o; return mId == other.mId && mType == other.mType && mAddress == other.mAddress && mCecSupported == other.mCecSupported && mArcSupported == other.mArcSupported - && mMhlSupported == other.mMhlSupported; + && mMhlSupported == other.mMhlSupported && mEarcSupported == other.mEarcSupported; } @Override public int hashCode() { return java.util.Objects.hash( - mId, mType, mAddress, mCecSupported, mArcSupported, mMhlSupported); + mId, mType, mAddress, mCecSupported, mArcSupported, mMhlSupported, mEarcSupported); } } diff --git a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiPortInfoTest.java b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiPortInfoTest.java index d8dc1eabe0456..64df3487bf530 100755 --- a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiPortInfoTest.java +++ b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiPortInfoTest.java @@ -37,26 +37,38 @@ public class HdmiPortInfoTest { boolean isCec = true; boolean isMhl = false; boolean isArcSupported = false; + boolean isEarcSupported = false; new EqualsTester() .addEqualityGroup( - new HdmiPortInfo(portId, portType, address, isCec, isMhl, isArcSupported), - new HdmiPortInfo(portId, portType, address, isCec, isMhl, isArcSupported)) + new HdmiPortInfo(portId, portType, address, isCec, isMhl, isArcSupported, + isEarcSupported), + new HdmiPortInfo(portId, portType, address, isCec, isMhl, isArcSupported, + isEarcSupported)) .addEqualityGroup( new HdmiPortInfo( - portId + 1, portType, address, isCec, isMhl, isArcSupported)) + portId + 1, portType, address, isCec, isMhl, isArcSupported, + isEarcSupported)) .addEqualityGroup( new HdmiPortInfo( - portId, portType + 1, address, isCec, isMhl, isArcSupported)) + portId, portType + 1, address, isCec, isMhl, isArcSupported, + isEarcSupported)) .addEqualityGroup( new HdmiPortInfo( - portId, portType, address + 1, isCec, isMhl, isArcSupported)) + portId, portType, address + 1, isCec, isMhl, isArcSupported, + isEarcSupported)) .addEqualityGroup( - new HdmiPortInfo(portId, portType, address, !isCec, isMhl, isArcSupported)) + new HdmiPortInfo(portId, portType, address, !isCec, isMhl, isArcSupported, + isEarcSupported)) .addEqualityGroup( - new HdmiPortInfo(portId, portType, address, isCec, !isMhl, isArcSupported)) + new HdmiPortInfo(portId, portType, address, isCec, !isMhl, isArcSupported, + isEarcSupported)) .addEqualityGroup( - new HdmiPortInfo(portId, portType, address, isCec, isMhl, !isArcSupported)) + new HdmiPortInfo(portId, portType, address, isCec, isMhl, !isArcSupported, + isEarcSupported)) + .addEqualityGroup( + new HdmiPortInfo(portId, portType, address, isCec, isMhl, isArcSupported, + !isEarcSupported)) .testEquals(); } } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java index 50edd0e8e0083..2bf7812e1c3de 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecController.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java @@ -70,6 +70,10 @@ import java.util.function.Predicate; *

It can be created only by {@link HdmiCecController#create} * *

Declared as package-private, accessed by {@link HdmiControlService} only. + * + *

Also manages HDMI HAL methods that are shared between CEC and eARC. To make eARC + * fully independent of the presence of a CEC HAL, we should split this class into HdmiCecController + * and HdmiController TODO(b/255751565). */ final class HdmiCecController { private static final String TAG = "HdmiCecController"; @@ -1066,6 +1070,8 @@ final class HdmiCecController { HdmiPortInfo[] hdmiPortInfo = new HdmiPortInfo[hdmiPortInfos.length]; int i = 0; for (android.hardware.tv.hdmi.HdmiPortInfo portInfo : hdmiPortInfos) { + // TODO: the earc argument is stubbed for now. + // To be replaced by portInfo.earcSupported. hdmiPortInfo[i] = new HdmiPortInfo( portInfo.portId, @@ -1073,7 +1079,8 @@ final class HdmiCecController { portInfo.physicalAddress, portInfo.cecSupported, false, - portInfo.arcSupported); + portInfo.arcSupported, + false); i++; } return hdmiPortInfo; @@ -1234,7 +1241,8 @@ final class HdmiCecController { portInfo.physicalAddress, portInfo.cecSupported, false, - portInfo.arcSupported); + portInfo.arcSupported, + false); i++; } return hdmiPortInfo; @@ -1415,7 +1423,8 @@ final class HdmiCecController { portInfo.physicalAddress, portInfo.cecSupported, false, - portInfo.arcSupported); + portInfo.arcSupported, + false); i++; } return hdmiPortInfo; diff --git a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java index f7089417a5800..a57292af70286 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java @@ -470,7 +470,8 @@ public class HdmiCecNetwork { for (HdmiPortInfo info : cecPortInfo) { if (mhlSupportedPorts.contains(info.getId())) { result.add(new HdmiPortInfo(info.getId(), info.getType(), info.getAddress(), - info.isCecSupported(), true, info.isArcSupported())); + info.isCecSupported(), true, info.isArcSupported(), + info.isEarcSupported())); } else { result.add(info); } From ad77ca9fb06074bd4da96aa931f764d22f36ed54 Mon Sep 17 00:00:00 2001 From: Nathalie Le Clair Date: Thu, 28 Jul 2022 14:24:55 +0200 Subject: [PATCH 3/7] Add HdmiEarcController stub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add initial methods. Bind to HdmiControlService. Don´t yet bind to native. Make the presence of the controller a condition for eARC being available on the device. Test: make Bug: 240388106 Change-Id: Id57fa5ad5d567f8d8d58e23bbf4d1ea8df0ac89b --- .../server/hdmi/HdmiControlService.java | 43 ++++++++-- .../server/hdmi/HdmiEarcController.java | 82 +++++++++++++++++++ 2 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 services/core/java/com/android/server/hdmi/HdmiEarcController.java diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 820700b0af236..7f5a7c8b7aae3 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -127,6 +127,8 @@ import java.util.stream.Collectors; /** * Provides a service for sending and processing HDMI control messages, * HDMI-CEC and MHL control command, and providing the information on both standard. + * + * Additionally takes care of establishing and managing an eARC connection. */ public class HdmiControlService extends SystemService { private static final String TAG = "HdmiControlService"; @@ -386,7 +388,8 @@ public class HdmiControlService extends SystemService { @HdmiControlManager.HdmiCecControl private int mHdmiControlEnabled; - // Set to true while the eARC feature is supported by the hardware and the eARC HAL is present. + // Set to true while the eARC feature is supported by the hardware on at least one port + // and the eARC HAL is present. @GuardedBy("mLock") @VisibleForTesting protected boolean mEarcSupported; @@ -428,6 +431,9 @@ public class HdmiControlService extends SystemService { private HdmiCecPowerStatusController mPowerStatusController; + @Nullable + private HdmiEarcController mEarcController; + @Nullable private HdmiEarcLocalDevice mEarcLocalDevice; @@ -664,7 +670,6 @@ public class HdmiControlService extends SystemService { } if (mCecController == null) { Slog.i(TAG, "Device does not support HDMI-CEC."); - return; } if (mMhlController == null) { mMhlController = HdmiMhlControllerStub.create(this); @@ -672,20 +677,46 @@ public class HdmiControlService extends SystemService { if (!mMhlController.isReady()) { Slog.i(TAG, "Device does not support MHL-control."); } + if (mEarcController == null) { + mEarcController = HdmiEarcController.create(this); + } + if (mEarcController == null) { + Slog.i(TAG, "Device does not support eARC."); + } + if (mCecController == null && mEarcController == null) { + return; + } mHdmiCecNetwork = new HdmiCecNetwork(this, mCecController, mMhlController); if (isCecControlEnabled()) { initializeCec(INITIATED_BY_BOOT_UP); } else { mCecController.enableCec(false); } + synchronized (mLock) { - if (mEarcSupported && mEarcEnabled) { - initializeEarc(INITIATED_BY_BOOT_UP); - } + mMhlDevices = Collections.emptyList(); } - mMhlDevices = Collections.emptyList(); mHdmiCecNetwork.initPortInfo(); + List ports = getPortInfo(); + synchronized (mLock) { + mEarcSupported = false; + for (HdmiPortInfo port : ports) { + if (mEarcSupported && port.isEarcSupported()) { + // The HDMI specification only allows 1 active eARC connection. Android does not + // support devices with multiple eARC-enabled ports. + Slog.e(TAG, "HDMI eARC supported on more than 1 port."); + break; + } + mEarcSupported |= port.isEarcSupported(); + } + + mEarcSupported &= (mEarcController != null); + } + if (isEarcSupportedAndEnabled()) { + initializeEarc(INITIATED_BY_BOOT_UP); + } + mHdmiCecConfig.registerChangeListener(HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED, new HdmiCecConfig.SettingChangeListener() { @Override diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcController.java b/services/core/java/com/android/server/hdmi/HdmiEarcController.java new file mode 100644 index 0000000000000..e4acbd11acb2d --- /dev/null +++ b/services/core/java/com/android/server/hdmi/HdmiEarcController.java @@ -0,0 +1,82 @@ +/* + * 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.os.Handler; +import android.os.Looper; + +import com.android.internal.annotations.VisibleForTesting; + +final class HdmiEarcController { + private static final String TAG = "HdmiEarcController"; + + // Handler instance to process HAL calls. + private Handler mControlHandler; + + private final HdmiControlService mService; + + // Private constructor. Use HdmiEarcController.create(). + private HdmiEarcController(HdmiControlService service) { + mService = service; + } + + /** + * A factory method to get {@link HdmiEarcController}. If it fails to initialize + * inner device or has no device it will return {@code null}. + * + *

Declared as package-private, accessed by {@link HdmiControlService} only. + * @param service {@link HdmiControlService} instance used to create internal handler + * and to pass callback for incoming message or event. + * @return {@link HdmiEarcController} if device is initialized successfully. Otherwise, + * returns {@code null}. + */ + static HdmiEarcController create(HdmiControlService service) { + // TODO add the native wrapper and return null if eARC HAL is not present. + HdmiEarcController controller = new HdmiEarcController(service); + controller.init(); + return controller; + } + + private void init() { + mControlHandler = new Handler(mService.getServiceLooper()); + } + + private void assertRunOnServiceThread() { + if (Looper.myLooper() != mControlHandler.getLooper()) { + throw new IllegalStateException("Should run on service thread."); + } + } + + @VisibleForTesting + void runOnServiceThread(Runnable runnable) { + mControlHandler.post(new WorkSourceUidPreservingRunnable(runnable)); + } + + /** + * Enable eARC in the HAL + * @param enabled + */ + @HdmiAnnotations.ServiceThreadOnly + void setEarcEnabled(boolean enabled) { + assertRunOnServiceThread(); + // Stub. + // TODO: bind to native. + // TODO: handle error return values here, with logging. + } + + // TODO: bind to native. +} From 40355178f20c01a9141146bb3f828d32243f4208 Mon Sep 17 00:00:00 2001 From: Nathalie Le Clair Date: Tue, 9 Aug 2022 09:18:03 +0200 Subject: [PATCH 4/7] Enable and disable eARC in the HAL Depending on the settings value, enable or disable on boot and wakeup. Enable when the setting gets enabled. Disable when the setting gets disabled. Test: atest Bug: 240388084 Change-Id: I74a90d26e8cde0f9acfd5eb1b33c845a6a995b18 --- .../server/hdmi/HdmiCecLocalDevice.java | 2 +- .../server/hdmi/HdmiControlService.java | 68 +++++++--- .../server/hdmi/HdmiEarcLocalDevice.java | 5 +- .../android/server/hdmi/HdmiLocalDevice.java | 26 ++++ .../server/hdmi/HdmiControlServiceTest.java | 128 +++++++++++++++++- 5 files changed, 203 insertions(+), 26 deletions(-) create mode 100644 services/core/java/com/android/server/hdmi/HdmiLocalDevice.java diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java index b4d7fb9d91532..689183fbbf74a 100755 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java @@ -54,7 +54,7 @@ import java.util.concurrent.ArrayBlockingQueue; * Class that models a logical CEC device hosted in this system. Handles initialization, CEC * commands that call for actions customized per device type. */ -abstract class HdmiCecLocalDevice { +abstract class HdmiCecLocalDevice extends HdmiLocalDevice { private static final String TAG = "HdmiCecLocalDevice"; private static final int MAX_HDMI_ACTIVE_SOURCE_HISTORY = 10; diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 7f5a7c8b7aae3..45c7de9763b7a 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -713,8 +713,12 @@ public class HdmiControlService extends SystemService { mEarcSupported &= (mEarcController != null); } - if (isEarcSupportedAndEnabled()) { - initializeEarc(INITIATED_BY_BOOT_UP); + if (isEarcSupported()) { + if (isEarcEnabled()) { + initializeEarc(INITIATED_BY_BOOT_UP); + } else { + setEarcEnabledInHal(false); + } } mHdmiCecConfig.registerChangeListener(HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED, @@ -3386,9 +3390,15 @@ public class HdmiControlService extends SystemService { } } - private boolean isEarcSupportedAndEnabled() { + private boolean isEarcEnabled() { synchronized (mLock) { - return mEarcSupported && mEarcEnabled; + return mEarcEnabled; + } + } + + private boolean isEarcSupported() { + synchronized (mLock) { + return mEarcSupported; } } @@ -3485,21 +3495,24 @@ public class HdmiControlService extends SystemService { } else { Slog.i(TAG, "Device does not support HDMI-CEC."); } - if (isEarcSupportedAndEnabled()) { - int startReason = -1; - switch (wakeUpAction) { - case WAKE_UP_SCREEN_ON: - startReason = INITIATED_BY_SCREEN_ON; - break; - case WAKE_UP_BOOT_UP: - startReason = INITIATED_BY_BOOT_UP; - break; - default: - Slog.e(TAG, "wakeUpAction " + wakeUpAction + " not defined."); - return; - + if (isEarcSupported()) { + if (isEarcEnabled()) { + int startReason = -1; + switch (wakeUpAction) { + case WAKE_UP_SCREEN_ON: + startReason = INITIATED_BY_SCREEN_ON; + break; + case WAKE_UP_BOOT_UP: + startReason = INITIATED_BY_BOOT_UP; + break; + default: + Slog.e(TAG, "wakeUpAction " + wakeUpAction + " not defined."); + return; + } + initializeEarc(startReason); + } else { + setEarcEnabledInHal(false); } - initializeEarc(startReason); } // TODO: Initialize MHL local devices. } @@ -4394,6 +4407,7 @@ public class HdmiControlService extends SystemService { private void initializeEarc(int initiatedBy) { Slog.i(TAG, "eARC initialized, reason = " + initiatedBy); + setEarcEnabledInHal(true); initializeEarcLocalDevice(initiatedBy); } @@ -4415,8 +4429,8 @@ public class HdmiControlService extends SystemService { synchronized (mLock) { mEarcEnabled = (enabled == EARC_FEATURE_ENABLED); - if (!mEarcSupported) { - Slog.i(TAG, "Enabled/disabled eARC setting, but the hardware doesn´t support eARC." + if (!isEarcSupported()) { + Slog.i(TAG, "Enabled/disabled eARC setting, but the hardware doesn´t support eARC. " + "This settings change doesn´t have an effect."); return; } @@ -4448,6 +4462,8 @@ public class HdmiControlService extends SystemService { @ServiceThreadOnly private void onDisableEarc() { + disableEarcLocalDevice(); + setEarcEnabledInHal(false); clearEarcLocalDevice(); } @@ -4471,5 +4487,17 @@ public class HdmiControlService extends SystemService { assertRunOnServiceThread(); return mEarcLocalDevice; } + private void disableEarcLocalDevice() { + if (mEarcLocalDevice == null) { + return; + } + mEarcLocalDevice.disableDevice(); + } + @ServiceThreadOnly + @VisibleForTesting + protected void setEarcEnabledInHal(boolean enabled) { + assertRunOnServiceThread(); + mEarcController.setEarcEnabled(enabled); + } } diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java index 1e9d338fc2d68..720dff5e77adb 100644 --- a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java @@ -22,9 +22,12 @@ import android.util.IndentingPrintWriter; * Class that models a local eARC device hosted in this system. * The class contains methods that are common between eARC TX and eARC RX devices. */ -public class HdmiEarcLocalDevice { +public class HdmiEarcLocalDevice extends HdmiLocalDevice { private static final String TAG = "HdmiEarcLocalDevice"; + protected void disableDevice() { + } + /** Dump internal status of HdmiEarcLocalDevice object */ protected void dump(final IndentingPrintWriter pw) { // Should be overridden in the more specific classes diff --git a/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java new file mode 100644 index 0000000000000..96fb28c5321ca --- /dev/null +++ b/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java @@ -0,0 +1,26 @@ +/* + * 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; + +/** + * Class that models an HDMI device hosted in this system. + * Can be used to share methods between CEC and eARC local devices. + * Currently just a placeholder. + */ +abstract class HdmiLocalDevice { + private static final String TAG = "HdmiLocalDevice"; +} 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 f07ee6c2be4e7..ff873faf02b7d 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java @@ -22,6 +22,7 @@ import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_TV; import static com.android.server.SystemService.PHASE_BOOT_COMPLETED; import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; +import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON; import static com.google.common.truth.Truth.assertThat; @@ -29,6 +30,7 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static junit.framework.TestCase.assertEquals; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; @@ -128,13 +130,13 @@ public class HdmiControlServiceTest { mLocalDevices.add(mPlaybackDeviceSpy); mHdmiPortInfo = new HdmiPortInfo[4]; mHdmiPortInfo[0] = - new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x2100, true, false, false); + new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x2100, true, false, false, false); mHdmiPortInfo[1] = - new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2200, true, false, false); + new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2200, true, false, false, false); mHdmiPortInfo[2] = - new HdmiPortInfo(3, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, false); + new HdmiPortInfo(3, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true, true); mHdmiPortInfo[3] = - new HdmiPortInfo(4, HdmiPortInfo.PORT_INPUT, 0x3000, true, false, false); + new HdmiPortInfo(4, HdmiPortInfo.PORT_INPUT, 0x3000, true, false, false, false); mNativeWrapper.setPortInfo(mHdmiPortInfo); mHdmiControlServiceSpy.initService(); mPowerManager = new FakePowerManagerWrapper(mContextSpy); @@ -1131,6 +1133,124 @@ public class HdmiControlServiceTest { verify(mHdmiControlServiceSpy, times(1)).initializeEarcLocalDevice(anyInt()); } + @Test + public void disableCec_DoNotInformHalAboutEarc() { + mHdmiControlServiceSpy.setEarcSupported(true); + mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue( + HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED, + HdmiControlManager.HDMI_CEC_CONTROL_ENABLED); + mTestLooper.dispatchAll(); + Mockito.clearInvocations(mHdmiControlServiceSpy); + mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue( + HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED, + HdmiControlManager.HDMI_CEC_CONTROL_DISABLED); + mTestLooper.dispatchAll(); + verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(anyBoolean()); + } + + @Test + public void disableEarc_informHalAboutEarc() { + mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue( + HdmiControlManager.SETTING_NAME_EARC_ENABLED, + HdmiControlManager.EARC_FEATURE_ENABLED); + mHdmiControlServiceSpy.setEarcSupported(true); + mTestLooper.dispatchAll(); + Mockito.clearInvocations(mHdmiControlServiceSpy); + mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue( + HdmiControlManager.SETTING_NAME_EARC_ENABLED, + HdmiControlManager.EARC_FEATURE_DISABLED); + mTestLooper.dispatchAll(); + verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false); + verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(true); + } + + @Test + public void enableCec_DoNotInformHalAboutEarc() { + mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue( + HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED, + HdmiControlManager.HDMI_CEC_CONTROL_DISABLED); + mHdmiControlServiceSpy.setEarcSupported(true); + mTestLooper.dispatchAll(); + Mockito.clearInvocations(mHdmiControlServiceSpy); + mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue( + HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED, + HdmiControlManager.HDMI_CEC_CONTROL_ENABLED); + mTestLooper.dispatchAll(); + verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(anyBoolean()); + } + + @Test + public void enableEarc_informHalAboutEarc() { + mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue( + HdmiControlManager.SETTING_NAME_EARC_ENABLED, + HdmiControlManager.EARC_FEATURE_DISABLED); + mHdmiControlServiceSpy.setEarcSupported(true); + mTestLooper.dispatchAll(); + Mockito.clearInvocations(mHdmiControlServiceSpy); + mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue( + HdmiControlManager.SETTING_NAME_EARC_ENABLED, + HdmiControlManager.EARC_FEATURE_ENABLED); + mTestLooper.dispatchAll(); + verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true); + verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(false); + } + + @Test + public void bootWithEarcEnabled_informHalAboutEarc() { + mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue( + HdmiControlManager.SETTING_NAME_EARC_ENABLED, + HdmiControlManager.EARC_FEATURE_ENABLED); + mHdmiControlServiceSpy.setEarcSupported(true); + mTestLooper.dispatchAll(); + Mockito.clearInvocations(mHdmiControlServiceSpy); + mHdmiControlServiceSpy.initService(); + mTestLooper.dispatchAll(); + verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true); + verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(false); + } + + @Test + public void bootWithEarcDisabled_informHalAboutEarc() { + mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue( + HdmiControlManager.SETTING_NAME_EARC_ENABLED, + HdmiControlManager.EARC_FEATURE_DISABLED); + mHdmiControlServiceSpy.setEarcSupported(true); + mTestLooper.dispatchAll(); + Mockito.clearInvocations(mHdmiControlServiceSpy); + mHdmiControlServiceSpy.initService(); + mTestLooper.dispatchAll(); + verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false); + verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(true); + } + + @Test + public void wakeUpWithEarcEnabled_informHalAboutEarc() { + mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue( + HdmiControlManager.SETTING_NAME_EARC_ENABLED, + HdmiControlManager.EARC_FEATURE_ENABLED); + mHdmiControlServiceSpy.setEarcSupported(true); + mTestLooper.dispatchAll(); + Mockito.clearInvocations(mHdmiControlServiceSpy); + mHdmiControlServiceSpy.onWakeUp(WAKE_UP_SCREEN_ON); + mTestLooper.dispatchAll(); + verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true); + verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(false); + } + + @Test + public void wakeUpWithEarcDisabled_informHalAboutEarc() { + mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue( + HdmiControlManager.SETTING_NAME_EARC_ENABLED, + HdmiControlManager.EARC_FEATURE_DISABLED); + mHdmiControlServiceSpy.setEarcSupported(true); + mTestLooper.dispatchAll(); + Mockito.clearInvocations(mHdmiControlServiceSpy); + mHdmiControlServiceSpy.onWakeUp(WAKE_UP_SCREEN_ON); + mTestLooper.dispatchAll(); + verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false); + verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(true); + } + protected static class MockPlaybackDevice extends HdmiCecLocalDevicePlayback { private boolean mCanGoToStandby; From 3814f1e23e50e857fdaff9a4e91328a4868f8be9 Mon Sep 17 00:00:00 2001 From: Nathalie Le Clair Date: Fri, 23 Sep 2022 09:35:29 +0200 Subject: [PATCH 5/7] Process eARC status updates from HAL Also inform the HDMI HAL which hotplug signal it should use for features other than eARC, e.g. EDID updates. When eARC is enabled, eARC uses the physical HPD signal and the other applications need to use the HPD bit instead. Bug: 260547935 Test: make and atest Change-Id: I7dc17270a16ece368d6db6e3674108f960fad63b --- .../com/android/server/hdmi/Constants.java | 20 +++++++++ .../server/hdmi/HdmiCecController.java | 25 +++++++++++ .../server/hdmi/HdmiCecLocalDevice.java | 8 +--- .../server/hdmi/HdmiControlService.java | 42 +++++++++++++++---- .../server/hdmi/HdmiEarcController.java | 20 +++++++++ .../server/hdmi/HdmiEarcLocalDevice.java | 27 +++++++++++- .../server/hdmi/HdmiEarcLocalDeviceTx.java | 41 +++++++++++++++++- .../android/server/hdmi/HdmiLocalDevice.java | 14 +++++++ .../server/hdmi/HdmiControlServiceTest.java | 6 ++- 9 files changed, 184 insertions(+), 19 deletions(-) diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java index aaa9ee5f59267..5353092c9a0b9 100644 --- a/services/core/java/com/android/server/hdmi/Constants.java +++ b/services/core/java/com/android/server/hdmi/Constants.java @@ -599,6 +599,26 @@ final class Constants { }) @interface RcProfileSource {} + static final int HDMI_EARC_STATUS_IDLE = 0; // IDLE1 + static final int HDMI_EARC_STATUS_EARC_PENDING = 1; // DISC1 and DISC2 + static final int HDMI_EARC_STATUS_ARC_PENDING = 2; // IDLE2 for ARC + static final int HDMI_EARC_STATUS_EARC_CONNECTED = 3; // eARC connected + @IntDef({ + HDMI_EARC_STATUS_IDLE, + HDMI_EARC_STATUS_EARC_PENDING, + HDMI_EARC_STATUS_ARC_PENDING, + HDMI_EARC_STATUS_EARC_CONNECTED + }) + @interface EarcStatus {} + + static final int HDMI_HPD_TYPE_PHYSICAL = 0; // Default. Physical hotplug signal. + static final int HDMI_HPD_TYPE_STATUS_BIT = 1; // HDMI_HPD status bit. + @IntDef({ + HDMI_HPD_TYPE_PHYSICAL, + HDMI_HPD_TYPE_STATUS_BIT + }) + @interface HpdSignalType {} + private Constants() { /* cannot be instantiated */ } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java index 2bf7812e1c3de..426f81638bb33 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecController.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java @@ -416,6 +416,31 @@ final class HdmiCecController { mNativeWrapperImpl.enableSystemCecControl(enabled); } + /** + * Configures the type of HDP signal that the driver and HAL use for actions other than eARC, + * such as signaling EDID updates. + */ + @ServiceThreadOnly + void setHpdSignalType(@Constants.HpdSignalType int signal, int portId) { + assertRunOnServiceThread(); + // Stub. + // TODO: bind to native. + // TODO: handle error return values here, with logging. + } + + /** + * Gets the type of the HDP signal that the driver and HAL use for actions other than eARC, + * such as signaling EDID updates. + */ + @ServiceThreadOnly + @Constants.HpdSignalType + int getHpdSignalType(int portId) { + assertRunOnServiceThread(); + // Stub. + // TODO: bind to native. + return Constants.HDMI_HPD_TYPE_PHYSICAL; + } + /** * Informs CEC HAL about the current system language. * diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java index 689183fbbf74a..91f58db407f21 100755 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java @@ -67,8 +67,6 @@ abstract class HdmiCecLocalDevice extends HdmiLocalDevice { // When it expires, we can assume is received. private static final int FOLLOWER_SAFETY_TIMEOUT = 550; - protected final HdmiControlService mService; - protected final int mDeviceType; protected int mPreferredAddress; @GuardedBy("mLock") private HdmiDeviceInfo mDeviceInfo; @@ -154,8 +152,6 @@ abstract class HdmiCecLocalDevice extends HdmiLocalDevice { private int mActiveRoutingPath; protected final HdmiCecMessageCache mCecMessageCache = new HdmiCecMessageCache(); - @VisibleForTesting - protected final Object mLock; // A collection of FeatureAction. // Note that access to this collection should happen in service thread. @@ -188,9 +184,7 @@ abstract class HdmiCecLocalDevice extends HdmiLocalDevice { protected PendingActionClearedCallback mPendingActionClearedCallback; protected HdmiCecLocalDevice(HdmiControlService service, int deviceType) { - mService = service; - mDeviceType = deviceType; - mLock = service.getServiceLock(); + super(service, deviceType); } // Factory method that returns HdmiCecLocalDevice of corresponding type. diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 45c7de9763b7a..735dddf2f4274 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -398,6 +398,8 @@ public class HdmiControlService extends SystemService { @GuardedBy("mLock") private boolean mEarcEnabled; + private int mEarcPortId = -1; + // Set to true while the service is in normal mode. While set to false, no input change is // allowed. Used for situations where input change can confuse users such as channel auto-scan, // system upgrade, etc., a.k.a. "prohibit mode". @@ -702,15 +704,21 @@ public class HdmiControlService extends SystemService { synchronized (mLock) { mEarcSupported = false; for (HdmiPortInfo port : ports) { - if (mEarcSupported && port.isEarcSupported()) { - // The HDMI specification only allows 1 active eARC connection. Android does not - // support devices with multiple eARC-enabled ports. + boolean earcSupportedOnPort = port.isEarcSupported(); + if (earcSupportedOnPort && mEarcSupported) { + // This means that more than 1 port supports eARC. + // The HDMI specification only allows 1 active eARC connection. + // Android does not support devices with multiple eARC-enabled ports. + // Consider eARC not supported in this case. Slog.e(TAG, "HDMI eARC supported on more than 1 port."); + mEarcSupported = false; + mEarcPortId = -1; break; + } else if (earcSupportedOnPort) { + mEarcPortId = port.getId(); + mEarcSupported = earcSupportedOnPort; } - mEarcSupported |= port.isEarcSupported(); } - mEarcSupported &= (mEarcController != null); } if (isEarcSupported()) { @@ -4416,8 +4424,8 @@ public class HdmiControlService extends SystemService { protected void initializeEarcLocalDevice(final int initiatedBy) { // TODO remove initiatedBy argument if it stays unused assertRunOnServiceThread(); - if (isTvDevice() && mEarcLocalDevice == null) { - mEarcLocalDevice = new HdmiEarcLocalDeviceTx(); + if (mEarcLocalDevice == null) { + mEarcLocalDevice = HdmiEarcLocalDevice.create(this, HdmiDeviceInfo.DEVICE_TV); } // TODO create HdmiEarcLocalDeviceRx if we're an audio system device. } @@ -4483,10 +4491,11 @@ public class HdmiControlService extends SystemService { @ServiceThreadOnly @VisibleForTesting - protected HdmiEarcLocalDevice getEarcLocalDevice() { + HdmiEarcLocalDevice getEarcLocalDevice() { assertRunOnServiceThread(); return mEarcLocalDevice; } + private void disableEarcLocalDevice() { if (mEarcLocalDevice == null) { return; @@ -4499,5 +4508,22 @@ public class HdmiControlService extends SystemService { protected void setEarcEnabledInHal(boolean enabled) { assertRunOnServiceThread(); mEarcController.setEarcEnabled(enabled); + mCecController.setHpdSignalType( + enabled ? Constants.HDMI_HPD_TYPE_STATUS_BIT : Constants.HDMI_HPD_TYPE_PHYSICAL, + mEarcPortId); + } + + @ServiceThreadOnly + void handleEarcStateChange(int status, int portId) { + assertRunOnServiceThread(); + if (!getPortInfo(portId).isEarcSupported()) { + Slog.w(TAG, "Tried to update eARC status on a port that doesn't support eARC."); + return; + } + // If eARC is disabled, the local device is null. In this case, the HAL shouldn't have + // reported connection state changes, but even if it did, it won't take effect. + if (mEarcLocalDevice != null) { + mEarcLocalDevice.handleEarcStateChange(status); + } } } diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcController.java b/services/core/java/com/android/server/hdmi/HdmiEarcController.java index e4acbd11acb2d..e38b2cd82c15d 100644 --- a/services/core/java/com/android/server/hdmi/HdmiEarcController.java +++ b/services/core/java/com/android/server/hdmi/HdmiEarcController.java @@ -78,5 +78,25 @@ final class HdmiEarcController { // TODO: handle error return values here, with logging. } + /** + * Getter for the current eARC state. + * @param portId the ID of the port on which to get the connection state + * @return the current eARC state + */ + @HdmiAnnotations.ServiceThreadOnly + @Constants.EarcStatus + int getState(int portId) { + // Stub. + // TODO: bind to native. + return Constants.HDMI_EARC_STATUS_IDLE; + } + + final class EarcCallback { + public void onStateChange(@Constants.EarcStatus int status, int portId) { + runOnServiceThread( + () -> mService.handleEarcStateChange(status, portId)); + } + } + // TODO: bind to native. } diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java index 720dff5e77adb..231775e374792 100644 --- a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java @@ -16,15 +16,40 @@ package com.android.server.hdmi; +import android.hardware.hdmi.HdmiDeviceInfo; import android.util.IndentingPrintWriter; +import com.android.internal.annotations.GuardedBy; + /** * Class that models a local eARC device hosted in this system. * The class contains methods that are common between eARC TX and eARC RX devices. */ -public class HdmiEarcLocalDevice extends HdmiLocalDevice { +abstract class HdmiEarcLocalDevice extends HdmiLocalDevice { private static final String TAG = "HdmiEarcLocalDevice"; + // The current status of the eARC connection, as reported by the HAL + @GuardedBy("mLock") + @Constants.EarcStatus + protected int mEarcStatus; + + protected HdmiEarcLocalDevice(HdmiControlService service, int deviceType) { + super(service, deviceType); + } + + // Factory method that returns HdmiCecLocalDevice of corresponding type. + static HdmiEarcLocalDevice create(HdmiControlService service, int deviceType) { + switch (deviceType) { + case HdmiDeviceInfo.DEVICE_TV: + return new HdmiEarcLocalDeviceTx(service); + default: + return null; + } + } + + protected abstract void handleEarcStateChange(@Constants.EarcStatus int status); + + protected void disableDevice() { } diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java index c1c1e7794676d..b01daa51fb482 100644 --- a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java +++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java @@ -16,8 +16,19 @@ package com.android.server.hdmi; +import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_ARC_PENDING; +import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_CONNECTED; +import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_IDLE; + +import android.hardware.hdmi.HdmiDeviceInfo; +import android.media.AudioDescriptor; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; +import android.media.AudioProfile; import android.util.IndentingPrintWriter; +import java.util.ArrayList; + /** * Represents a local eARC device of type TX residing in the Android system. * Only TV panel devices can have a local eARC TX device. @@ -25,8 +36,36 @@ import android.util.IndentingPrintWriter; public class HdmiEarcLocalDeviceTx extends HdmiEarcLocalDevice { private static final String TAG = "HdmiEarcLocalDeviceTx"; + HdmiEarcLocalDeviceTx(HdmiControlService service) { + super(service, HdmiDeviceInfo.DEVICE_TV); + } + + protected void handleEarcStateChange(@Constants.EarcStatus int status) { + synchronized (mLock) { + HdmiLogger.debug(TAG, "eARC state change [old:%b new %b]", mEarcStatus, + status); + mEarcStatus = status; + } + if (status == HDMI_EARC_STATUS_IDLE) { + notifyEarcStatusToAudioService(false); + } else if (status == HDMI_EARC_STATUS_ARC_PENDING) { + notifyEarcStatusToAudioService(false); + } else if (status == HDMI_EARC_STATUS_EARC_CONNECTED) { + notifyEarcStatusToAudioService(true); + } + } + + private void notifyEarcStatusToAudioService(boolean enabled) { + AudioDeviceAttributes attributes = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI_EARC, "", "", + new ArrayList(), new ArrayList()); + mService.getAudioManager().setWiredDeviceConnectionState(attributes, enabled ? 1 : 0); + } + /** Dump internal status of HdmiEarcLocalDeviceTx object */ protected void dump(final IndentingPrintWriter pw) { - pw.println("TX"); + synchronized (mLock) { + pw.println("TX, mEarcStatus: " + mEarcStatus); + } } } diff --git a/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java index 96fb28c5321ca..1e68734aed3a5 100644 --- a/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java @@ -16,6 +16,8 @@ package com.android.server.hdmi; +import com.android.internal.annotations.VisibleForTesting; + /** * Class that models an HDMI device hosted in this system. * Can be used to share methods between CEC and eARC local devices. @@ -23,4 +25,16 @@ package com.android.server.hdmi; */ abstract class HdmiLocalDevice { private static final String TAG = "HdmiLocalDevice"; + + protected final HdmiControlService mService; + protected final int mDeviceType; + + @VisibleForTesting + protected final Object mLock; + + protected HdmiLocalDevice(HdmiControlService service, int deviceType) { + mService = service; + mDeviceType = deviceType; + mLock = service.getServiceLock(); + } } 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 ff873faf02b7d..2b555a07a50fd 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java @@ -1089,7 +1089,8 @@ public class HdmiControlServiceTest { public void disableEarc_clearEarcLocalDevice() { mHdmiControlServiceSpy.setEarcSupported(true); mHdmiControlServiceSpy.clearEarcLocalDevice(); - mHdmiControlServiceSpy.addEarcLocalDevice(new HdmiEarcLocalDeviceTx()); + mHdmiControlServiceSpy.addEarcLocalDevice( + new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy)); assertThat(mHdmiControlServiceSpy.getEarcLocalDevice()).isNotNull(); mHdmiControlServiceSpy.setEarcEnabled(HdmiControlManager.EARC_FEATURE_DISABLED); @@ -1101,7 +1102,8 @@ public class HdmiControlServiceTest { public void disableCec_doNotClearEarcLocalDevice() { mHdmiControlServiceSpy.setEarcSupported(true); mHdmiControlServiceSpy.clearEarcLocalDevice(); - mHdmiControlServiceSpy.addEarcLocalDevice(new HdmiEarcLocalDeviceTx()); + mHdmiControlServiceSpy.addEarcLocalDevice( + new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy)); assertThat(mHdmiControlServiceSpy.getEarcLocalDevice()).isNotNull(); mHdmiControlServiceSpy.setCecEnabled(HdmiControlManager.HDMI_CEC_CONTROL_DISABLED); From ccc138adba9de2d3da7f1200e7ed4e4188e0185c Mon Sep 17 00:00:00 2001 From: Nathalie Le Clair Date: Fri, 4 Nov 2022 16:40:55 +0100 Subject: [PATCH 6/7] Process eARC capabilities reported by the HAL Test: atest Bug: 260547656 Change-Id: Iec7435316194c51ffea41596bbdaeb308d61cdba --- .../server/hdmi/HdmiControlService.java | 16 +- .../server/hdmi/HdmiEarcController.java | 17 ++ .../server/hdmi/HdmiEarcLocalDevice.java | 3 + .../server/hdmi/HdmiEarcLocalDeviceTx.java | 56 +++++- .../android/server/hdmi/HdmiLocalDevice.java | 3 - .../hdmi/HdmiEarcLocalDeviceTxTest.java | 175 ++++++++++++++++++ 6 files changed, 261 insertions(+), 9 deletions(-) create mode 100644 services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 735dddf2f4274..de01f9383fa58 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -1320,7 +1320,6 @@ public class HdmiControlService extends SystemService { * Returns {@link Looper} of main thread. Use this {@link Looper} instance * for tasks that are running on main service thread. */ - @VisibleForTesting protected Looper getServiceLooper() { return mHandler.getLooper(); } @@ -4526,4 +4525,19 @@ public class HdmiControlService extends SystemService { mEarcLocalDevice.handleEarcStateChange(status); } } + + @ServiceThreadOnly + void handleEarcCapabilitiesReported(List capabilities, int portId) { + assertRunOnServiceThread(); + if (!getPortInfo(portId).isEarcSupported()) { + Slog.w(TAG, + "Tried to process eARC capabilities from a port that doesn't support eARC."); + return; + } + // If eARC is disabled, the local device is null. In this case, the HAL shouldn't have + // reported eARC capabilities, but even if it did, it won't take effect. + if (mEarcLocalDevice != null) { + mEarcLocalDevice.handleEarcCapabilitiesReported(capabilities); + } + } } diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcController.java b/services/core/java/com/android/server/hdmi/HdmiEarcController.java index e38b2cd82c15d..ca623810131b3 100644 --- a/services/core/java/com/android/server/hdmi/HdmiEarcController.java +++ b/services/core/java/com/android/server/hdmi/HdmiEarcController.java @@ -21,6 +21,8 @@ import android.os.Looper; import com.android.internal.annotations.VisibleForTesting; +import java.util.List; + final class HdmiEarcController { private static final String TAG = "HdmiEarcController"; @@ -91,11 +93,26 @@ final class HdmiEarcController { return Constants.HDMI_EARC_STATUS_IDLE; } + /** + * Ask the HAL to report the last eARC capabilities that the connected audio system reported. + * @return the raw eARC capabilities + */ + @HdmiAnnotations.ServiceThreadOnly + byte[] getLastReportedCaps() { + // Stub. TODO: bind to native. + return new byte[] {}; + } + final class EarcCallback { public void onStateChange(@Constants.EarcStatus int status, int portId) { runOnServiceThread( () -> mService.handleEarcStateChange(status, portId)); } + + public void onCapabilitiesReported(List capabilities, int portId) { + runOnServiceThread( + () -> mService.handleEarcCapabilitiesReported(capabilities, portId)); + } } // TODO: bind to native. diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java index 231775e374792..6859bdbab7f09 100644 --- a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java @@ -21,6 +21,8 @@ import android.util.IndentingPrintWriter; import com.android.internal.annotations.GuardedBy; +import java.util.List; + /** * Class that models a local eARC device hosted in this system. * The class contains methods that are common between eARC TX and eARC RX devices. @@ -49,6 +51,7 @@ abstract class HdmiEarcLocalDevice extends HdmiLocalDevice { protected abstract void handleEarcStateChange(@Constants.EarcStatus int status); + protected abstract void handleEarcCapabilitiesReported(List capabilities); protected void disableDevice() { } diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java index b01daa51fb482..6e629059e6d2f 100644 --- a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java +++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java @@ -25,9 +25,12 @@ import android.media.AudioDescriptor; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioProfile; +import android.os.Handler; import android.util.IndentingPrintWriter; import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; /** * Represents a local eARC device of type TX residing in the Android system. @@ -36,8 +39,19 @@ import java.util.ArrayList; public class HdmiEarcLocalDeviceTx extends HdmiEarcLocalDevice { private static final String TAG = "HdmiEarcLocalDeviceTx"; + // How long to wait for the audio system to report its capabilities after eARC was connected + static final long REPORT_CAPS_MAX_DELAY_MS = 2_000; + + // Handler and runnable for waiting for the audio system to report its capabilities after eARC + // was connected + private Handler mReportCapsHandler; + private ReportCapsRunnable mReportCapsRunnable; + HdmiEarcLocalDeviceTx(HdmiControlService service) { super(service, HdmiDeviceInfo.DEVICE_TV); + + mReportCapsHandler = new Handler(service.getServiceLooper()); + mReportCapsRunnable = new ReportCapsRunnable(); } protected void handleEarcStateChange(@Constants.EarcStatus int status) { @@ -46,22 +60,54 @@ public class HdmiEarcLocalDeviceTx extends HdmiEarcLocalDevice { status); mEarcStatus = status; } + + mReportCapsHandler.removeCallbacksAndMessages(null); if (status == HDMI_EARC_STATUS_IDLE) { - notifyEarcStatusToAudioService(false); + notifyEarcStatusToAudioService(false, new ArrayList<>()); } else if (status == HDMI_EARC_STATUS_ARC_PENDING) { - notifyEarcStatusToAudioService(false); + notifyEarcStatusToAudioService(false, new ArrayList<>()); } else if (status == HDMI_EARC_STATUS_EARC_CONNECTED) { - notifyEarcStatusToAudioService(true); + mReportCapsHandler.postDelayed(mReportCapsRunnable, REPORT_CAPS_MAX_DELAY_MS); } } - private void notifyEarcStatusToAudioService(boolean enabled) { + protected void handleEarcCapabilitiesReported(List capabilities) { + synchronized (mLock) { + if (mEarcStatus == HDMI_EARC_STATUS_EARC_CONNECTED + && mReportCapsHandler.hasCallbacks(mReportCapsRunnable)) { + mReportCapsHandler.removeCallbacksAndMessages(null); + notifyEarcStatusToAudioService(true, capabilities); + } + } + } + + private void notifyEarcStatusToAudioService(boolean enabled, List capabilities) { AudioDeviceAttributes attributes = new AudioDeviceAttributes( AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI_EARC, "", "", - new ArrayList(), new ArrayList()); + new ArrayList(), capabilities.stream() + .map(cap -> new AudioDescriptor(AudioDescriptor.STANDARD_EDID, + AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE, cap)) + .collect(Collectors.toList())); mService.getAudioManager().setWiredDeviceConnectionState(attributes, enabled ? 1 : 0); } + /** + * Runnable for waiting for a certain amount of time for the audio system to report its + * capabilities after eARC was connected. If the audio system doesn´t report its capabilities in + * this time, we inform AudioService about the connection state only, without any specified + * capabilities. + */ + private class ReportCapsRunnable implements Runnable { + @Override + public void run() { + synchronized (mLock) { + if (mEarcStatus == HDMI_EARC_STATUS_EARC_CONNECTED) { + notifyEarcStatusToAudioService(true, new ArrayList<>()); + } + } + } + } + /** Dump internal status of HdmiEarcLocalDeviceTx object */ protected void dump(final IndentingPrintWriter pw) { synchronized (mLock) { diff --git a/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java index 1e68734aed3a5..f661e40bb3f16 100644 --- a/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java @@ -16,8 +16,6 @@ package com.android.server.hdmi; -import com.android.internal.annotations.VisibleForTesting; - /** * Class that models an HDMI device hosted in this system. * Can be used to share methods between CEC and eARC local devices. @@ -29,7 +27,6 @@ abstract class HdmiLocalDevice { protected final HdmiControlService mService; protected final int mDeviceType; - @VisibleForTesting protected final Object mLock; protected HdmiLocalDevice(HdmiControlService service, int deviceType) { diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java new file mode 100644 index 0000000000000..bf78a659a26ba --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java @@ -0,0 +1,175 @@ +/* + * 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 com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; +import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_ARC_PENDING; +import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_CONNECTED; +import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_PENDING; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.hardware.hdmi.HdmiDeviceInfo; +import android.media.AudioManager; +import android.os.Looper; +import android.os.test.TestLooper; +import android.platform.test.annotations.Presubmit; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Collections; + +@SmallTest +@Presubmit +@RunWith(JUnit4.class) +/** Tests for {@link HdmiEarcLocalDeviceTx} class. */ +public class HdmiEarcLocalDeviceTxTest { + + private HdmiControlService mHdmiControlService; + private HdmiCecController mHdmiCecController; + private HdmiEarcLocalDevice mHdmiEarcLocalDeviceTx; + private FakeNativeWrapper mNativeWrapper; + private FakePowerManagerWrapper mPowerManager; + private Looper mMyLooper; + private TestLooper mTestLooper = new TestLooper(); + + @Mock + private AudioManager mAudioManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + Context context = InstrumentationRegistry.getTargetContext(); + mMyLooper = mTestLooper.getLooper(); + + mHdmiControlService = + new HdmiControlService(InstrumentationRegistry.getTargetContext(), + Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), + new FakeAudioDeviceVolumeManagerWrapper()) { + @Override + boolean isCecControlEnabled() { + return true; + } + + @Override + boolean isTvDevice() { + return true; + } + + @Override + protected void writeStringSystemProperty(String key, String value) { + // do nothing + } + + @Override + boolean isPowerStandby() { + return false; + } + + @Override + AudioManager getAudioManager() { + return mAudioManager; + } + }; + + mHdmiControlService.setIoLooper(mMyLooper); + mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context)); + mNativeWrapper = new FakeNativeWrapper(); + mHdmiCecController = HdmiCecController.createWithNativeWrapper( + mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter()); + mHdmiControlService.setCecController(mHdmiCecController); + mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService)); + mHdmiControlService.initService(); + mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY); + mPowerManager = new FakePowerManagerWrapper(context); + mHdmiControlService.setPowerManager(mPowerManager); + mTestLooper.dispatchAll(); + mHdmiControlService.initializeEarcLocalDevice(HdmiControlService.INITIATED_BY_BOOT_UP); + mHdmiEarcLocalDeviceTx = mHdmiControlService.getEarcLocalDevice(); + } + + @Test + public void earcGetsConnected_capsReportedInTime() { + mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED); + mTestLooper.moveTimeForward(HdmiEarcLocalDeviceTx.REPORT_CAPS_MAX_DELAY_MS - 200); + mTestLooper.dispatchAll(); + // TO DO: add meaningful capabilities and test that they get forwarded to AudioManager + // correctly. + mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new ArrayList<>()); + mTestLooper.dispatchAll(); + verify(mAudioManager, times(1)).setWiredDeviceConnectionState(any(), eq(1)); + } + + @Test + public void earcGetsConnected_capsReportedTooLate() { + mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED); + mTestLooper.moveTimeForward(HdmiEarcLocalDeviceTx.REPORT_CAPS_MAX_DELAY_MS + 1); + mTestLooper.dispatchAll(); + // TO DO: verify that empty capabilities are forwarded to AudioManager. + verify(mAudioManager, times(1)).setWiredDeviceConnectionState(any(), eq(1)); + Mockito.clearInvocations(mAudioManager); + + mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new ArrayList<>()); + mTestLooper.dispatchAll(); + verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), anyInt()); + } + + @Test + public void earcGetsConnected_earcGetsDisconnectedBeforeCapsReported() { + mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED); + mTestLooper.dispatchAll(); + mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_ARC_PENDING); + mTestLooper.dispatchAll(); + verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), eq(1)); + verify(mAudioManager, times(1)).setWiredDeviceConnectionState(any(), eq(0)); + Mockito.clearInvocations(mAudioManager); + + mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new ArrayList<>()); + mTestLooper.dispatchAll(); + verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), anyInt()); + } + + @Test + public void earcGetsConnected_earcBecomesPendingBeforeCapsReported() { + mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED); + mTestLooper.dispatchAll(); + mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_PENDING); + mTestLooper.dispatchAll(); + verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), anyInt()); + Mockito.clearInvocations(mAudioManager); + + mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new ArrayList<>()); + mTestLooper.dispatchAll(); + verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), anyInt()); + } +} From 8658ee82b37866c28576fb90a7389cd158256336 Mon Sep 17 00:00:00 2001 From: Nathalie Le Clair Date: Thu, 27 Oct 2022 15:34:40 +0200 Subject: [PATCH 7/7] Parse raw eARC capabilities Test: atest Bug: 260547656 Change-Id: I3126d10d10926885adf4f2fd22531f36ffefd5f9 --- core/api/current.txt | 2 + core/jni/android_media_AudioDescriptor.h | 8 + media/java/android/media/AudioDescriptor.java | 10 ++ .../media/audio/common/AidlConversion.java | 8 + .../server/hdmi/HdmiControlService.java | 4 +- .../server/hdmi/HdmiEarcController.java | 6 +- .../server/hdmi/HdmiEarcLocalDevice.java | 4 +- .../server/hdmi/HdmiEarcLocalDeviceTx.java | 109 +++++++++++- .../hdmi/HdmiEarcLocalDeviceTxTest.java | 162 ++++++++++++++++-- 9 files changed, 285 insertions(+), 28 deletions(-) diff --git a/core/api/current.txt b/core/api/current.txt index 85ae6a149d311..4ddf67e7dc378 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -20371,6 +20371,8 @@ package android.media { field @NonNull public static final android.os.Parcelable.Creator CREATOR; field public static final int STANDARD_EDID = 1; // 0x1 field public static final int STANDARD_NONE = 0; // 0x0 + field public static final int STANDARD_SADB = 2; // 0x2 + field public static final int STANDARD_VSADB = 3; // 0x3 } public abstract class AudioDeviceCallback { diff --git a/core/jni/android_media_AudioDescriptor.h b/core/jni/android_media_AudioDescriptor.h index 680ac3f7519ea..bac761c48aaae 100644 --- a/core/jni/android_media_AudioDescriptor.h +++ b/core/jni/android_media_AudioDescriptor.h @@ -25,6 +25,8 @@ namespace android { // keep these values in sync with ExtraAudioDescriptor.java #define STANDARD_NONE 0 #define STANDARD_EDID 1 +#define STANDARD_SADB 2 +#define STANDARD_VSADB 3 static inline status_t audioStandardFromNative(audio_standard_t nStandard, int* standard) { status_t result = NO_ERROR; @@ -35,6 +37,12 @@ static inline status_t audioStandardFromNative(audio_standard_t nStandard, int* case AUDIO_STANDARD_EDID: *standard = STANDARD_EDID; break; + case AUDIO_STANDARD_SADB: + *standard = STANDARD_SADB; + break; + case AUDIO_STANDARD_VSADB: + *standard = STANDARD_VSADB; + break; default: result = BAD_VALUE; } diff --git a/media/java/android/media/AudioDescriptor.java b/media/java/android/media/AudioDescriptor.java index df648be4c157b..85a653c9d80c9 100644 --- a/media/java/android/media/AudioDescriptor.java +++ b/media/java/android/media/AudioDescriptor.java @@ -41,11 +41,21 @@ public class AudioDescriptor implements Parcelable { * The Extended Display Identification Data (EDID) standard for a short audio descriptor. */ public static final int STANDARD_EDID = 1; + /** + * The standard for a Speaker Allocation Data Block (SADB). + */ + public static final int STANDARD_SADB = 2; + /** + * The standard for a Vendor-Specific Audio Data Block (VSADB). + */ + public static final int STANDARD_VSADB = 3; /** @hide */ @IntDef({ STANDARD_NONE, STANDARD_EDID, + STANDARD_SADB, + STANDARD_VSADB, }) @Retention(RetentionPolicy.SOURCE) public @interface AudioDescriptorStandard {} diff --git a/media/java/android/media/audio/common/AidlConversion.java b/media/java/android/media/audio/common/AidlConversion.java index 490809c63e960..65fd51b9ece91 100644 --- a/media/java/android/media/audio/common/AidlConversion.java +++ b/media/java/android/media/audio/common/AidlConversion.java @@ -585,6 +585,10 @@ public class AidlConversion { switch (standard) { case AudioDescriptor.STANDARD_EDID: return AudioStandard.EDID; + case AudioDescriptor.STANDARD_SADB: + return AudioStandard.SADB; + case AudioDescriptor.STANDARD_VSADB: + return AudioStandard.VSADB; case AudioDescriptor.STANDARD_NONE: default: return AudioStandard.NONE; @@ -599,6 +603,10 @@ public class AidlConversion { switch (standard) { case AudioStandard.EDID: return AudioDescriptor.STANDARD_EDID; + case AudioStandard.SADB: + return AudioDescriptor.STANDARD_SADB; + case AudioStandard.VSADB: + return AudioDescriptor.STANDARD_VSADB; case AudioStandard.NONE: default: return AudioDescriptor.STANDARD_NONE; diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index de01f9383fa58..85bd16b2cb2f7 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -4527,7 +4527,7 @@ public class HdmiControlService extends SystemService { } @ServiceThreadOnly - void handleEarcCapabilitiesReported(List capabilities, int portId) { + void handleEarcCapabilitiesReported(byte[] rawCapabilities, int portId) { assertRunOnServiceThread(); if (!getPortInfo(portId).isEarcSupported()) { Slog.w(TAG, @@ -4537,7 +4537,7 @@ public class HdmiControlService extends SystemService { // If eARC is disabled, the local device is null. In this case, the HAL shouldn't have // reported eARC capabilities, but even if it did, it won't take effect. if (mEarcLocalDevice != null) { - mEarcLocalDevice.handleEarcCapabilitiesReported(capabilities); + mEarcLocalDevice.handleEarcCapabilitiesReported(rawCapabilities); } } } diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcController.java b/services/core/java/com/android/server/hdmi/HdmiEarcController.java index ca623810131b3..85225097ba5a6 100644 --- a/services/core/java/com/android/server/hdmi/HdmiEarcController.java +++ b/services/core/java/com/android/server/hdmi/HdmiEarcController.java @@ -21,8 +21,6 @@ import android.os.Looper; import com.android.internal.annotations.VisibleForTesting; -import java.util.List; - final class HdmiEarcController { private static final String TAG = "HdmiEarcController"; @@ -109,9 +107,9 @@ final class HdmiEarcController { () -> mService.handleEarcStateChange(status, portId)); } - public void onCapabilitiesReported(List capabilities, int portId) { + public void onCapabilitiesReported(byte[] rawCapabilities, int portId) { runOnServiceThread( - () -> mService.handleEarcCapabilitiesReported(capabilities, portId)); + () -> mService.handleEarcCapabilitiesReported(rawCapabilities, portId)); } } diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java index 6859bdbab7f09..e95ecd2bb6cdd 100644 --- a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java @@ -21,8 +21,6 @@ import android.util.IndentingPrintWriter; import com.android.internal.annotations.GuardedBy; -import java.util.List; - /** * Class that models a local eARC device hosted in this system. * The class contains methods that are common between eARC TX and eARC RX devices. @@ -51,7 +49,7 @@ abstract class HdmiEarcLocalDevice extends HdmiLocalDevice { protected abstract void handleEarcStateChange(@Constants.EarcStatus int status); - protected abstract void handleEarcCapabilitiesReported(List capabilities); + protected abstract void handleEarcCapabilitiesReported(byte[] rawCapabilities); protected void disableDevice() { } diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java index 6e629059e6d2f..c7c7702b871a1 100644 --- a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java +++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java @@ -27,10 +27,11 @@ import android.media.AudioDeviceInfo; import android.media.AudioProfile; import android.os.Handler; import android.util.IndentingPrintWriter; +import android.util.Slog; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; /** * Represents a local eARC device of type TX residing in the Android system. @@ -42,6 +43,24 @@ public class HdmiEarcLocalDeviceTx extends HdmiEarcLocalDevice { // How long to wait for the audio system to report its capabilities after eARC was connected static final long REPORT_CAPS_MAX_DELAY_MS = 2_000; + // eARC Capability Data Structure parameters + private static final int EARC_CAPS_PAYLOAD_LENGTH = 0x02; + private static final int EARC_CAPS_DATA_START = 0x03; + + // Table 55 CTA Data Block Tag Codes + private static final int TAGCODE_AUDIO_DATA_BLOCK = 0x01; // Includes one or more Short Audio + // Descriptors + private static final int TAGCODE_SADB_DATA_BLOCK = 0x04; // Speaker Allocation Data Block + private static final int TAGCODE_USE_EXTENDED_TAG = 0x07; // Use Extended Tag + + // Table 56 Extended Tag Format (2nd byte of Data Block) + private static final int EXTENDED_TAGCODE_VSADB = 0x11; // Vendor-Specific Audio Data Block + + // eARC capability mask and shift + private static final int EARC_CAPS_TAGCODE_MASK = 0xE0; + private static final int EARC_CAPS_TAGCODE_SHIFT = 0x05; + private static final int EARC_CAPS_LENGTH_MASK = 0x1F; + // Handler and runnable for waiting for the audio system to report its capabilities after eARC // was connected private Handler mReportCapsHandler; @@ -71,23 +90,22 @@ public class HdmiEarcLocalDeviceTx extends HdmiEarcLocalDevice { } } - protected void handleEarcCapabilitiesReported(List capabilities) { + protected void handleEarcCapabilitiesReported(byte[] rawCapabilities) { synchronized (mLock) { if (mEarcStatus == HDMI_EARC_STATUS_EARC_CONNECTED && mReportCapsHandler.hasCallbacks(mReportCapsRunnable)) { mReportCapsHandler.removeCallbacksAndMessages(null); - notifyEarcStatusToAudioService(true, capabilities); + List audioDescriptors = parseCapabilities(rawCapabilities); + notifyEarcStatusToAudioService(true, audioDescriptors); } } } - private void notifyEarcStatusToAudioService(boolean enabled, List capabilities) { + private void notifyEarcStatusToAudioService( + boolean enabled, List audioDescriptors) { AudioDeviceAttributes attributes = new AudioDeviceAttributes( AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI_EARC, "", "", - new ArrayList(), capabilities.stream() - .map(cap -> new AudioDescriptor(AudioDescriptor.STANDARD_EDID, - AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE, cap)) - .collect(Collectors.toList())); + new ArrayList(), audioDescriptors); mService.getAudioManager().setWiredDeviceConnectionState(attributes, enabled ? 1 : 0); } @@ -108,6 +126,81 @@ public class HdmiEarcLocalDeviceTx extends HdmiEarcLocalDevice { } } + private List parseCapabilities(byte[] rawCapabilities) { + List audioDescriptors = new ArrayList<>(); + if (rawCapabilities.length < EARC_CAPS_DATA_START + 1) { + Slog.i(TAG, "Raw eARC capabilities array doesn´t contain any blocks."); + return audioDescriptors; + } + int earcCapsSize = rawCapabilities[EARC_CAPS_PAYLOAD_LENGTH]; + if (rawCapabilities.length < earcCapsSize) { + Slog.i(TAG, "Raw eARC capabilities array is shorter than the reported payload length."); + return audioDescriptors; + } + int firstByteOfBlock = EARC_CAPS_DATA_START; + while (firstByteOfBlock < earcCapsSize) { + // Tag Code: Bit 5-7 + int tagCode = + (rawCapabilities[firstByteOfBlock] & EARC_CAPS_TAGCODE_MASK) + >> EARC_CAPS_TAGCODE_SHIFT; + // Length: Bit 0-4 + int length = rawCapabilities[firstByteOfBlock] & EARC_CAPS_LENGTH_MASK; + if (length == 0) { + // End Marker of eARC capability. + break; + } + AudioDescriptor descriptor; + switch (tagCode) { + case TAGCODE_AUDIO_DATA_BLOCK: + int earcSadLen = length; + if (length % 3 != 0) { + Slog.e(TAG, "Invalid length of SAD block: expected a factor of 3 but got " + + length % 3); + break; + } + byte[] earcSad = new byte[earcSadLen]; + System.arraycopy(rawCapabilities, firstByteOfBlock + 1, earcSad, 0, earcSadLen); + for (int i = 0; i < earcSadLen; i += 3) { + descriptor = new AudioDescriptor( + AudioDescriptor.STANDARD_EDID, + AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE, + Arrays.copyOfRange(earcSad, i, i + 3)); + audioDescriptors.add(descriptor); + } + break; + case TAGCODE_SADB_DATA_BLOCK: + //Include Tag code size + int earcSadbLen = length + 1; + byte[] earcSadb = new byte[earcSadbLen]; + System.arraycopy(rawCapabilities, firstByteOfBlock, earcSadb, 0, earcSadbLen); + descriptor = new AudioDescriptor( + AudioDescriptor.STANDARD_SADB, + AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE, + earcSadb); + audioDescriptors.add(descriptor); + break; + case TAGCODE_USE_EXTENDED_TAG: + if (rawCapabilities[firstByteOfBlock + 1] == EXTENDED_TAGCODE_VSADB) { + int earcVsadbLen = length + 1; //Include Tag code size + byte[] earcVsadb = new byte[earcVsadbLen]; + System.arraycopy(rawCapabilities, firstByteOfBlock, earcVsadb, 0, + earcVsadbLen); + descriptor = new AudioDescriptor( + AudioDescriptor.STANDARD_VSADB, + AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE, + earcVsadb); + audioDescriptors.add(descriptor); + } + break; + default: + Slog.w(TAG, "This tagcode was not handled: " + tagCode); + break; + } + firstByteOfBlock += (length + 1); + } + return audioDescriptors; + } + /** Dump internal status of HdmiEarcLocalDeviceTx object */ protected void dump(final IndentingPrintWriter pw) { synchronized (mLock) { diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java index bf78a659a26ba..bf44e0944bf07 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java @@ -16,11 +16,15 @@ package com.android.server.hdmi; +import static android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE; + import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_ARC_PENDING; import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_CONNECTED; import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_PENDING; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -29,6 +33,8 @@ import static org.mockito.Mockito.verify; import android.content.Context; import android.hardware.hdmi.HdmiDeviceInfo; +import android.media.AudioDescriptor; +import android.media.AudioDeviceAttributes; import android.media.AudioManager; import android.os.Looper; import android.os.test.TestLooper; @@ -41,12 +47,16 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.List; @SmallTest @Presubmit @@ -59,12 +69,19 @@ public class HdmiEarcLocalDeviceTxTest { private HdmiEarcLocalDevice mHdmiEarcLocalDeviceTx; private FakeNativeWrapper mNativeWrapper; private FakePowerManagerWrapper mPowerManager; + private byte[] mEarcCapabilities = new byte[]{ + 0x01, 0x01, 0x1a, 0x35, 0x0f, 0x7f, 0x07, 0x15, 0x07, 0x50, 0x3d, 0x1f, (byte) 0xc0, + 0x57, 0x06, 0x03, 0x67, 0x7e, 0x03, 0x5f, 0x7e, 0x03, 0x5f, 0x7e, 0x01, (byte) 0x83, + 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00}; private Looper mMyLooper; private TestLooper mTestLooper = new TestLooper(); @Mock private AudioManager mAudioManager; + @Captor + ArgumentCaptor mAudioAttributesCaptor; + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -119,15 +136,131 @@ public class HdmiEarcLocalDeviceTxTest { } @Test - public void earcGetsConnected_capsReportedInTime() { + public void earcGetsConnected_capsReportedInTime_sad() { mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED); mTestLooper.moveTimeForward(HdmiEarcLocalDeviceTx.REPORT_CAPS_MAX_DELAY_MS - 200); mTestLooper.dispatchAll(); - // TO DO: add meaningful capabilities and test that they get forwarded to AudioManager - // correctly. - mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new ArrayList<>()); + mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new byte[]{ + 0x01, 0x01, 0x1a, 0x35, 0x0f, 0x7f, 0x07, 0x15, 0x07, 0x50, 0x3d, 0x1f, (byte) 0xc0, + 0x57, 0x06, 0x03, 0x67, 0x7e, 0x03, 0x5f, 0x7e, 0x03, 0x5f, 0x7e, 0x01, 0x00, 0x5f, + 0x00, 0x00, 0x00, 0x00, 0x00 + }); mTestLooper.dispatchAll(); - verify(mAudioManager, times(1)).setWiredDeviceConnectionState(any(), eq(1)); + verify(mAudioManager, times(1)).setWiredDeviceConnectionState( + mAudioAttributesCaptor.capture(), eq(1)); + AudioDeviceAttributes attributes = mAudioAttributesCaptor.getValue(); + List descriptors = attributes.getAudioDescriptors(); + List expectedDescriptors = new ArrayList(Arrays.asList( + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {15, 127, 7}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {21, 7, 80}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {61, 31, -64}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {87, 6, 3}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {103, 126, 3}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {95, 126, 3}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {95, 126, 1}))); + assertThat(descriptors).isEqualTo(expectedDescriptors); + } + + @Test + public void earcGetsConnected_capsReportedInTime_sad_sadb() { + mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED); + mTestLooper.moveTimeForward(HdmiEarcLocalDeviceTx.REPORT_CAPS_MAX_DELAY_MS - 200); + mTestLooper.dispatchAll(); + mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new byte[]{ + 0x01, 0x01, 0x1a, 0x35, 0x0f, 0x7f, 0x07, 0x15, 0x07, 0x50, 0x3d, 0x1f, (byte) 0xc0, + 0x57, 0x06, 0x03, 0x67, 0x7e, 0x03, 0x5f, 0x7e, 0x03, 0x5f, 0x7e, 0x01, (byte) 0x83, + 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00}); + mTestLooper.dispatchAll(); + verify(mAudioManager, times(1)).setWiredDeviceConnectionState( + mAudioAttributesCaptor.capture(), eq(1)); + AudioDeviceAttributes attributes = mAudioAttributesCaptor.getValue(); + List descriptors = attributes.getAudioDescriptors(); + List expectedDescriptors = new ArrayList(Arrays.asList( + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {15, 127, 7}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {21, 7, 80}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {61, 31, -64}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {87, 6, 3}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {103, 126, 3}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {95, 126, 3}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {95, 126, 1}), + new AudioDescriptor(AudioDescriptor.STANDARD_SADB, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {-125, 95, 0, 0}))); + assertThat(descriptors).isEqualTo(expectedDescriptors); + } + + @Test + public void earcGetsConnected_capsReportedInTime_sad_sadb_vsadb() { + mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED); + mTestLooper.moveTimeForward(HdmiEarcLocalDeviceTx.REPORT_CAPS_MAX_DELAY_MS - 200); + mTestLooper.dispatchAll(); + mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new byte[]{ + 0x01, 0x01, 0x21, 0x35, 0x5F, 0x7E, 0x03, 0x5F, 0x7E, 0x01, 0x67, 0x7E, 0x03, 0x57, + 0x06, 0x03, 0x3D, 0x1E, (byte) 0xC0, 0x15, 0x07, 0x50, 0x0F, 0x7F, 0x07, + (byte) 0x83, 0x5F, 0x00, 0x00, (byte) 0xE6, 0x11, 0x46, (byte) 0xD0, 0x00, 0x70, + 0x00, 0x03, 0x01, (byte) 0x80, 0x00, (byte) 0x9D, (byte) 0xAD, (byte) 0x9E, 0x7B, + 0x08, (byte) 0xC1, (byte) 0xA8, 0x23, (byte) 0x9B, 0x49, 0x5C, (byte) 0xF5, 0x6B, + (byte) 0xAC, 0x22, (byte) 0xC2, (byte) 0x80, 0x48, 0x67, 0x7F, 0x59, 0x1C, 0x20, + 0x71, 0x35, 0x25, (byte) 0x9F, 0x43, 0x70, 0x1E, 0x32, 0x15, 0x60, (byte) 0xED, + (byte) 0xC8, 0x77, (byte) 0xA3, 0x24, 0x2E, (byte) 0xDA, (byte) 0x94, 0x6D, 0x35, + 0x34, 0x0F, 0x30, 0x62, 0x1A, 0x3B, (byte) 0xC9, 0x5A, (byte) 0xE6, (byte) 0xD8, + 0x22, 0x11, 0x56, (byte) 0xA6, (byte) 0x99, (byte) 0xCF, (byte) 0xE3, 0x1B, + (byte) 0x88, (byte) 0xA0, 0x2A, 0x5B, 0x6C, 0x5E, 0x53, 0x01, 0x47, 0x69, 0x51, + 0x61, (byte) 0xC7, (byte) 0xCB, 0x1B, 0x28, 0x14, 0x23, 0x10, (byte) 0xB1, 0x34, + 0x5E, 0x57, (byte) 0x97, (byte) 0xB3, 0x78, 0x03, 0x79, (byte) 0x8A, (byte) 0xFE, + 0x1E, (byte) 0xC8, (byte) 0xAB, 0x14, 0x74, 0x73, (byte) 0xFA, (byte) 0xBB, + (byte) 0xF7, 0x4E, 0x00, (byte) 0xFC, 0x5C, (byte) 0xDC, (byte) 0x8B, (byte) 0xC9, + 0x1E, 0x16, 0x35, (byte) 0xB1, (byte) 0x98, (byte) 0xEB, 0x2B, (byte) 0xE6, + (byte) 0xFC, (byte) 0xCC, 0x3C, 0x30, 0x19, 0x40, (byte) 0xC0, 0x50, (byte) 0xF2, + 0x58, 0x30, 0x4B, 0x0C, 0x7A, (byte) 0xE0, (byte) 0xFF, 0x7A, 0x64, 0x78, + (byte) 0xF8, 0x56, (byte) 0xF8, 0x6E, 0x72, 0x42, 0x49, 0x4E, (byte) 0xA6, + (byte) 0x95, (byte) 0xF5, 0x4C, 0x4F, (byte) 0xFF, 0x7F, 0x21, (byte) 0xA2, + (byte) 0x98, 0x33, (byte) 0x90, (byte) 0xFD, 0x17, 0x08, 0x13, (byte) 0xB2, 0x00, + (byte) 0xA9, (byte) 0xB5, (byte) 0xBD, (byte) 0xB5, (byte) 0xC1, (byte) 0xC7, 0x45, + (byte) 0xD9, (byte) 0xDC, (byte) 0x8B, 0x58, (byte) 0xB3, 0x5D, 0x5E, 0x72, + (byte) 0xE6, (byte) 0x8D, (byte) 0xDD, 0x0B, 0x21, (byte) 0xF3, (byte) 0x9A, + (byte) 0x8E, 0x1B, 0x79, 0x59, (byte) 0xE1, 0x3F, (byte) 0xAC, 0x24, (byte) 0xA0, + (byte) 0xC8, 0x56, (byte) 0xFD, (byte) 0x85, (byte) 0x8F, 0x6A, (byte) 0x80, 0x41, + (byte) 0xA8, 0x5D, 0x2C, (byte) 0xC2, 0x69, (byte) 0xA1, 0x0D, (byte) 0x82, 0x04, + 0x5D, (byte) 0xCA, (byte) 0xB4, (byte) 0x9F, 0x3A, 0x2D, (byte) 0xBF, 0x24}); + mTestLooper.dispatchAll(); + verify(mAudioManager, times(1)).setWiredDeviceConnectionState( + mAudioAttributesCaptor.capture(), eq(1)); + AudioDeviceAttributes attributes = mAudioAttributesCaptor.getValue(); + List descriptors = attributes.getAudioDescriptors(); + List expectedDescriptors = new ArrayList(Arrays.asList( + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {95, 126, 3}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {95, 126, 1}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {103, 126, 3}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {87, 6, 3}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {61, 30, -64}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {21, 7, 80}), + new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {15, 127, 7}), + new AudioDescriptor(AudioDescriptor.STANDARD_SADB, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {-125, 95, 0, 0}), + new AudioDescriptor(AudioDescriptor.STANDARD_VSADB, AUDIO_ENCAPSULATION_TYPE_NONE, + new byte[] {-26, 17, 70, -48, 0, 112, 0}))); + assertThat(descriptors).isEqualTo(expectedDescriptors); } @Test @@ -135,11 +268,14 @@ public class HdmiEarcLocalDeviceTxTest { mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED); mTestLooper.moveTimeForward(HdmiEarcLocalDeviceTx.REPORT_CAPS_MAX_DELAY_MS + 1); mTestLooper.dispatchAll(); - // TO DO: verify that empty capabilities are forwarded to AudioManager. - verify(mAudioManager, times(1)).setWiredDeviceConnectionState(any(), eq(1)); + verify(mAudioManager, times(1)).setWiredDeviceConnectionState( + mAudioAttributesCaptor.capture(), eq(1)); + AudioDeviceAttributes attributes = mAudioAttributesCaptor.getValue(); + List descriptors = attributes.getAudioDescriptors(); + assertThat(descriptors).hasSize(0); Mockito.clearInvocations(mAudioManager); - mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new ArrayList<>()); + mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(mEarcCapabilities); mTestLooper.dispatchAll(); verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), anyInt()); } @@ -151,10 +287,14 @@ public class HdmiEarcLocalDeviceTxTest { mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_ARC_PENDING); mTestLooper.dispatchAll(); verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), eq(1)); - verify(mAudioManager, times(1)).setWiredDeviceConnectionState(any(), eq(0)); + verify(mAudioManager, times(1)).setWiredDeviceConnectionState( + mAudioAttributesCaptor.capture(), eq(0)); + AudioDeviceAttributes attributes = mAudioAttributesCaptor.getValue(); + List descriptors = attributes.getAudioDescriptors(); + assertThat(descriptors).hasSize(0); Mockito.clearInvocations(mAudioManager); - mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new ArrayList<>()); + mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(mEarcCapabilities); mTestLooper.dispatchAll(); verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), anyInt()); } @@ -168,7 +308,7 @@ public class HdmiEarcLocalDeviceTxTest { verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), anyInt()); Mockito.clearInvocations(mAudioManager); - mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new ArrayList<>()); + mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(mEarcCapabilities); mTestLooper.dispatchAll(); verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), anyInt()); }