From ccc138adba9de2d3da7f1200e7ed4e4188e0185c Mon Sep 17 00:00:00 2001 From: Nathalie Le Clair Date: Fri, 4 Nov 2022 16:40:55 +0100 Subject: [PATCH] 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()); + } +}