From c7737bee3c08db5b88c11cb0b523302fed0b5d8e Mon Sep 17 00:00:00 2001 From: Marvin Ramin Date: Fri, 26 Feb 2021 09:15:23 +0100 Subject: [PATCH] Add retry to SystemAudioAutoInitiationAction In cases where audio devices don't respond to the system audio status request in time this can bring the system audio status of the TV and audio device out of sync. Add a retry to attempt gettingthe system audio mode status of the audio device again. Bug: 156579158 Bug: 176920796 Test: atest SystemAudioAutoInitiationAction Change-Id: Ifedf1b71aecb02897ac4b06f21cd0b2cffb6f8c1 --- .../hdmi/SystemAudioAutoInitiationAction.java | 17 + .../SystemAudioAutoInitiationActionTest.java | 343 ++++++++++++++++++ 2 files changed, 360 insertions(+) create mode 100644 services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java diff --git a/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java b/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java index f7e871d0b6452..56e538b1abfaa 100644 --- a/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java +++ b/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java @@ -17,6 +17,8 @@ package com.android.server.hdmi; import android.hardware.tv.cec.V1_0.SendMessageResult; + +import com.android.internal.annotations.VisibleForTesting; import com.android.server.hdmi.HdmiControlService.SendMessageCallback; /** @@ -30,6 +32,14 @@ final class SystemAudioAutoInitiationAction extends HdmiCecFeatureAction { // to AV Receiver. private static final int STATE_WAITING_FOR_SYSTEM_AUDIO_MODE_STATUS = 1; + @VisibleForTesting + static final int RETRIES_ON_TIMEOUT = 1; + + // On some audio devices the message can be delayed as the device + // is just waking up. Retry the message to ensure we properly + // initialize system audio. + private int mRetriesOnTimeOut = RETRIES_ON_TIMEOUT; + SystemAudioAutoInitiationAction(HdmiCecLocalDevice source, int avrAddress) { super(source); mAvrAddress = avrAddress; @@ -100,6 +110,13 @@ final class SystemAudioAutoInitiationAction extends HdmiCecFeatureAction { switch (mState) { case STATE_WAITING_FOR_SYSTEM_AUDIO_MODE_STATUS: + if (mRetriesOnTimeOut > 0) { + mRetriesOnTimeOut--; + addTimer(mState, HdmiConfig.TIMEOUT_MS); + sendGiveSystemAudioModeStatus(); + return; + } + handleSystemAudioModeStatusTimeout(); break; } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java new file mode 100644 index 0000000000000..865eb7a3b56dd --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.hdmi; + + +import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; +import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; +import static com.android.server.hdmi.SystemAudioAutoInitiationAction.RETRIES_ON_TIMEOUT; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.ContextWrapper; +import android.hardware.hdmi.HdmiPortInfo; +import android.media.AudioManager; +import android.os.Handler; +import android.os.IPowerManager; +import android.os.IThermalService; +import android.os.Looper; +import android.os.PowerManager; +import android.os.test.TestLooper; + +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.MockitoAnnotations; + +import java.util.ArrayList; + +/** + * Test for {@link SystemAudioAutoInitiationAction}. + */ +@SmallTest +@RunWith(JUnit4.class) +public class SystemAudioAutoInitiationActionTest { + + private Context mContextSpy; + private HdmiControlService mHdmiControlService; + private FakeNativeWrapper mNativeWrapper; + + private HdmiCecLocalDeviceTv mHdmiCecLocalDeviceTv; + + private TestLooper mTestLooper = new TestLooper(); + private ArrayList mLocalDevices = new ArrayList<>(); + private int mPhysicalAddress; + + @Mock + private IPowerManager mIPowerManagerMock; + @Mock + private IThermalService mIThermalServiceMock; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); + + Looper myLooper = mTestLooper.getLooper(); + PowerManager powerManager = new PowerManager(mContextSpy, mIPowerManagerMock, + mIThermalServiceMock, new Handler(myLooper)); + when(mContextSpy.getSystemService(Context.POWER_SERVICE)).thenReturn(powerManager); + when(mContextSpy.getSystemService(PowerManager.class)).thenReturn(powerManager); + when(mIPowerManagerMock.isInteractive()).thenReturn(true); + + mHdmiControlService = new HdmiControlService(mContextSpy) { + @Override + AudioManager getAudioManager() { + return new AudioManager() { + @Override + public void setWiredDeviceConnectionState( + int type, int state, String address, String name) { + // Do nothing. + } + }; + } + + @Override + void wakeUp() { + } + + @Override + boolean isPowerStandby() { + return false; + } + + @Override + protected PowerManager getPowerManager() { + return powerManager; + } + + @Override + protected void writeStringSystemProperty(String key, String value) { + // do nothing + } + }; + + mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService); + mHdmiCecLocalDeviceTv.init(); + mHdmiControlService.setIoLooper(myLooper); + mNativeWrapper = new FakeNativeWrapper(); + HdmiCecController hdmiCecController = HdmiCecController.createWithNativeWrapper( + mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter()); + mHdmiControlService.setCecController(hdmiCecController); + mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService)); + mHdmiControlService.setMessageValidator(new HdmiCecMessageValidator(mHdmiControlService)); + mLocalDevices.add(mHdmiCecLocalDeviceTv); + HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[2]; + hdmiPortInfos[0] = + new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x1000, true, false, false); + hdmiPortInfos[1] = + new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true); + mNativeWrapper.setPortInfo(hdmiPortInfos); + mHdmiControlService.initService(); + mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + mPhysicalAddress = 0x0000; + mNativeWrapper.setPhysicalAddress(mPhysicalAddress); + mTestLooper.dispatchAll(); + mPhysicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress(); + mNativeWrapper.clearResultMessages(); + } + + private void setSystemAudioSetting(boolean on) { + mHdmiCecLocalDeviceTv.setSystemAudioControlFeatureEnabled(on); + } + + private void setTvHasSystemAudioChangeAction() { + mHdmiCecLocalDeviceTv.addAndStartAction( + new SystemAudioActionFromTv(mHdmiCecLocalDeviceTv, Constants.ADDR_AUDIO_SYSTEM, + true, null)); + } + + @Test + public void testReceiveSystemAudioMode_systemAudioOn() { + // Record that previous system audio mode is on. + setSystemAudioSetting(true); + + HdmiCecFeatureAction action = new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, + ADDR_AUDIO_SYSTEM); + mHdmiCecLocalDeviceTv.addAndStartAction(action); + mTestLooper.dispatchAll(); + + HdmiCecMessage giveSystemAudioModeStatus = + HdmiCecMessageBuilder.buildGiveSystemAudioModeStatus( + mHdmiCecLocalDeviceTv.mAddress, ADDR_AUDIO_SYSTEM); + + assertThat(mNativeWrapper.getResultMessages()).contains(giveSystemAudioModeStatus); + + HdmiCecMessage reportSystemAudioMode = HdmiCecMessageBuilder.buildReportSystemAudioMode( + ADDR_AUDIO_SYSTEM, mHdmiCecLocalDeviceTv.mAddress, true); + mHdmiControlService.handleCecCommand(reportSystemAudioMode); + mTestLooper.dispatchAll(); + + assertThat(mHdmiControlService.isSystemAudioActivated()).isTrue(); + } + + @Test + public void testReceiveSystemAudioMode_systemAudioOnAndImpossibleToChangeSystemAudio() { + // Turn on system audio. + setSystemAudioSetting(true); + // Impossible to change system audio mode while SystemAudioActionFromTv is in progress. + setTvHasSystemAudioChangeAction(); + + HdmiCecFeatureAction action = new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, + ADDR_AUDIO_SYSTEM); + mHdmiCecLocalDeviceTv.addAndStartAction(action); + mTestLooper.dispatchAll(); + + HdmiCecMessage giveSystemAudioModeStatus = + HdmiCecMessageBuilder.buildGiveSystemAudioModeStatus( + mHdmiCecLocalDeviceTv.mAddress, ADDR_AUDIO_SYSTEM); + + assertThat(mNativeWrapper.getResultMessages()).contains(giveSystemAudioModeStatus); + + HdmiCecMessage reportSystemAudioMode = HdmiCecMessageBuilder.buildReportSystemAudioMode( + ADDR_AUDIO_SYSTEM, mHdmiCecLocalDeviceTv.mAddress, true); + mHdmiControlService.handleCecCommand(reportSystemAudioMode); + mTestLooper.dispatchAll(); + + assertThat(mHdmiControlService.isSystemAudioActivated()).isFalse(); + } + + @Test + public void testReceiveSystemAudioMode_systemAudioOnAndResponseOff() { + // Record that previous system audio mode is on. + setSystemAudioSetting(true); + + HdmiCecFeatureAction action = new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, + ADDR_AUDIO_SYSTEM); + mHdmiCecLocalDeviceTv.addAndStartAction(action); + mTestLooper.dispatchAll(); + + HdmiCecMessage giveSystemAudioModeStatus = + HdmiCecMessageBuilder.buildGiveSystemAudioModeStatus( + mHdmiCecLocalDeviceTv.mAddress, ADDR_AUDIO_SYSTEM); + + assertThat(mNativeWrapper.getResultMessages()).contains(giveSystemAudioModeStatus); + + HdmiCecMessage reportSystemAudioMode = HdmiCecMessageBuilder.buildReportSystemAudioMode( + ADDR_AUDIO_SYSTEM, mHdmiCecLocalDeviceTv.mAddress, false); + mHdmiControlService.handleCecCommand(reportSystemAudioMode); + mTestLooper.dispatchAll(); + + assertThat(mHdmiCecLocalDeviceTv.getActions(SystemAudioActionFromTv.class)).isNotEmpty(); + SystemAudioActionFromTv resultingAction = mHdmiCecLocalDeviceTv.getActions( + SystemAudioActionFromTv.class).get(0); + assertThat(resultingAction.mTargetAudioStatus).isTrue(); + } + + @Test + public void testReceiveSystemAudioMode_settingOffAndResponseOn() { + // Turn off system audio. + setSystemAudioSetting(false); + + HdmiCecFeatureAction action = new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, + ADDR_AUDIO_SYSTEM); + mHdmiCecLocalDeviceTv.addAndStartAction(action); + mTestLooper.dispatchAll(); + + HdmiCecMessage giveSystemAudioModeStatus = + HdmiCecMessageBuilder.buildGiveSystemAudioModeStatus( + mHdmiCecLocalDeviceTv.mAddress, ADDR_AUDIO_SYSTEM); + + assertThat(mNativeWrapper.getResultMessages()).contains(giveSystemAudioModeStatus); + + HdmiCecMessage reportSystemAudioMode = HdmiCecMessageBuilder.buildReportSystemAudioMode( + ADDR_AUDIO_SYSTEM, mHdmiCecLocalDeviceTv.mAddress, true); + mHdmiControlService.handleCecCommand(reportSystemAudioMode); + mTestLooper.dispatchAll(); + + assertThat(mHdmiCecLocalDeviceTv.getActions(SystemAudioActionFromTv.class)).isNotEmpty(); + SystemAudioActionFromTv resultingAction = mHdmiCecLocalDeviceTv.getActions( + SystemAudioActionFromTv.class).get(0); + assertThat(resultingAction.mTargetAudioStatus).isFalse(); + } + + @Test + public void testReceiveSystemAudioMode_settingOffAndResponseOff() { + // Turn off system audio. + setSystemAudioSetting(false); + + HdmiCecFeatureAction action = new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, + ADDR_AUDIO_SYSTEM); + mHdmiCecLocalDeviceTv.addAndStartAction(action); + mTestLooper.dispatchAll(); + + HdmiCecMessage giveSystemAudioModeStatus = + HdmiCecMessageBuilder.buildGiveSystemAudioModeStatus( + mHdmiCecLocalDeviceTv.mAddress, ADDR_AUDIO_SYSTEM); + + assertThat(mNativeWrapper.getResultMessages()).contains(giveSystemAudioModeStatus); + + HdmiCecMessage reportSystemAudioMode = HdmiCecMessageBuilder.buildReportSystemAudioMode( + ADDR_AUDIO_SYSTEM, mHdmiCecLocalDeviceTv.mAddress, false); + mHdmiControlService.handleCecCommand(reportSystemAudioMode); + mTestLooper.dispatchAll(); + + assertThat(mHdmiCecLocalDeviceTv.getActions(SystemAudioActionFromTv.class)).isEmpty(); + assertThat(mHdmiControlService.isSystemAudioActivated()).isFalse(); + } + + @Test + public void testTimeout_systemAudioOn_retries() { + // Turn on system audio. + setSystemAudioSetting(true); + + HdmiCecFeatureAction action = new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, + ADDR_AUDIO_SYSTEM); + mHdmiCecLocalDeviceTv.addAndStartAction(action); + mTestLooper.dispatchAll(); + + HdmiCecMessage giveSystemAudioModeStatus = + HdmiCecMessageBuilder.buildGiveSystemAudioModeStatus( + mHdmiCecLocalDeviceTv.mAddress, ADDR_AUDIO_SYSTEM); + + assertThat(mNativeWrapper.getResultMessages()).contains(giveSystemAudioModeStatus); + mNativeWrapper.clearResultMessages(); + + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + + // Retry sends again + assertThat(mNativeWrapper.getResultMessages()).contains(giveSystemAudioModeStatus); + } + + @Test + public void testTimeout_systemAudioOn_allRetriesFail() { + boolean targetStatus = true; + // Turn on system audio. + setSystemAudioSetting(targetStatus); + + HdmiCecFeatureAction action = new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, + ADDR_AUDIO_SYSTEM); + mHdmiCecLocalDeviceTv.addAndStartAction(action); + mTestLooper.dispatchAll(); + + HdmiCecMessage giveSystemAudioModeStatus = + HdmiCecMessageBuilder.buildGiveSystemAudioModeStatus( + mHdmiCecLocalDeviceTv.mAddress, ADDR_AUDIO_SYSTEM); + assertThat(mNativeWrapper.getResultMessages()).contains(giveSystemAudioModeStatus); + + for (int i = 0; i < RETRIES_ON_TIMEOUT; i++) { + mNativeWrapper.clearResultMessages(); + + // Target device doesn't respond within timeout + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + + // Retry sends again + assertThat(mNativeWrapper.getResultMessages()).contains(giveSystemAudioModeStatus); + } + + // Target device doesn't respond within timeouts + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + + assertThat(mHdmiCecLocalDeviceTv.getActions(SystemAudioActionFromTv.class)).isNotEmpty(); + SystemAudioActionFromTv resultingAction = mHdmiCecLocalDeviceTv.getActions( + SystemAudioActionFromTv.class).get(0); + assertThat(resultingAction.mTargetAudioStatus).isEqualTo(targetStatus); + } +}