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
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java b/services/core/java/com/android/server/hdmi/SystemAudioAutoInitiationAction.java
index f7e871d..56e538b 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 @@
     // <Give System Audio Mode Status> 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 <System Audio Mode Status> message can be delayed as the device
+    // is just waking up. Retry the <Give System Audio Mode Status> 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 @@
 
         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 0000000..865eb7a
--- /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<HdmiCecLocalDevice> 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 <Give System Audio Mode Status> 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 <Give System Audio Mode Status> 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);
+    }
+}