From 23181eb8a6d657108bcde84f0e29dbfe5be760de Mon Sep 17 00:00:00 2001 From: Yan Han Date: Wed, 15 Dec 2021 10:02:16 +0100 Subject: [PATCH] Add CEC feature tracking to HdmiDeviceInfo HdmiDeviceInfo now contains an object for tracking features whose support is communicated in the [Device Features] operand. Bug: 204854610 Test: atest DeviceFeaturesTest Change-Id: Id2e19fb2e4480b5ed88529fd15012c7abad5d3a7 --- .../android/hardware/hdmi/DeviceFeatures.java | 358 ++++++++++++++++++ .../android/hardware/hdmi/HdmiDeviceInfo.java | 33 ++ .../hardware/hdmi/DeviceFeaturesTest.java | 77 ++++ 3 files changed, 468 insertions(+) create mode 100644 core/java/android/hardware/hdmi/DeviceFeatures.java create mode 100644 core/tests/hdmitests/src/android/hardware/hdmi/DeviceFeaturesTest.java diff --git a/core/java/android/hardware/hdmi/DeviceFeatures.java b/core/java/android/hardware/hdmi/DeviceFeatures.java new file mode 100644 index 0000000000000..fb42e62effa06 --- /dev/null +++ b/core/java/android/hardware/hdmi/DeviceFeatures.java @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.hdmi; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; + +/** + * Immutable class that stores support status for features in the [Device Features] operand. + * Each feature may be supported, be not supported, or have an unknown support status. + * + * @hide + */ +public class DeviceFeatures { + + @IntDef({ + FEATURE_NOT_SUPPORTED, + FEATURE_SUPPORTED, + FEATURE_SUPPORT_UNKNOWN + }) + public @interface FeatureSupportStatus {}; + + public static final int FEATURE_NOT_SUPPORTED = 0; + public static final int FEATURE_SUPPORTED = 1; + public static final int FEATURE_SUPPORT_UNKNOWN = 2; + + /** + * Instance representing no knowledge of any feature's support. + */ + @NonNull + public static final DeviceFeatures ALL_FEATURES_SUPPORT_UNKNOWN = + new Builder(FEATURE_SUPPORT_UNKNOWN).build(); + + /** + * Instance representing no support for any feature. + */ + @NonNull + public static final DeviceFeatures NO_FEATURES_SUPPORTED = + new Builder(FEATURE_NOT_SUPPORTED).build(); + + @FeatureSupportStatus private final int mRecordTvScreenSupport; + @FeatureSupportStatus private final int mSetOsdStringSupport; + @FeatureSupportStatus private final int mDeckControlSupport; + @FeatureSupportStatus private final int mSetAudioRateSupport; + @FeatureSupportStatus private final int mArcTxSupport; + @FeatureSupportStatus private final int mArcRxSupport; + @FeatureSupportStatus private final int mSetAudioVolumeLevelSupport; + + private DeviceFeatures(@NonNull Builder builder) { + this.mRecordTvScreenSupport = builder.mRecordTvScreenSupport; + this.mSetOsdStringSupport = builder.mOsdStringSupport; + this.mDeckControlSupport = builder.mDeckControlSupport; + this.mSetAudioRateSupport = builder.mSetAudioRateSupport; + this.mArcTxSupport = builder.mArcTxSupport; + this.mArcRxSupport = builder.mArcRxSupport; + this.mSetAudioVolumeLevelSupport = builder.mSetAudioVolumeLevelSupport; + } + + /** + * Converts an instance to a builder. + */ + public Builder toBuilder() { + return new Builder(this); + } + + /** + * Constructs an instance from a [Device Features] operand. + * + * Bit 7 of [Device Features] is currently ignored. It indicates whether the operand spans more + * than one byte, but only the first byte contains information as of CEC 2.0. + * + * @param deviceFeaturesOperand The [Device Features] operand to parse. + * @return Instance representing the [Device Features] operand. + */ + @NonNull + public static DeviceFeatures fromOperand(@NonNull byte[] deviceFeaturesOperand) { + Builder builder = new Builder(FEATURE_SUPPORT_UNKNOWN); + + // Read feature support status from the bits of [Device Features] + if (deviceFeaturesOperand.length >= 1) { + byte b = deviceFeaturesOperand[0]; + builder + .setRecordTvScreenSupport(bitToFeatureSupportStatus(((b >> 6) & 1) == 1)) + .setSetOsdStringSupport(bitToFeatureSupportStatus(((b >> 5) & 1) == 1)) + .setDeckControlSupport(bitToFeatureSupportStatus(((b >> 4) & 1) == 1)) + .setSetAudioRateSupport(bitToFeatureSupportStatus(((b >> 3) & 1) == 1)) + .setArcTxSupport(bitToFeatureSupportStatus(((b >> 2) & 1) == 1)) + .setArcRxSupport(bitToFeatureSupportStatus(((b >> 1) & 1) == 1)) + .setSetAudioVolumeLevelSupport(bitToFeatureSupportStatus((b & 1) == 1)); + } + return builder.build(); + } + + /** + * Returns the [Device Features] operand corresponding to this instance. + * {@link #FEATURE_SUPPORT_UNKNOWN} maps to 0, indicating no support. + * + * As of CEC 2.0, the returned byte array will always be of length 1. + */ + @NonNull + public byte[] toOperand() { + byte result = 0; + + if (mRecordTvScreenSupport == FEATURE_SUPPORTED) result |= (byte) (1 << 6); + if (mSetOsdStringSupport == FEATURE_SUPPORTED) result = (byte) (1 << 5); + if (mDeckControlSupport == FEATURE_SUPPORTED) result = (byte) (1 << 4); + if (mSetAudioRateSupport == FEATURE_SUPPORTED) result = (byte) (1 << 3); + if (mArcTxSupport == FEATURE_SUPPORTED) result = (byte) (1 << 2); + if (mArcRxSupport == FEATURE_SUPPORTED) result = (byte) (1 << 1); + if (mSetAudioVolumeLevelSupport == FEATURE_SUPPORTED) result = (byte) 1; + + return new byte[]{ result }; + } + + @FeatureSupportStatus + private static int bitToFeatureSupportStatus(boolean bit) { + return bit ? FEATURE_SUPPORTED : FEATURE_NOT_SUPPORTED; + } + + /** + * Returns whether the device is a TV that supports . + */ + @FeatureSupportStatus + public int getRecordTvScreenSupport() { + return mRecordTvScreenSupport; + } + + /** + * Returns whether the device is a TV that supports . + */ + @FeatureSupportStatus + public int getSetOsdStringSupport() { + return mSetOsdStringSupport; + } + + /** + * Returns whether the device supports being controlled by Deck Control. + */ + @FeatureSupportStatus + public int getDeckControlSupport() { + return mDeckControlSupport; + } + + /** + * Returns whether the device is a Source that supports . + */ + @FeatureSupportStatus + public int getSetAudioRateSupport() { + return mSetAudioRateSupport; + } + + /** + * Returns whether the device is a Sink that supports ARC Tx. + */ + @FeatureSupportStatus + public int getArcTxSupport() { + return mArcTxSupport; + } + + /** + * Returns whether the device is a Source that supports ARC Rx. + */ + @FeatureSupportStatus + public int getArcRxSupport() { + return mArcRxSupport; + } + + /** + * Returns whether the device supports . + */ + @FeatureSupportStatus + public int getSetAudioVolumeLevelSupport() { + return mSetAudioVolumeLevelSupport; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof DeviceFeatures)) { + return false; + } + + DeviceFeatures other = (DeviceFeatures) obj; + return mRecordTvScreenSupport == other.mRecordTvScreenSupport + && mSetOsdStringSupport == other.mSetOsdStringSupport + && mDeckControlSupport == other.mDeckControlSupport + && mSetAudioRateSupport == other.mSetAudioRateSupport + && mArcTxSupport == other.mArcTxSupport + && mArcRxSupport == other.mArcRxSupport + && mSetAudioVolumeLevelSupport == other.mSetAudioVolumeLevelSupport; + } + + @Override + public int hashCode() { + return java.util.Objects.hash( + mRecordTvScreenSupport, + mSetOsdStringSupport, + mDeckControlSupport, + mSetAudioRateSupport, + mArcTxSupport, + mArcRxSupport, + mSetAudioVolumeLevelSupport + ); + } + + @NonNull + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + s.append("Device features: "); + s.append("record_tv_screen: ") + .append(featureSupportStatusToString(mRecordTvScreenSupport)).append(" "); + s.append("set_osd_string: ") + .append(featureSupportStatusToString(mSetOsdStringSupport)).append(" "); + s.append("deck_control: ") + .append(featureSupportStatusToString(mDeckControlSupport)).append(" "); + s.append("set_audio_rate: ") + .append(featureSupportStatusToString(mSetAudioRateSupport)).append(" "); + s.append("arc_tx: ") + .append(featureSupportStatusToString(mArcTxSupport)).append(" "); + s.append("arc_rx: ") + .append(featureSupportStatusToString(mArcRxSupport)).append(" "); + s.append("set_audio_volume_level: ") + .append(featureSupportStatusToString(mSetAudioVolumeLevelSupport)).append(" "); + return s.toString(); + } + + @NonNull + private static String featureSupportStatusToString(@FeatureSupportStatus int status) { + switch (status) { + case FEATURE_SUPPORTED: + return "Y"; + case FEATURE_NOT_SUPPORTED: + return "N"; + case FEATURE_SUPPORT_UNKNOWN: + default: + return "?"; + } + } + + /** + * Builder for {@link DeviceFeatures} instances. + */ + public static final class Builder { + @FeatureSupportStatus private int mRecordTvScreenSupport; + @FeatureSupportStatus private int mOsdStringSupport; + @FeatureSupportStatus private int mDeckControlSupport; + @FeatureSupportStatus private int mSetAudioRateSupport; + @FeatureSupportStatus private int mArcTxSupport; + @FeatureSupportStatus private int mArcRxSupport; + @FeatureSupportStatus private int mSetAudioVolumeLevelSupport; + + private Builder(@FeatureSupportStatus int defaultFeatureSupportStatus) { + mRecordTvScreenSupport = defaultFeatureSupportStatus; + mOsdStringSupport = defaultFeatureSupportStatus; + mDeckControlSupport = defaultFeatureSupportStatus; + mSetAudioRateSupport = defaultFeatureSupportStatus; + mArcTxSupport = defaultFeatureSupportStatus; + mArcRxSupport = defaultFeatureSupportStatus; + mSetAudioVolumeLevelSupport = defaultFeatureSupportStatus; + } + + private Builder(DeviceFeatures info) { + mRecordTvScreenSupport = info.getRecordTvScreenSupport(); + mOsdStringSupport = info.getSetOsdStringSupport(); + mDeckControlSupport = info.getDeckControlSupport(); + mSetAudioRateSupport = info.getSetAudioRateSupport(); + mArcTxSupport = info.getArcTxSupport(); + mArcRxSupport = info.getArcRxSupport(); + mSetAudioVolumeLevelSupport = info.getSetAudioVolumeLevelSupport(); + } + + /** + * Creates a new {@link DeviceFeatures} object. + */ + public DeviceFeatures build() { + return new DeviceFeatures(this); + } + + /** + * Sets the value for {@link #getRecordTvScreenSupport()}. + */ + @NonNull + public Builder setRecordTvScreenSupport(@FeatureSupportStatus int recordTvScreenSupport) { + mRecordTvScreenSupport = recordTvScreenSupport; + return this; + } + + /** + * Sets the value for {@link #getSetOsdStringSupport()}. + */ + @NonNull + public Builder setSetOsdStringSupport(@FeatureSupportStatus int setOsdStringSupport) { + mOsdStringSupport = setOsdStringSupport; + return this; + } + + /** + * Sets the value for {@link #getDeckControlSupport()}. + */ + @NonNull + public Builder setDeckControlSupport(@FeatureSupportStatus int deckControlSupport) { + mDeckControlSupport = deckControlSupport; + return this; + } + + /** + * Sets the value for {@link #getSetAudioRateSupport()}. + */ + @NonNull + public Builder setSetAudioRateSupport(@FeatureSupportStatus int setAudioRateSupport) { + mSetAudioRateSupport = setAudioRateSupport; + return this; + } + + /** + * Sets the value for {@link #getArcTxSupport()}. + */ + @NonNull + public Builder setArcTxSupport(@FeatureSupportStatus int arcTxSupport) { + mArcTxSupport = arcTxSupport; + return this; + } + + /** + * Sets the value for {@link #getArcRxSupport()}. + */ + @NonNull + public Builder setArcRxSupport(@FeatureSupportStatus int arcRxSupport) { + mArcRxSupport = arcRxSupport; + return this; + } + + /** + * Sets the value for {@link #getSetAudioRateSupport()}. + */ + @NonNull + public Builder setSetAudioVolumeLevelSupport( + @FeatureSupportStatus int setAudioVolumeLevelSupport) { + mSetAudioVolumeLevelSupport = setAudioVolumeLevelSupport; + return this; + } + } +} diff --git a/core/java/android/hardware/hdmi/HdmiDeviceInfo.java b/core/java/android/hardware/hdmi/HdmiDeviceInfo.java index 96255bcbdff75..ca90661d56aa9 100644 --- a/core/java/android/hardware/hdmi/HdmiDeviceInfo.java +++ b/core/java/android/hardware/hdmi/HdmiDeviceInfo.java @@ -124,6 +124,7 @@ public class HdmiDeviceInfo implements Parcelable { private final int mVendorId; private final String mDisplayName; private final int mDevicePowerStatus; + private final DeviceFeatures mDeviceFeatures; // MHL only parameters. private final int mDeviceId; @@ -198,6 +199,7 @@ public class HdmiDeviceInfo implements Parcelable { mDevicePowerStatus = HdmiControlManager.POWER_STATUS_UNKNOWN; mDisplayName = "Inactive"; mVendorId = 0; + mDeviceFeatures = DeviceFeatures.ALL_FEATURES_SUPPORT_UNKNOWN; mDeviceId = -1; mAdopterId = -1; @@ -222,6 +224,7 @@ public class HdmiDeviceInfo implements Parcelable { this.mVendorId = builder.mVendorId; this.mDisplayName = builder.mDisplayName; this.mDevicePowerStatus = builder.mDevicePowerStatus; + this.mDeviceFeatures = builder.mDeviceFeatures; this.mDeviceId = builder.mDeviceId; this.mAdopterId = builder.mAdopterId; @@ -294,6 +297,15 @@ public class HdmiDeviceInfo implements Parcelable { return mId; } + /** + * Returns the CEC features that this device supports. + * + * @hide + */ + public DeviceFeatures getDeviceFeatures() { + return mDeviceFeatures; + } + /** * Returns the id to be used for CEC device. * @@ -521,6 +533,11 @@ public class HdmiDeviceInfo implements Parcelable { s.append("physical_address: ").append(String.format("0x%04X", mPhysicalAddress)); s.append(" "); s.append("port_id: ").append(mPortId); + + if (mHdmiDeviceType == HDMI_DEVICE_TYPE_CEC) { + s.append("\n ").append(mDeviceFeatures.toString()); + } + return s.toString(); } @@ -581,6 +598,7 @@ public class HdmiDeviceInfo implements Parcelable { private int mVendorId = VENDOR_ID_UNKNOWN; private String mDisplayName = ""; private int mDevicePowerStatus = HdmiControlManager.POWER_STATUS_UNKNOWN; + private DeviceFeatures mDeviceFeatures; // MHL parameters private int mDeviceId = -1; @@ -588,6 +606,11 @@ public class HdmiDeviceInfo implements Parcelable { private Builder(int hdmiDeviceType) { mHdmiDeviceType = hdmiDeviceType; + if (hdmiDeviceType == HDMI_DEVICE_TYPE_CEC) { + mDeviceFeatures = DeviceFeatures.ALL_FEATURES_SUPPORT_UNKNOWN; + } else { + mDeviceFeatures = DeviceFeatures.NO_FEATURES_SUPPORTED; + } } private Builder(@NonNull HdmiDeviceInfo hdmiDeviceInfo) { @@ -602,6 +625,7 @@ public class HdmiDeviceInfo implements Parcelable { mDevicePowerStatus = hdmiDeviceInfo.mDevicePowerStatus; mDeviceId = hdmiDeviceInfo.mDeviceId; mAdopterId = hdmiDeviceInfo.mAdopterId; + mDeviceFeatures = hdmiDeviceInfo.mDeviceFeatures; } /** @@ -684,6 +708,15 @@ public class HdmiDeviceInfo implements Parcelable { return this; } + /** + * Sets the value for {@link #getDeviceFeatures()}. + */ + @NonNull + public Builder setDeviceFeatures(DeviceFeatures deviceFeatures) { + this.mDeviceFeatures = deviceFeatures; + return this; + } + /** * Sets the value for {@link #getDeviceId()}. */ diff --git a/core/tests/hdmitests/src/android/hardware/hdmi/DeviceFeaturesTest.java b/core/tests/hdmitests/src/android/hardware/hdmi/DeviceFeaturesTest.java new file mode 100644 index 0000000000000..772393c5c4522 --- /dev/null +++ b/core/tests/hdmitests/src/android/hardware/hdmi/DeviceFeaturesTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 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 android.hardware.hdmi; + +import static android.hardware.hdmi.DeviceFeatures.FEATURE_NOT_SUPPORTED; +import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORTED; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.filters.SmallTest; + +import com.google.common.testing.EqualsTester; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link DeviceFeatures} */ +@RunWith(JUnit4.class) +@SmallTest +public class DeviceFeaturesTest { + + @Test + public void testEquals() { + new EqualsTester() + .addEqualityGroup(DeviceFeatures.ALL_FEATURES_SUPPORT_UNKNOWN) + .addEqualityGroup(DeviceFeatures.NO_FEATURES_SUPPORTED) + .addEqualityGroup( + DeviceFeatures.fromOperand( + new byte[]{(byte) 0b0111_0000}), + DeviceFeatures.fromOperand( + new byte[]{(byte) 0b1111_0000}), + DeviceFeatures.fromOperand( + new byte[]{(byte) 0b1111_0000, (byte) 0b0101_0101}), + DeviceFeatures.ALL_FEATURES_SUPPORT_UNKNOWN.toBuilder() + .setRecordTvScreenSupport(FEATURE_SUPPORTED) + .setSetOsdStringSupport(FEATURE_SUPPORTED) + .setDeckControlSupport(FEATURE_SUPPORTED) + .setSetAudioRateSupport(FEATURE_NOT_SUPPORTED) + .setArcTxSupport(FEATURE_NOT_SUPPORTED) + .setArcRxSupport(FEATURE_NOT_SUPPORTED) + .setSetAudioVolumeLevelSupport(FEATURE_NOT_SUPPORTED) + .build() + ) + .testEquals(); + } + + @Test + public void testDeviceFeaturesOperandConversion() { + DeviceFeatures info = DeviceFeatures.fromOperand( + new byte[]{(byte) 0b0111_0000}); + + assertThat(info.getRecordTvScreenSupport()).isEqualTo(FEATURE_SUPPORTED); + assertThat(info.getSetOsdStringSupport()).isEqualTo(FEATURE_SUPPORTED); + assertThat(info.getDeckControlSupport()).isEqualTo(FEATURE_SUPPORTED); + assertThat(info.getSetAudioRateSupport()).isEqualTo(FEATURE_NOT_SUPPORTED); + assertThat(info.getArcTxSupport()).isEqualTo(FEATURE_NOT_SUPPORTED); + assertThat(info.getArcRxSupport()).isEqualTo(FEATURE_NOT_SUPPORTED); + assertThat(info.getSetAudioVolumeLevelSupport()).isEqualTo(FEATURE_NOT_SUPPORTED); + + assertThat(info.toOperand()).isEqualTo(new byte[]{(byte) 0b0111_0000}); + } +}