Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5aab7804 authored by Marvin Ramin's avatar Marvin Ramin
Browse files

Add retries to CEC detection

When CEC source devices attempt to query whether the sink device
supports CEC, they query the target devices power status.

As this query might happen when the sink device is just waking up, as
part of a One Touch Play sequence, the sink device might be too slow to
respond to the query in time (2s).

Add a single retry to ensure the sink device has another chance to
respond to the query. If the target device doesn't ACK the query
message, assume no CEC support. The retry should increase stability of
CEC volume control mode detection.

Bug: 172539380
Test: atest DevicePowerStatusActionTest
Change-Id: Ic6cb5da968da5e12fbaea6b3ad2fa5cff82728e8
parent c25364b5
Loading
Loading
Loading
Loading
+21 −2
Original line number Original line Diff line number Diff line
@@ -20,6 +20,7 @@ import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiPlaybackClient;
import android.hardware.hdmi.HdmiPlaybackClient;
import android.hardware.hdmi.HdmiPlaybackClient.DisplayStatusCallback;
import android.hardware.hdmi.HdmiPlaybackClient.DisplayStatusCallback;
import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.tv.cec.V1_0.SendMessageResult;
import android.os.RemoteException;
import android.os.RemoteException;
import android.util.Slog;
import android.util.Slog;


@@ -42,6 +43,10 @@ final class DevicePowerStatusAction extends HdmiCecFeatureAction {
    private final int mTargetAddress;
    private final int mTargetAddress;
    private final List<IHdmiControlCallback> mCallbacks = new ArrayList<>();
    private final List<IHdmiControlCallback> mCallbacks = new ArrayList<>();


    // Retry the power status query as it might happen when the target device is waking up. In
    // that case a device may be quite busy and can fail to respond within the 2s timeout.
    private int mRetriesOnTimeout = 1;

    static DevicePowerStatusAction create(HdmiCecLocalDevice source,
    static DevicePowerStatusAction create(HdmiCecLocalDevice source,
            int targetAddress, IHdmiControlCallback callback) {
            int targetAddress, IHdmiControlCallback callback) {
        if (source == null || callback == null) {
        if (source == null || callback == null) {
@@ -68,7 +73,15 @@ final class DevicePowerStatusAction extends HdmiCecFeatureAction {


    private void queryDevicePowerStatus() {
    private void queryDevicePowerStatus() {
        sendCommand(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(getSourceAddress(),
        sendCommand(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(getSourceAddress(),
                mTargetAddress));
                mTargetAddress), error -> {
                // Don't retry on timeout if the remote device didn't even ACK the message. Assume
                // the device is not present or not capable of CEC.
                if (error == SendMessageResult.NACK) {
                    // Got no response from TV. Report status 'unknown'.
                    invokeCallback(HdmiControlManager.POWER_STATUS_UNKNOWN);
                    finish();
                }
            });
    }
    }


    @Override
    @Override
@@ -92,6 +105,12 @@ final class DevicePowerStatusAction extends HdmiCecFeatureAction {
            return;
            return;
        }
        }
        if (state == STATE_WAITING_FOR_REPORT_POWER_STATUS) {
        if (state == STATE_WAITING_FOR_REPORT_POWER_STATUS) {
            if (mRetriesOnTimeout > 0) {
                mRetriesOnTimeout--;
                start();
                return;
            }

            // Got no response from TV. Report status 'unknown'.
            // Got no response from TV. Report status 'unknown'.
            invokeCallback(HdmiControlManager.POWER_STATUS_UNKNOWN);
            invokeCallback(HdmiControlManager.POWER_STATUS_UNKNOWN);
            finish();
            finish();
+217 −0
Original line number Original line Diff line number Diff line
/*
 * 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 com.android.server.hdmi;

import static com.android.server.hdmi.Constants.ADDR_TV;
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.ContextWrapper;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.tv.cec.V1_0.SendMessageResult;
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;

/** Tests for {@link DevicePowerStatusAction} */
@SmallTest
@RunWith(JUnit4.class)
public class DevicePowerStatusActionTest {

    private static final int TIMEOUT_MS = HdmiConfig.TIMEOUT_MS + 1;

    private Context mContextSpy;
    private HdmiControlService mHdmiControlService;
    private HdmiCecLocalDevice mPlaybackDevice;
    private FakeNativeWrapper mNativeWrapper;

    private TestLooper mTestLooper = new TestLooper();
    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
    private int mPhysicalAddress;

    private DevicePowerStatusAction mDevicePowerStatusAction;

    @Mock private IHdmiControlCallback mCallbackMock;
    @Mock private IPowerManager mIPowerManagerMock;
    @Mock private IThermalService mIThermalServiceMock;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);

        mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));

        PowerManager powerManager = new PowerManager(mContextSpy, mIPowerManagerMock,
                mIThermalServiceMock, new Handler(mTestLooper.getLooper()));
        when(mContextSpy.getSystemService(Context.POWER_SERVICE)).thenReturn(powerManager);
        when(mContextSpy.getSystemService(PowerManager.class)).thenReturn(powerManager);
        when(mIPowerManagerMock.isInteractive()).thenReturn(true);

        HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);

        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
            }

            @Override
            protected HdmiCecConfig getHdmiCecConfig() {
                return hdmiCecConfig;
            }
        };

        Looper looper = mTestLooper.getLooper();
        mHdmiControlService.setIoLooper(looper);
        mNativeWrapper = new FakeNativeWrapper();
        HdmiCecController hdmiCecController = HdmiCecController.createWithNativeWrapper(
                this.mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
        mHdmiControlService.setCecController(hdmiCecController);
        mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
        mHdmiControlService.setMessageValidator(new HdmiCecMessageValidator(mHdmiControlService));
        mHdmiControlService.initService();
        mPhysicalAddress = 0x2000;
        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
        mPlaybackDevice = new HdmiCecLocalDevicePlayback(
                mHdmiControlService);
        mPlaybackDevice.init();
        mLocalDevices.add(mPlaybackDevice);
        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
        mDevicePowerStatusAction = DevicePowerStatusAction.create(mPlaybackDevice, ADDR_TV,
                mCallbackMock);
        mTestLooper.dispatchAll();
    }

    @Test
    public void queryDisplayStatus_sendsRequestAndHandlesResponse() throws Exception {
        mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction);
        mTestLooper.dispatchAll();

        HdmiCecMessage expected = HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
                mPlaybackDevice.mAddress, ADDR_TV);
        assertThat(mNativeWrapper.getResultMessages()).contains(expected);

        HdmiCecMessage response = HdmiCecMessageBuilder.buildReportPowerStatus(
                ADDR_TV, mPlaybackDevice.mAddress, HdmiControlManager.POWER_STATUS_STANDBY);
        mNativeWrapper.onCecMessage(response);
        mTestLooper.dispatchAll();

        verify(mCallbackMock).onComplete(HdmiControlManager.POWER_STATUS_STANDBY);
    }

    @Test
    public void queryDisplayStatus_sendsRequest_nack() throws Exception {
        mNativeWrapper.setMessageSendResult(Constants.MESSAGE_GIVE_DEVICE_POWER_STATUS,
                SendMessageResult.NACK);

        mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction);
        mTestLooper.dispatchAll();

        verify(mCallbackMock).onComplete(HdmiControlManager.POWER_STATUS_UNKNOWN);
    }

    @Test
    public void queryDisplayStatus_sendsRequest_timeout_retriesSuccessfully() throws Exception {
        mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction);

        HdmiCecMessage expected = HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
                mPlaybackDevice.mAddress, ADDR_TV);
        mTestLooper.dispatchAll();

        assertThat(mNativeWrapper.getResultMessages()).contains(expected);
        mNativeWrapper.clearResultMessages();
        mTestLooper.moveTimeForward(TIMEOUT_MS);
        mTestLooper.dispatchAll();

        assertThat(mNativeWrapper.getResultMessages()).contains(expected);

        HdmiCecMessage response = HdmiCecMessageBuilder.buildReportPowerStatus(
                ADDR_TV, mPlaybackDevice.mAddress, HdmiControlManager.POWER_STATUS_STANDBY);
        mNativeWrapper.onCecMessage(response);
        mTestLooper.dispatchAll();

        verify(mCallbackMock).onComplete(HdmiControlManager.POWER_STATUS_STANDBY);
    }

    @Test
    public void queryDisplayStatus_sendsRequest_timeout_retriesFailure() throws Exception {
        mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction);
        mTestLooper.dispatchAll();

        HdmiCecMessage expected = HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
                mPlaybackDevice.mAddress, ADDR_TV);
        assertThat(mNativeWrapper.getResultMessages()).contains(expected);
        mNativeWrapper.clearResultMessages();
        mTestLooper.moveTimeForward(TIMEOUT_MS);
        mTestLooper.dispatchAll();

        assertThat(mNativeWrapper.getResultMessages()).contains(expected);
        mNativeWrapper.clearResultMessages();
        mTestLooper.moveTimeForward(TIMEOUT_MS);
        mTestLooper.dispatchAll();

        verify(mCallbackMock).onComplete(HdmiControlManager.POWER_STATUS_UNKNOWN);
    }
}