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

Commit dcf47e90 authored by Paul Colța's avatar Paul Colța
Browse files

HDMICEC: Add deviceSelect API for Playback devices

Create DeviceSelectActionFromPlayback class and add unit tests for it.
Implement deviceSelect inside the HdmiCecLocalDevicePlayback.
Move the Active Source validation from deviceSelect HdmiCecLocalDeviceTv to the abstract class in order to make it visible for HdmiCecLocalDevicePlayback and avoid duplicate code.
Modify the deviceSelect method from HdmiControlService so the playback devices can handle device selection.

Bug: 196934198
Test: make && atest DeviceSelectActionFromPlaybackTest
Test: Manually run the shell command "deviceselect <device id>"
Change-Id: I53c776a042170980b61e823ee67230e1870fa21d
parent 12f914e3
Loading
Loading
Loading
Loading
+251 −0
Original line number Diff line number Diff line
/*
 * 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 android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.HdmiPlaybackClient;
import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.tv.cec.V1_0.SendMessageResult;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;

/**
 * Handles an action that selects a logical device as a new active source.
 *
 * Triggered by {@link HdmiPlaybackClient}, attempts to select the given target device
 * for a new active source. A &gt;Routing Change&lt; command is issued in order to select
 * the target device. If that doesn't succeed a &gt;Set Stream Path&lt; command is sent.
 * It does its best to wake up the target in standby mode, before issuing the device
 * select command.
 */
final class DeviceSelectActionFromPlayback extends HdmiCecFeatureAction {
    private static final String TAG = "DeviceSelectActionFromPlayback";

    // Time in milliseconds we wait for the device power status to switch to 'Standby'
    private static final int TIMEOUT_TRANSIT_TO_STANDBY_MS = 5 * 1000;

    // Time in milliseconds we wait for the device power status to turn to 'On'.
    private static final int TIMEOUT_POWER_ON_MS = 5 * 1000;

    // The number of times we try to wake up the target device before we give up
    // and just send <Routing Change>.
    private static final int LOOP_COUNTER_MAX = 2;

    // State in which we wait for <Report Power Status> to come in response to the command
    // <Give Device Power Status> we have sent.
    @VisibleForTesting
    static final int STATE_WAIT_FOR_REPORT_POWER_STATUS = 1;

    // State in which we wait for the device power status to switch to 'Standby'.
    // We wait till the status becomes 'Standby' before we send <Routing Change>
    // to wake up the device again.
    private static final int STATE_WAIT_FOR_DEVICE_TO_TRANSIT_TO_STANDBY = 2;

    // State in which we wait for the device power status to switch to 'on'. We wait
    // maximum 100 seconds (20 * 5) before we give up and just send <Set Stream Path>.
    @VisibleForTesting
    static final int STATE_WAIT_FOR_DEVICE_POWER_ON = 3;

    // State in which we wait for <Active Source> to come in response to the command
    // <Routing Change> we have sent.
    @VisibleForTesting
    static final int STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_ROUTING_CHANGE = 4;

    // State in which we wait for <Active Source> to come in response to the command
    // <Set Stream Path> we have sent.
    @VisibleForTesting
    private static final int STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_SET_STREAM_PATH = 5;

    private final HdmiDeviceInfo mTarget;
    private final HdmiCecMessage mGivePowerStatus;
    private final boolean mIsCec20;

    private int mPowerStatusCounter = 0;

    /**
     * Constructor.
     *
     * @param source {@link HdmiCecLocalDevice} instance
     * @param target target logical device that will be a new active source
     * @param callback callback object
     */
    DeviceSelectActionFromPlayback(HdmiCecLocalDevicePlayback source, HdmiDeviceInfo target,
            IHdmiControlCallback callback) {
        this(source, target, callback,
                source.getDeviceInfo().getCecVersion() >= HdmiControlManager.HDMI_CEC_VERSION_2_0
                        && target.getCecVersion() >= HdmiControlManager.HDMI_CEC_VERSION_2_0);
    }

    @VisibleForTesting
    DeviceSelectActionFromPlayback(HdmiCecLocalDevicePlayback source, HdmiDeviceInfo target,
            IHdmiControlCallback callback, boolean isCec20) {
        super(source, callback);
        mTarget = target;
        mGivePowerStatus = HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
                getSourceAddress(), getTargetAddress());
        mIsCec20 = isCec20;
    }

    private int getTargetAddress() {
        return mTarget.getLogicalAddress();
    }

    private int getTargetPath() {
        return mTarget.getPhysicalAddress();
    }

    @Override
    public boolean start() {
        // This <Routing Change> message wakes up the target device in most cases.
        sendRoutingChange();

        if (!mIsCec20) {
            queryDevicePowerStatus();
        } else {
            int targetPowerStatus = HdmiControlManager.POWER_STATUS_UNKNOWN;
            HdmiDeviceInfo targetDevice = localDevice().mService.getHdmiCecNetwork()
                    .getCecDeviceInfo(getTargetAddress());
            if (targetDevice != null) {
                targetPowerStatus = targetDevice.getDevicePowerStatus();
            }
            if (targetPowerStatus == HdmiControlManager.POWER_STATUS_UNKNOWN) {
                queryDevicePowerStatus();
            } else if (targetPowerStatus == HdmiControlManager.POWER_STATUS_ON) {
                mState = STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_ROUTING_CHANGE;
                addTimer(mState, HdmiConfig.TIMEOUT_MS);
                return true;
            }
        }

        mState = STATE_WAIT_FOR_REPORT_POWER_STATUS;
        addTimer(mState, HdmiConfig.TIMEOUT_MS);
        return true;
    }

    private void queryDevicePowerStatus() {
        sendCommand(
                mGivePowerStatus,
                new HdmiControlService.SendMessageCallback() {
                    @Override
                    public void onSendCompleted(int error) {
                        if (error != SendMessageResult.SUCCESS) {
                            finishWithCallback(HdmiControlManager.RESULT_COMMUNICATION_FAILED);
                            return;
                        }
                    }
                });
    }

    @Override
    public boolean processCommand(HdmiCecMessage cmd) {
        if (cmd.getSource() != getTargetAddress()) {
            return false;
        }
        int opcode = cmd.getOpcode();
        byte[] params = cmd.getParams();
        if (opcode == Constants.MESSAGE_ACTIVE_SOURCE) {
            // The target device was successfully set as the active source
            finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
            return true;
        }
        if (mState == STATE_WAIT_FOR_REPORT_POWER_STATUS
                && opcode == Constants.MESSAGE_REPORT_POWER_STATUS) {
            return handleReportPowerStatus(params[0]);
        }
        return false;
    }

    private boolean handleReportPowerStatus(int powerStatus) {
        switch (powerStatus) {
            case HdmiControlManager.POWER_STATUS_ON:
                selectDevice();
                return true;
            case HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY:
                if (mPowerStatusCounter < 4) {
                    mState = STATE_WAIT_FOR_DEVICE_TO_TRANSIT_TO_STANDBY;
                    addTimer(mState, TIMEOUT_TRANSIT_TO_STANDBY_MS);
                } else {
                    selectDevice();
                }
                return true;
            case HdmiControlManager.POWER_STATUS_STANDBY:
                if (mPowerStatusCounter == 0) {
                    sendRoutingChange();
                    mState = STATE_WAIT_FOR_DEVICE_POWER_ON;
                    addTimer(mState, TIMEOUT_POWER_ON_MS);
                } else {
                    selectDevice();
                }
                return true;
            case HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON:
                if (mPowerStatusCounter < LOOP_COUNTER_MAX) {
                    mState = STATE_WAIT_FOR_DEVICE_POWER_ON;
                    addTimer(mState, TIMEOUT_POWER_ON_MS);
                } else {
                    selectDevice();
                }
                return true;
        }
        return false;
    }

    private void selectDevice() {
        sendRoutingChange();
        mState = STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_ROUTING_CHANGE;
        addTimer(mState, HdmiConfig.TIMEOUT_MS);
    }

    @Override
    void handleTimerEvent(int timeoutState) {
        if (mState != timeoutState) {
            Slog.w(TAG, "Timer in a wrong state. Ignored.");
            return;
        }
        switch (mState) {
            case STATE_WAIT_FOR_REPORT_POWER_STATUS:
                selectDevice();
                addTimer(mState, HdmiConfig.TIMEOUT_MS);
                break;
            case STATE_WAIT_FOR_DEVICE_POWER_ON:
                mPowerStatusCounter++;
                queryDevicePowerStatus();
                mState = STATE_WAIT_FOR_REPORT_POWER_STATUS;
                addTimer(mState, HdmiConfig.TIMEOUT_MS);
                break;
            case STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_ROUTING_CHANGE:
                sendSetStreamPath();
                mState = STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_SET_STREAM_PATH;
                addTimer(mState, HdmiConfig.TIMEOUT_MS);
                break;
            case STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_SET_STREAM_PATH:
                finishWithCallback(HdmiControlManager.RESULT_TIMEOUT);
                break;
        }
    }

    private void sendRoutingChange() {
        sendCommand(HdmiCecMessageBuilder.buildRoutingChange(getSourceAddress(),
                playback().getActiveSource().physicalAddress, getTargetPath()));
    }

    private void sendSetStreamPath() {
        sendCommand(HdmiCecMessageBuilder.buildSetStreamPath(getSourceAddress(),
                getTargetPath()));
    }
}
+14 −0
Original line number Diff line number Diff line
@@ -260,6 +260,20 @@ abstract class HdmiCecLocalDevice {
        return onMessage(message);
    }

    @ServiceThreadOnly
    @VisibleForTesting
    protected boolean isAlreadyActiveSource(HdmiDeviceInfo targetDevice, int targetAddress,
            IHdmiControlCallback callback) {
        ActiveSource active = getActiveSource();
        if (targetDevice.getDevicePowerStatus() == HdmiControlManager.POWER_STATUS_ON
                && active.isValid()
                && targetAddress == active.logicalAddress) {
            invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS);
            return true;
        }
        return false;
    }

    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected final int onMessage(HdmiCecMessage message) {
+33 −0
Original line number Diff line number Diff line
@@ -124,6 +124,39 @@ public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource {
                String.valueOf(addr));
    }

    /**
     * Performs the action 'device select' or 'one touch play' initiated by a Playback device.
     *
     * @param id id of HDMI device to select
     * @param callback callback object to report the result with
     */
    @ServiceThreadOnly
    void deviceSelect(int id, IHdmiControlCallback callback) {
        assertRunOnServiceThread();
        synchronized (mLock) {
            if (id == getDeviceInfo().getId()) {
                mService.oneTouchPlay(callback);
                return;
            }
        }
        HdmiDeviceInfo targetDevice = mService.getHdmiCecNetwork().getDeviceInfo(id);
        if (targetDevice == null) {
            invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
            return;
        }
        int targetAddress = targetDevice.getLogicalAddress();
        if (isAlreadyActiveSource(targetDevice, targetAddress, callback)) {
            return;
        }
        if (!mService.isControlEnabled()) {
            setActiveSource(targetDevice, "HdmiCecLocalDevicePlayback#deviceSelect()");
            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
            return;
        }
        removeAction(DeviceSelectActionFromPlayback.class);
        addAndStartAction(new DeviceSelectActionFromPlayback(this, targetDevice, callback));
    }

    @Override
    @ServiceThreadOnly
    void onHotplug(int portId, boolean connected) {
+1 −5
Original line number Diff line number Diff line
@@ -242,11 +242,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
            return;
        }
        int targetAddress = targetDevice.getLogicalAddress();
        ActiveSource active = getActiveSource();
        if (targetDevice.getDevicePowerStatus() == HdmiControlManager.POWER_STATUS_ON
                && active.isValid()
                && targetAddress == active.logicalAddress) {
            invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS);
        if (isAlreadyActiveSource(targetDevice, targetAddress, callback)) {
            return;
        }
        if (targetAddress == Constants.ADDR_INTERNAL) {
+16 −11
Original line number Diff line number Diff line
@@ -1685,7 +1685,8 @@ public class HdmiControlService extends SystemService {
                        return;
                    }
                    HdmiCecLocalDeviceTv tv = tv();
                    if (tv == null) {
                    HdmiCecLocalDevicePlayback playback = playback();
                    if (tv == null && playback == null) {
                        if (!mAddressAllocated) {
                            mSelectRequestBuffer.set(SelectRequestBuffer.newDeviceSelect(
                                    HdmiControlService.this, deviceId, callback));
@@ -1698,6 +1699,7 @@ public class HdmiControlService extends SystemService {
                        invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE);
                        return;
                    }
                    if (tv != null) {
                        HdmiMhlLocalDeviceStub device = mMhlController.getLocalDeviceById(deviceId);
                        if (device != null) {
                            if (device.getPortId() == tv.getActivePortId()) {
@@ -1712,6 +1714,9 @@ public class HdmiControlService extends SystemService {
                            return;
                        }
                        tv.deviceSelect(deviceId, callback);
                        return;
                    }
                    playback.deviceSelect(deviceId, callback);
                }
            });
        }
Loading