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

Commit 9582acea authored by Michal Olech's avatar Michal Olech
Browse files

[CEC] Always send <Set Stream Path> after Routing Changes

Completely removes querying for power status of the tail device

Bug: 154458501
Test: atest RoutingControlActionTest
Change-Id: Ief7d7d5d72e6c8f17889832469d82e1fdf9115ad
parent 5c124736
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -88,7 +88,7 @@ final class ActiveSourceHandler {
                tv.updateActiveSource(current, "ActiveSourceHandler");
                invokeCallback(HdmiControlManager.RESULT_SUCCESS);
            } else {
                tv.startRoutingControl(newActive.physicalAddress, current.physicalAddress, true,
                tv.startRoutingControl(newActive.physicalAddress, current.physicalAddress,
                        mCallback);
            }
        }
+7 −8
Original line number Diff line number Diff line
@@ -370,12 +370,11 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
            return;
        }
        int newPath = mService.portIdToPath(portId);
        startRoutingControl(oldPath, newPath, true, callback);
        startRoutingControl(oldPath, newPath, callback);
    }

    @ServiceThreadOnly
    void startRoutingControl(int oldPath, int newPath, boolean queryDevicePowerStatus,
            IHdmiControlCallback callback) {
    void startRoutingControl(int oldPath, int newPath, IHdmiControlCallback callback) {
        assertRunOnServiceThread();
        if (oldPath == newPath) {
            return;
@@ -385,7 +384,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
        mService.sendCecCommand(routingChange);
        removeAction(RoutingControlAction.class);
        addAndStartAction(
                new RoutingControlAction(this, newPath, queryDevicePowerStatus, callback));
                new RoutingControlAction(this, newPath, callback));
    }

    @ServiceThreadOnly
@@ -554,7 +553,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
        if (isTailOfActivePath(path, getActivePath())) {
            int newPath = mService.portIdToPath(getActivePortId());
            setActivePath(newPath);
            startRoutingControl(getActivePath(), newPath, false, null);
            startRoutingControl(getActivePath(), newPath, null);
            return true;
        }
        return false;
@@ -598,7 +597,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
            getActiveSource().invalidate();
            removeAction(RoutingControlAction.class);
            int newPath = HdmiUtils.twoBytesToInt(params, 2);
            addAndStartAction(new RoutingControlAction(this, newPath, true, null));
            addAndStartAction(new RoutingControlAction(this, newPath, null));
        }
        return true;
    }
@@ -1140,7 +1139,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
        // Seq #23
        if (isTailOfActivePath(path, getActivePath())) {
            int newPath = mService.portIdToPath(getActivePortId());
            startRoutingControl(getActivePath(), newPath, true, null);
            startRoutingControl(getActivePath(), newPath, null);
        }
    }

@@ -1158,7 +1157,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
            if (!routingForBootup && !isProhibitMode()) {
                int newPath = mService.portIdToPath(getActivePortId());
                setActivePath(newPath);
                startRoutingControl(getActivePath(), newPath, routingForBootup, null);
                startRoutingControl(getActivePath(), newPath, null);
            }
        } else {
            int activePath = mService.getPhysicalAddress();
+9 −79
Original line number Diff line number Diff line
@@ -18,12 +18,11 @@ package com.android.server.hdmi;

import android.annotation.Nullable;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.IHdmiControlCallback;
import android.os.RemoteException;
import android.util.Slog;

import com.android.server.hdmi.HdmiControlService.SendMessageCallback;
import com.android.internal.annotations.VisibleForTesting;

/**
 * Feature action for routing control. Exchanges routing-related commands with other devices
@@ -43,23 +42,12 @@ final class RoutingControlAction extends HdmiCecFeatureAction {

    // State in which we wait for <Routing Information> to arrive. If timed out, we use the
    // latest routing path to set the new active source.
    private static final int STATE_WAIT_FOR_ROUTING_INFORMATION = 1;

    // State in which we wait for <Report Power Status> in response to <Give Device Power Status>
    // we have sent. If the response tells us the device power is on, we send <Set Stream Path>
    // to make it the active source. Otherwise we do not send <Set Stream Path>, and possibly
    // just show the blank screen.
    private static final int STATE_WAIT_FOR_REPORT_POWER_STATUS = 2;
    @VisibleForTesting
    static final int STATE_WAIT_FOR_ROUTING_INFORMATION = 1;

    // Time out in millseconds used for <Routing Information>
    private static final int TIMEOUT_ROUTING_INFORMATION_MS = 1000;

    // Time out in milliseconds used for <Report Power Status>
    private static final int TIMEOUT_REPORT_POWER_STATUS_MS = 1000;

    // true if <Give Power Status> should be sent once the new active routing path is determined.
    private final boolean mQueryDevicePowerStatus;

    // If set to true, call {@link HdmiControlService#invokeInputChangeListener()} when
    // the routing control/active source change happens. The listener should be called if
    // the events are triggered by external events such as manual switch port change or incoming
@@ -71,12 +59,10 @@ final class RoutingControlAction extends HdmiCecFeatureAction {
    // The latest routing path. Updated by each <Routing Information> from CEC switches.
    private int mCurrentRoutingPath;

    RoutingControlAction(HdmiCecLocalDevice localDevice, int path, boolean queryDevicePowerStatus,
            IHdmiControlCallback callback) {
    RoutingControlAction(HdmiCecLocalDevice localDevice, int path, IHdmiControlCallback callback) {
        super(localDevice);
        mCallback = callback;
        mCurrentRoutingPath = path;
        mQueryDevicePowerStatus = queryDevicePowerStatus;
        // Callback is non-null when routing control action is brought up by binder API. Use
        // this as an indicator for the input change notification. These API calls will get
        // the result through this callback, not through notification. Any other events that
@@ -109,39 +95,16 @@ final class RoutingControlAction extends HdmiCecFeatureAction {
            removeActionExcept(RoutingControlAction.class, this);
            addTimer(mState, TIMEOUT_ROUTING_INFORMATION_MS);
            return true;
        } else if (mState == STATE_WAIT_FOR_REPORT_POWER_STATUS
                  && opcode == Constants.MESSAGE_REPORT_POWER_STATUS) {
            handleReportPowerStatus(cmd.getParams()[0]);
            return true;
        }
        return false;
    }

    private void handleReportPowerStatus(int devicePowerStatus) {
        if (isPowerOnOrTransient(getTvPowerStatus())) {
            updateActiveInput();
            if (isPowerOnOrTransient(devicePowerStatus)) {
                sendSetStreamPath();
            }
        }
        finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
    }

    private void updateActiveInput() {
        HdmiCecLocalDeviceTv tv = tv();
        tv.setPrevPortId(tv.getActivePortId());
        tv.updateActiveInput(mCurrentRoutingPath, mNotifyInputChange);
    }

    private int getTvPowerStatus() {
        return tv().getPowerStatus();
    }

    private static boolean isPowerOnOrTransient(int status) {
        return status == HdmiControlManager.POWER_STATUS_ON
                || status == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON;
    }

    private void sendSetStreamPath() {
        sendCommand(HdmiCecMessageBuilder.buildSetStreamPath(getSourceAddress(),
                mCurrentRoutingPath));
@@ -160,46 +123,13 @@ final class RoutingControlAction extends HdmiCecFeatureAction {
        }
        switch (timeoutState) {
            case STATE_WAIT_FOR_ROUTING_INFORMATION:
                HdmiDeviceInfo device =
                        localDevice().mService.getHdmiCecNetwork().getDeviceInfoByPath(
                                mCurrentRoutingPath);
                if (device != null && mQueryDevicePowerStatus) {
                    int deviceLogicalAddress = device.getLogicalAddress();
                    queryDevicePowerStatus(deviceLogicalAddress, new SendMessageCallback() {
                        @Override
                        public void onSendCompleted(int error) {
                            handlDevicePowerStatusAckResult(
                                    error == HdmiControlManager.RESULT_SUCCESS);
                        }
                    });
                } else {
                    updateActiveInput();
                    finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
                }
                return;
            case STATE_WAIT_FOR_REPORT_POWER_STATUS:
                if (isPowerOnOrTransient(getTvPowerStatus())) {
                updateActiveInput();
                sendSetStreamPath();
                }
                finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
                return;
        }
    }

    private void queryDevicePowerStatus(int address, SendMessageCallback callback) {
        sendCommand(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(getSourceAddress(), address),
                callback);
    }

    private void handlDevicePowerStatusAckResult(boolean acked) {
        if (acked) {
            mState = STATE_WAIT_FOR_REPORT_POWER_STATUS;
            addTimer(mState, TIMEOUT_REPORT_POWER_STATUS_MS);
        } else {
            updateActiveInput();
            sendSetStreamPath();
            finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
            default:
                Slog.e("CEC", "Invalid timeoutState (" + timeoutState + ").");
                return;
        }
    }

+280 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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.Constants.ADDR_BROADCAST;
import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
import static com.android.server.hdmi.Constants.ADDR_TUNER_1;
import static com.android.server.hdmi.Constants.ADDR_TV;
import static com.android.server.hdmi.Constants.ADDR_UNREGISTERED;
import static com.android.server.hdmi.Constants.MESSAGE_ACTIVE_SOURCE;
import static com.android.server.hdmi.Constants.MESSAGE_ROUTING_INFORMATION;
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
import static com.android.server.hdmi.RoutingControlAction.STATE_WAIT_FOR_ROUTING_INFORMATION;

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

import android.content.Context;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.HdmiPortInfo;
import android.hardware.hdmi.IHdmiControlCallback;
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 com.android.server.hdmi.HdmiCecFeatureAction.ActionTimer;

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;
import java.util.List;

@SmallTest
@RunWith(JUnit4.class)
public class RoutingControlActionTest {
    /*
     * Example connection diagram used in tests. Double-lined paths indicate the currently active
     * routes.
     *
     *
     *                              +-----------+
     *                              |    TV     |
     *                              |  0.0.0.0  |
     *                              +---+-----+-+
     *                                  |     |
     *                               <----------+ 1) AVR -> Switch
     *             +----------+         |     |  +-----------+
     *             | AVR      +---------+     +--+ Switch    |
     *             | 1.0.0.0  |                  | 2.0.0.0   |
     *             +--+---++--+                  +--++-----+-+  <-------+ 2) Recorder -> Blu-ray
     *                |   ||                        ||     |
     *                |   ||                        ||     +--------+
     * +-----------+  |   ||  +----------+     +----++----+         |
     * | XBox      +--+   ++--+ Tuner    |     | Blueray  |   +-----+----+
     * | 1.1.0.0   |          | 1.2.0.0  |     | 2.1.0.0  |   | Recorder |
     * +-----------+          +----++----+     +----------+   | 2.2.0.0  |
     *                             ||                         +----------+
     *                             ||
     *                        +----++----+
     *                        | Player   |
     *                        | 1.2.1.0  |
     *                        +----------+
     *
     */

    private static final int PHYSICAL_ADDRESS_TV = 0x0000;
    private static final int PHYSICAL_ADDRESS_AVR = 0x1000;
    private static final int PHYSICAL_ADDRESS_SWITCH = 0x2000;
    private static final int PHYSICAL_ADDRESS_TUNER = 0x1200;
    private static final int PHYSICAL_ADDRESS_PLAYER = 0x1210;
    private static final int PHYSICAL_ADDRESS_BLUERAY = 0x2100;
    private static final int PHYSICAL_ADDRESS_RECORDER = 0x2200;
    private static final int PORT_1 = 1;
    private static final int PORT_2 = 2;
    private static final int VENDOR_ID_AVR = 0x11233;

    private static final byte[] TUNER_PARAM =
            new byte[] {(PHYSICAL_ADDRESS_TUNER >> 8) & 0xFF, PHYSICAL_ADDRESS_TUNER & 0xFF};
    private static final byte[] PLAYER_PARAM =
            new byte[] {(PHYSICAL_ADDRESS_PLAYER >> 8) & 0xFF, PHYSICAL_ADDRESS_PLAYER & 0xFF};

    private static final HdmiDeviceInfo DEVICE_INFO_AVR =
            new HdmiDeviceInfo(ADDR_AUDIO_SYSTEM, PHYSICAL_ADDRESS_AVR, PORT_1,
                    HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM, VENDOR_ID_AVR, "Audio");
    private static final HdmiDeviceInfo DEVICE_INFO_PLAYER =
            new HdmiDeviceInfo(ADDR_PLAYBACK_1, PHYSICAL_ADDRESS_PLAYER, PORT_1,
                    HdmiDeviceInfo.DEVICE_PLAYBACK, VENDOR_ID_AVR, "Player");
    private static final HdmiCecMessage ROUTING_INFORMATION_TUNER = new HdmiCecMessage(
            ADDR_UNREGISTERED, ADDR_BROADCAST, MESSAGE_ROUTING_INFORMATION, TUNER_PARAM);
    private static final HdmiCecMessage ROUTING_INFORMATION_PLAYER = new HdmiCecMessage(
            ADDR_UNREGISTERED, ADDR_BROADCAST, MESSAGE_ROUTING_INFORMATION, PLAYER_PARAM);
    private static final HdmiCecMessage ACTIVE_SOURCE_TUNER = new HdmiCecMessage(
            ADDR_TUNER_1, ADDR_BROADCAST, MESSAGE_ACTIVE_SOURCE, TUNER_PARAM);
    private static final HdmiCecMessage ACTIVE_SOURCE_PLAYER = new HdmiCecMessage(
            ADDR_PLAYBACK_1, ADDR_BROADCAST, MESSAGE_ACTIVE_SOURCE, PLAYER_PARAM);

    private HdmiControlService mHdmiControlService;
    private HdmiCecController mHdmiCecController;
    private HdmiCecLocalDeviceTv mHdmiCecLocalDeviceTv;
    private FakeNativeWrapper mNativeWrapper;
    private Looper mMyLooper;
    private TestLooper mTestLooper = new TestLooper();
    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();

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

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

        Context context = InstrumentationRegistry.getTargetContext();
        mMyLooper = mTestLooper.getLooper();
        PowerManager powerManager = new PowerManager(context, mIPowerManagerMock,
                mIThermalServiceMock, new Handler(mMyLooper));

        HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(context);

        mHdmiControlService =
                new HdmiControlService(InstrumentationRegistry.getTargetContext()) {
                    @Override
                    boolean isControlEnabled() {
                        return true;
                    }

                    @Override
                    void wakeUp() {
                    }

                    @Override
                    protected void writeStringSystemProperty(String key, String value) {
                        // do nothing
                    }

                    @Override
                    boolean isPowerStandbyOrTransient() {
                        return false;
                    }

                    @Override
                    protected PowerManager getPowerManager() {
                        return powerManager;
                    }

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

        mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService);
        mHdmiCecLocalDeviceTv.init();
        mHdmiControlService.setIoLooper(mMyLooper);
        mNativeWrapper = new FakeNativeWrapper();
        mHdmiCecController = HdmiCecController.createWithNativeWrapper(
                mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
        mHdmiControlService.setCecController(mHdmiCecController);
        mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
        mHdmiControlService.setMessageValidator(new HdmiCecMessageValidator(mHdmiControlService));
        mLocalDevices.add(mHdmiCecLocalDeviceTv);
        HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1];
        hdmiPortInfos[0] =
                new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, PHYSICAL_ADDRESS_AVR,
                                 true, false, false);
        mNativeWrapper.setPortInfo(hdmiPortInfos);
        mHdmiControlService.initService();
        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
        mNativeWrapper.setPhysicalAddress(0x0000);
        mTestLooper.dispatchAll();
        mNativeWrapper.clearResultMessages();
        mHdmiControlService.getHdmiCecNetwork().addCecDevice(DEVICE_INFO_AVR);
    }

    private static class TestActionTimer implements ActionTimer {
        private int mState;

        @Override
        public void sendTimerMessage(int state, long delayMillis) {
            mState = state;
        }

        @Override
        public void clearTimerMessage() {
        }

        private int getState() {
            return mState;
        }
    }

    private static class TestInputSelectCallback extends IHdmiControlCallback.Stub {
        private final List<Integer> mCallbackResult = new ArrayList<Integer>();

        @Override
        public void onComplete(int result) {
            mCallbackResult.add(result);
        }

        private int getResult() {
            assert (mCallbackResult.size() == 1);
            return mCallbackResult.get(0);
        }
    }

    private static RoutingControlAction createRoutingControlAction(HdmiCecLocalDeviceTv localDevice,
            TestInputSelectCallback callback) {
        return new RoutingControlAction(localDevice, PHYSICAL_ADDRESS_AVR, callback);
    }

    // Routing control succeeds against the device connected directly to the port. Action
    // won't get any <Routing Information> in this case. It times out on <Routing Information>,
    // regards the directly connected one as the new routing path to switch to.
    @Test
    public void testRoutingControl_succeedForDirectlyConnectedDevice() {
        TestInputSelectCallback callback = new TestInputSelectCallback();
        TestActionTimer actionTimer = new TestActionTimer();
        mHdmiControlService.getHdmiCecNetwork().addCecDevice(DEVICE_INFO_AVR);

        RoutingControlAction action = createRoutingControlAction(mHdmiCecLocalDeviceTv, callback);
        action.setActionTimer(actionTimer);
        action.start();
        assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_ROUTING_INFORMATION);

        action.handleTimerEvent(actionTimer.getState());
        mTestLooper.dispatchAll();
        HdmiCecMessage setStreamPath = HdmiCecMessageBuilder.buildSetStreamPath(
                        ADDR_TV, PHYSICAL_ADDRESS_AVR);
        assertThat(mNativeWrapper.getResultMessages()).contains(setStreamPath);
    }

    // Succeeds by receiving a couple of <Routing Information> commands, followed by
    // <Set Stream Path> going out in the end.
    @Test
    public void testRoutingControl_succeedForDeviceBehindSwitch() {
        TestInputSelectCallback callback = new TestInputSelectCallback();
        TestActionTimer actionTimer = new TestActionTimer();
        mHdmiControlService.getHdmiCecNetwork().addCecDevice(DEVICE_INFO_PLAYER);
        RoutingControlAction action = createRoutingControlAction(mHdmiCecLocalDeviceTv, callback);
        action.setActionTimer(actionTimer);
        action.start();

        assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_ROUTING_INFORMATION);

        action.processCommand(ROUTING_INFORMATION_TUNER);
        action.processCommand(ROUTING_INFORMATION_PLAYER);

        action.handleTimerEvent(actionTimer.getState());
        mTestLooper.dispatchAll();
        HdmiCecMessage setStreamPath = HdmiCecMessageBuilder.buildSetStreamPath(
                        ADDR_TV, PHYSICAL_ADDRESS_PLAYER);
        assertThat(mNativeWrapper.getResultMessages()).contains(setStreamPath);
    }
}