diff --git a/core/api/current.txt b/core/api/current.txt index d589a27e4e379..5bb6652aa9e93 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -20429,6 +20429,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/api/system-current.txt b/core/api/system-current.txt index 1eed2d4c0d6de..081560a5e95ee 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -4531,12 +4531,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/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/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/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/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 f3c67fb9bbfbe..2e73a413e43d1 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"; @@ -412,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. * @@ -1066,6 +1095,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 +1104,8 @@ final class HdmiCecController { portInfo.physicalAddress, portInfo.cecSupported, false, - portInfo.arcSupported); + portInfo.arcSupported, + false); i++; } return hdmiPortInfo; @@ -1234,7 +1266,8 @@ final class HdmiCecController { portInfo.physicalAddress, portInfo.cecSupported, false, - portInfo.arcSupported); + portInfo.arcSupported, + false); i++; } return hdmiPortInfo; @@ -1415,7 +1448,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/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java index b4d7fb9d91532..91f58db407f21 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; @@ -67,8 +67,6 @@ abstract class HdmiCecLocalDevice { // 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 { 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 { 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/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); } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 2f15e579b2149..85bd16b2cb2f7 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; @@ -126,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"; @@ -184,13 +187,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 +388,18 @@ public class HdmiControlService extends SystemService { @HdmiControlManager.HdmiCecControl private int mHdmiControlEnabled; + // 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; + + // Set to true while the eARC feature is enabled. + @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". @@ -417,6 +433,12 @@ public class HdmiControlService extends SystemService { private HdmiCecPowerStatusController mPowerStatusController; + @Nullable + private HdmiEarcController mEarcController; + + @Nullable + private HdmiEarcLocalDevice mEarcLocalDevice; + @ServiceThreadOnly private String mMenuLanguage = localeToMenuLanguage(Locale.getDefault()); @@ -631,9 +653,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); @@ -646,7 +672,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); @@ -654,15 +679,56 @@ 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 (mHdmiControlEnabled == HdmiControlManager.HDMI_CEC_CONTROL_ENABLED) { + if (isCecControlEnabled()) { initializeCec(INITIATED_BY_BOOT_UP); } else { mCecController.enableCec(false); } - mMhlDevices = Collections.emptyList(); + + synchronized (mLock) { + mMhlDevices = Collections.emptyList(); + } mHdmiCecNetwork.initPortInfo(); + List ports = getPortInfo(); + synchronized (mLock) { + mEarcSupported = false; + for (HdmiPortInfo port : 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 &= (mEarcController != null); + } + if (isEarcSupported()) { + if (isEarcEnabled()) { + initializeEarc(INITIATED_BY_BOOT_UP); + } else { + setEarcEnabledInHal(false); + } + } + mHdmiCecConfig.registerChangeListener(HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED, new HdmiCecConfig.SettingChangeListener() { @Override @@ -743,8 +809,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 +1157,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<>(); @@ -1245,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(); } @@ -2568,7 +2642,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 +2653,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 +2681,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 +3397,18 @@ public class HdmiControlService extends SystemService { } } + private boolean isEarcEnabled() { + synchronized (mLock) { + return mEarcEnabled; + } + } + + private boolean isEarcSupported() { + synchronized (mLock) { + return mEarcSupported; + } + } + @ServiceThreadOnly int getPowerStatus() { assertRunOnServiceThread(); @@ -3384,7 +3480,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 +3502,25 @@ public class HdmiControlService extends SystemService { } else { Slog.i(TAG, "Device does not support HDMI-CEC."); } + 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); + } + } // TODO: Initialize MHL local devices. } @@ -4296,4 +4411,133 @@ 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); + setEarcEnabledInHal(true); + initializeEarcLocalDevice(initiatedBy); + } + + @ServiceThreadOnly + @VisibleForTesting + protected void initializeEarcLocalDevice(final int initiatedBy) { + // TODO remove initiatedBy argument if it stays unused + assertRunOnServiceThread(); + if (mEarcLocalDevice == null) { + mEarcLocalDevice = HdmiEarcLocalDevice.create(this, HdmiDeviceInfo.DEVICE_TV); + } + // 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 (!isEarcSupported()) { + 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() { + disableEarcLocalDevice(); + setEarcEnabledInHal(false); + clearEarcLocalDevice(); + } + + @ServiceThreadOnly + @VisibleForTesting + protected void clearEarcLocalDevice() { + assertRunOnServiceThread(); + mEarcLocalDevice = null; + } + + @ServiceThreadOnly + @VisibleForTesting + protected void addEarcLocalDevice(HdmiEarcLocalDevice localDevice) { + assertRunOnServiceThread(); + mEarcLocalDevice = localDevice; + } + + @ServiceThreadOnly + @VisibleForTesting + HdmiEarcLocalDevice getEarcLocalDevice() { + assertRunOnServiceThread(); + return mEarcLocalDevice; + } + + private void disableEarcLocalDevice() { + if (mEarcLocalDevice == null) { + return; + } + mEarcLocalDevice.disableDevice(); + } + + @ServiceThreadOnly + @VisibleForTesting + 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); + } + } + + @ServiceThreadOnly + void handleEarcCapabilitiesReported(byte[] rawCapabilities, 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(rawCapabilities); + } + } } 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..85225097ba5a6 --- /dev/null +++ b/services/core/java/com/android/server/hdmi/HdmiEarcController.java @@ -0,0 +1,117 @@ +/* + * 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. + } + + /** + * 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; + } + + /** + * 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(byte[] rawCapabilities, int portId) { + runOnServiceThread( + () -> mService.handleEarcCapabilitiesReported(rawCapabilities, 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 new file mode 100644 index 0000000000000..e95ecd2bb6cdd --- /dev/null +++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.hdmi; + +import 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. + */ +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 abstract void handleEarcCapabilitiesReported(byte[] rawCapabilities); + + 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/HdmiEarcLocalDeviceTx.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java new file mode 100644 index 0000000000000..c7c7702b871a1 --- /dev/null +++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java @@ -0,0 +1,210 @@ +/* + * 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.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.os.Handler; +import android.util.IndentingPrintWriter; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 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"; + + // 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; + 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) { + synchronized (mLock) { + HdmiLogger.debug(TAG, "eARC state change [old:%b new %b]", mEarcStatus, + status); + mEarcStatus = status; + } + + mReportCapsHandler.removeCallbacksAndMessages(null); + if (status == HDMI_EARC_STATUS_IDLE) { + notifyEarcStatusToAudioService(false, new ArrayList<>()); + } else if (status == HDMI_EARC_STATUS_ARC_PENDING) { + notifyEarcStatusToAudioService(false, new ArrayList<>()); + } else if (status == HDMI_EARC_STATUS_EARC_CONNECTED) { + mReportCapsHandler.postDelayed(mReportCapsRunnable, REPORT_CAPS_MAX_DELAY_MS); + } + } + + protected void handleEarcCapabilitiesReported(byte[] rawCapabilities) { + synchronized (mLock) { + if (mEarcStatus == HDMI_EARC_STATUS_EARC_CONNECTED + && mReportCapsHandler.hasCallbacks(mReportCapsRunnable)) { + mReportCapsHandler.removeCallbacksAndMessages(null); + List audioDescriptors = parseCapabilities(rawCapabilities); + notifyEarcStatusToAudioService(true, audioDescriptors); + } + } + } + + private void notifyEarcStatusToAudioService( + boolean enabled, List audioDescriptors) { + AudioDeviceAttributes attributes = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI_EARC, "", "", + new ArrayList(), audioDescriptors); + 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<>()); + } + } + } + } + + 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) { + 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 new file mode 100644 index 0000000000000..f661e40bb3f16 --- /dev/null +++ b/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java @@ -0,0 +1,37 @@ +/* + * 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"; + + protected final HdmiControlService mService; + protected final int mDeviceType; + + 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/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..2b555a07a50fd 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,8 @@ 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; import static org.mockito.Mockito.doReturn; @@ -127,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); @@ -1082,6 +1085,174 @@ public class HdmiControlServiceTest { assertThat(mHdmiControlServiceSpy.audioSystem()).isNull(); } + @Test + public void disableEarc_clearEarcLocalDevice() { + mHdmiControlServiceSpy.setEarcSupported(true); + mHdmiControlServiceSpy.clearEarcLocalDevice(); + mHdmiControlServiceSpy.addEarcLocalDevice( + new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy)); + 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(mHdmiControlServiceSpy)); + 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()); + } + + @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; 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..bf44e0944bf07 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.hdmi; + +import static android.media.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; +import static org.mockito.Mockito.times; +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; +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.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 +@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 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); + + 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_sad() { + 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, 0x00, 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}))); + 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 + public void earcGetsConnected_capsReportedTooLate() { + mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED); + mTestLooper.moveTimeForward(HdmiEarcLocalDeviceTx.REPORT_CAPS_MAX_DELAY_MS + 1); + mTestLooper.dispatchAll(); + 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(mEarcCapabilities); + 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( + mAudioAttributesCaptor.capture(), eq(0)); + AudioDeviceAttributes attributes = mAudioAttributesCaptor.getValue(); + List descriptors = attributes.getAudioDescriptors(); + assertThat(descriptors).hasSize(0); + Mockito.clearInvocations(mAudioManager); + + mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(mEarcCapabilities); + 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(mEarcCapabilities); + mTestLooper.dispatchAll(); + verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), anyInt()); + } +}