diff --git a/services/core/java/com/android/server/hdmi/DeviceSelectActionFromPlayback.java b/services/core/java/com/android/server/hdmi/DeviceSelectActionFromPlayback.java new file mode 100644 index 0000000000000..b2338e6c76160 --- /dev/null +++ b/services/core/java/com/android/server/hdmi/DeviceSelectActionFromPlayback.java @@ -0,0 +1,251 @@ +/* + * 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 >Routing Change< command is issued in order to select + * the target device. If that doesn't succeed a >Set Stream Path< 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 . + private static final int LOOP_COUNTER_MAX = 2; + + // State in which we wait for to come in response to the command + // 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 + // 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 . + @VisibleForTesting + static final int STATE_WAIT_FOR_DEVICE_POWER_ON = 3; + + // State in which we wait for to come in response to the command + // we have sent. + @VisibleForTesting + static final int STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_ROUTING_CHANGE = 4; + + // State in which we wait for to come in response to the command + // 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 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())); + } +} diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java index 35090627cb2ae..f94d2207381f0 100755 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java @@ -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) { diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java index 4376c9a9d22b6..744436d41adb1 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java @@ -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) { diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index bfb387f1c3f96..8d0a7bd2cc8b0 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -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) { diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index f993b5d91fdb0..69270949b1935 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -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,20 +1699,24 @@ public class HdmiControlService extends SystemService { invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); return; } - HdmiMhlLocalDeviceStub device = mMhlController.getLocalDeviceById(deviceId); - if (device != null) { - if (device.getPortId() == tv.getActivePortId()) { - invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS); + if (tv != null) { + HdmiMhlLocalDeviceStub device = mMhlController.getLocalDeviceById(deviceId); + if (device != null) { + if (device.getPortId() == tv.getActivePortId()) { + invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS); + return; + } + // Upon selecting MHL device, we send RAP[Content On] to wake up + // the connected mobile device, start routing control to switch ports. + // callback is handled by MHL action. + device.turnOn(callback); + tv.doManualPortSwitching(device.getPortId(), null); return; } - // Upon selecting MHL device, we send RAP[Content On] to wake up - // the connected mobile device, start routing control to switch ports. - // callback is handled by MHL action. - device.turnOn(callback); - tv.doManualPortSwitching(device.getPortId(), null); + tv.deviceSelect(deviceId, callback); return; } - tv.deviceSelect(deviceId, callback); + playback.deviceSelect(deviceId, callback); } }); } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java new file mode 100644 index 0000000000000..f3070b64fa79d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java @@ -0,0 +1,428 @@ +/* + * 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 android.hardware.hdmi.HdmiControlManager.POWER_STATUS_ON; +import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_STANDBY; +import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON; + +import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1; +import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2; +import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_3; +import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_ROUTING_CHANGE; +import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_FOR_DEVICE_POWER_ON; +import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_FOR_REPORT_POWER_STATUS; +import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.hardware.hdmi.HdmiControlManager; +import android.hardware.hdmi.HdmiDeviceInfo; +import android.hardware.hdmi.IHdmiControlCallback; +import android.os.Looper; +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.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Collections; + +@SmallTest +@RunWith(JUnit4.class) +public class DeviceSelectActionFromPlaybackTest { + + private static final int PORT_1 = 1; + private static final int PORT_2 = 1; + private static final int PORT_3 = 1; + private static final int PHYSICAL_ADDRESS_PLAYBACK_1 = 0x1000; + private static final int PHYSICAL_ADDRESS_PLAYBACK_2 = 0x2000; + private static final int PHYSICAL_ADDRESS_PLAYBACK_3 = 0x3000; + + private static final byte[] POWER_ON = new byte[] { POWER_STATUS_ON }; + private static final byte[] POWER_STANDBY = new byte[] { POWER_STATUS_STANDBY }; + private static final byte[] POWER_TRANSIENT_TO_ON = new byte[] { POWER_STATUS_TRANSIENT_TO_ON }; + private static final HdmiCecMessage REPORT_POWER_STATUS_ON = new HdmiCecMessage( + ADDR_PLAYBACK_2, ADDR_PLAYBACK_1, Constants.MESSAGE_REPORT_POWER_STATUS, POWER_ON); + private static final HdmiCecMessage REPORT_POWER_STATUS_STANDBY = new HdmiCecMessage( + ADDR_PLAYBACK_2, ADDR_PLAYBACK_1, Constants.MESSAGE_REPORT_POWER_STATUS, POWER_STANDBY); + private static final HdmiCecMessage REPORT_POWER_STATUS_TRANSIENT_TO_ON = new HdmiCecMessage( + ADDR_PLAYBACK_2, ADDR_PLAYBACK_1, Constants.MESSAGE_REPORT_POWER_STATUS, + POWER_TRANSIENT_TO_ON); + private static final HdmiCecMessage SET_STREAM_PATH = HdmiCecMessageBuilder.buildSetStreamPath( + ADDR_PLAYBACK_1, PHYSICAL_ADDRESS_PLAYBACK_2); + private static final HdmiCecMessage ROUTING_CHANGE = HdmiCecMessageBuilder.buildRoutingChange( + ADDR_PLAYBACK_1, PHYSICAL_ADDRESS_PLAYBACK_3, PHYSICAL_ADDRESS_PLAYBACK_2); + private static final HdmiCecMessage ACTIVE_SOURCE = HdmiCecMessageBuilder.buildActiveSource( + ADDR_PLAYBACK_2, PHYSICAL_ADDRESS_PLAYBACK_2); + private static final HdmiDeviceInfo INFO_PLAYBACK_1 = new HdmiDeviceInfo( + ADDR_PLAYBACK_1, PHYSICAL_ADDRESS_PLAYBACK_1, PORT_1, HdmiDeviceInfo.DEVICE_PLAYBACK, + 0x1234, "Playback 1", + HdmiControlManager.POWER_STATUS_ON, HdmiControlManager.HDMI_CEC_VERSION_1_4_B); + private static final HdmiDeviceInfo INFO_PLAYBACK_2 = new HdmiDeviceInfo( + ADDR_PLAYBACK_2, PHYSICAL_ADDRESS_PLAYBACK_2, PORT_2, HdmiDeviceInfo.DEVICE_PLAYBACK, + 0x1234, "Playback 2", + HdmiControlManager.POWER_STATUS_ON, HdmiControlManager.HDMI_CEC_VERSION_1_4_B); + private static final HdmiDeviceInfo INFO_PLAYBACK_3 = new HdmiDeviceInfo( + ADDR_PLAYBACK_3, PHYSICAL_ADDRESS_PLAYBACK_3, PORT_3, HdmiDeviceInfo.DEVICE_PLAYBACK, + 0x1234, "Playback 3", + HdmiControlManager.POWER_STATUS_ON, HdmiControlManager.HDMI_CEC_VERSION_1_4_B); + + private HdmiCecController mHdmiCecController; + private HdmiCecLocalDevicePlayback mHdmiCecLocalDevicePlayback; + private HdmiCecNetwork mHdmiCecNetwork; + private HdmiControlService mHdmiControlService; + private HdmiMhlControllerStub mHdmiMhlControllerStub; + + private FakeNativeWrapper mNativeWrapper; + private FakePowerManagerWrapper mPowerManager; + private Looper mMyLooper; + private TestLooper mTestLooper = new TestLooper(); + private ArrayList mLocalDevices = new ArrayList<>(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + Context context = InstrumentationRegistry.getTargetContext(); + mMyLooper = mTestLooper.getLooper(); + + mHdmiControlService = + new HdmiControlService(InstrumentationRegistry.getTargetContext(), + Collections.emptyList()) { + @Override + boolean isControlEnabled() { + return true; + } + + @Override + protected void writeStringSystemProperty(String key, String value) { + // do nothing + } + + @Override + boolean isPowerStandbyOrTransient() { + return false; + } + }; + + + mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback(mHdmiControlService); + mHdmiCecLocalDevicePlayback.init(); + mHdmiControlService.setIoLooper(mMyLooper); + mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context)); + mNativeWrapper = new FakeNativeWrapper(); + mHdmiCecController = HdmiCecController.createWithNativeWrapper( + mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter()); + mHdmiControlService.setCecController(mHdmiCecController); + mHdmiMhlControllerStub = HdmiMhlControllerStub.create(mHdmiControlService); + mHdmiControlService.setHdmiMhlController(mHdmiMhlControllerStub); + mHdmiControlService.setMessageValidator(new HdmiCecMessageValidator(mHdmiControlService)); + mHdmiControlService.setCecController(mHdmiCecController); + mHdmiControlService.setHdmiMhlController(mHdmiMhlControllerStub); + mHdmiControlService.setMessageValidator(new HdmiCecMessageValidator(mHdmiControlService)); + mHdmiCecNetwork = new HdmiCecNetwork(mHdmiControlService, + mHdmiCecController, mHdmiMhlControllerStub); + mHdmiControlService.setHdmiCecNetwork(mHdmiCecNetwork); + + mLocalDevices.add(mHdmiCecLocalDevicePlayback); + mHdmiControlService.initService(); + mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); + mNativeWrapper.setPhysicalAddress(0x0000); + mPowerManager = new FakePowerManagerWrapper(context); + mHdmiControlService.setPowerManager(mPowerManager); + mTestLooper.dispatchAll(); + mNativeWrapper.clearResultMessages(); + + mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_PLAYBACK_1); + mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_PLAYBACK_2); + mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_PLAYBACK_3); + } + + 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 TestCallback extends IHdmiControlCallback.Stub { + private final ArrayList mCallbackResult = new ArrayList(); + + @Override + public void onComplete(int result) { + mCallbackResult.add(result); + } + + private int getResult() { + assertThat(mCallbackResult.size()).isEqualTo(1); + return mCallbackResult.get(0); + } + } + + private DeviceSelectActionFromPlayback createDeviceSelectActionFromPlayback( + TestActionTimer actionTimer, + TestCallback callback, + boolean isCec20) { + HdmiDeviceInfo hdmiDeviceInfo = + mHdmiControlService.getHdmiCecNetwork().getCecDeviceInfo(ADDR_PLAYBACK_2); + DeviceSelectActionFromPlayback action = new DeviceSelectActionFromPlayback( + mHdmiCecLocalDevicePlayback, + hdmiDeviceInfo, callback, isCec20); + action.setActionTimer(actionTimer); + return action; + } + + @Test + public void testDeviceSelect_DeviceInPowerOnStatus_Cec14b() { + // TV was watching playback3 device connected at port 3, and wants to select + // playback2. + TestActionTimer actionTimer = new TestActionTimer(); + TestCallback callback = new TestCallback(); + DeviceSelectActionFromPlayback action = createDeviceSelectActionFromPlayback(actionTimer, + callback, /*isCec20=*/false); + mHdmiControlService.setActiveSource(ADDR_PLAYBACK_3, PHYSICAL_ADDRESS_PLAYBACK_3, + "testDeviceSelectFromPlayback"); + action.start(); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + mNativeWrapper.clearResultMessages(); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_REPORT_POWER_STATUS); + action.processCommand(REPORT_POWER_STATUS_ON); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + action.processCommand(ACTIVE_SOURCE); + assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS); + } + + @Test + public void testDeviceSelect_DeviceInStandbyStatus_Cec14b() { + mHdmiControlService.setActiveSource(ADDR_PLAYBACK_3, PHYSICAL_ADDRESS_PLAYBACK_3, + "testDeviceSelectFromPlayback"); + TestActionTimer actionTimer = new TestActionTimer(); + TestCallback callback = new TestCallback(); + DeviceSelectActionFromPlayback action = createDeviceSelectActionFromPlayback(actionTimer, + callback, /*isCec20=*/false); + action.start(); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + mNativeWrapper.clearResultMessages(); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_REPORT_POWER_STATUS); + action.processCommand(REPORT_POWER_STATUS_STANDBY); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + mNativeWrapper.clearResultMessages(); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_DEVICE_POWER_ON); + action.handleTimerEvent(STATE_WAIT_FOR_DEVICE_POWER_ON); + action.processCommand(REPORT_POWER_STATUS_ON); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + action.processCommand(ACTIVE_SOURCE); + assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS); + } + + @Test + public void testDeviceSelect_DeviceInStandbyStatusWithSomeTimeouts_Cec14b() { + mHdmiControlService.setActiveSource(ADDR_PLAYBACK_3, PHYSICAL_ADDRESS_PLAYBACK_3, + "testDeviceSelectFromPlayback"); + TestActionTimer actionTimer = new TestActionTimer(); + TestCallback callback = new TestCallback(); + DeviceSelectActionFromPlayback action = createDeviceSelectActionFromPlayback(actionTimer, + callback, /*isCec20=*/false); + action.start(); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + mNativeWrapper.clearResultMessages(); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_REPORT_POWER_STATUS); + action.processCommand(REPORT_POWER_STATUS_STANDBY); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_DEVICE_POWER_ON); + action.handleTimerEvent(STATE_WAIT_FOR_DEVICE_POWER_ON); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_REPORT_POWER_STATUS); + action.processCommand(REPORT_POWER_STATUS_TRANSIENT_TO_ON); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_DEVICE_POWER_ON); + action.handleTimerEvent(STATE_WAIT_FOR_DEVICE_POWER_ON); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_REPORT_POWER_STATUS); + action.processCommand(REPORT_POWER_STATUS_ON); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + action.processCommand(ACTIVE_SOURCE); + assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS); + } + + @Test + public void testDeviceSelect_DeviceInStandbyAfterTimeoutForReportPowerStatus_Cec14b() { + mHdmiControlService.setActiveSource(ADDR_PLAYBACK_3, PHYSICAL_ADDRESS_PLAYBACK_3, + "testDeviceSelectFromPlayback"); + TestActionTimer actionTimer = new TestActionTimer(); + TestCallback callback = new TestCallback(); + DeviceSelectActionFromPlayback action = createDeviceSelectActionFromPlayback(actionTimer, + callback, /*isCec20=*/false); + action.start(); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + mNativeWrapper.clearResultMessages(); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_REPORT_POWER_STATUS); + action.processCommand(REPORT_POWER_STATUS_STANDBY); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_DEVICE_POWER_ON); + action.handleTimerEvent(STATE_WAIT_FOR_DEVICE_POWER_ON); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_REPORT_POWER_STATUS); + action.processCommand(REPORT_POWER_STATUS_TRANSIENT_TO_ON); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_DEVICE_POWER_ON); + action.handleTimerEvent(STATE_WAIT_FOR_DEVICE_POWER_ON); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_REPORT_POWER_STATUS); + action.handleTimerEvent(STATE_WAIT_FOR_REPORT_POWER_STATUS); + // Give up getting power status, and just send + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + action.processCommand(ACTIVE_SOURCE); + assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS); + } + + @Test + public void testDeviceSelect_ReachSetStreamPath_Cec14b() { + TestActionTimer actionTimer = new TestActionTimer(); + TestCallback callback = new TestCallback(); + DeviceSelectActionFromPlayback action = createDeviceSelectActionFromPlayback(actionTimer, + callback, /*isCec20=*/false); + mHdmiControlService.setActiveSource(ADDR_PLAYBACK_3, PHYSICAL_ADDRESS_PLAYBACK_3, + "testDeviceSelectFromPlayback"); + action.start(); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + mNativeWrapper.clearResultMessages(); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_REPORT_POWER_STATUS); + action.processCommand(REPORT_POWER_STATUS_ON); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + mNativeWrapper.clearResultMessages(); + assertThat(actionTimer.getState()).isEqualTo( + STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_ROUTING_CHANGE); + action.handleTimerEvent(STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_ROUTING_CHANGE); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(SET_STREAM_PATH); + action.processCommand(ACTIVE_SOURCE); + assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS); + } + + @Test + public void testDeviceSelect_ReachSetStreamPathDeviceInPowerOnStatus_Cec20() { + mHdmiControlService.getHdmiCecNetwork().updateDevicePowerStatus(ADDR_PLAYBACK_2, + HdmiControlManager.POWER_STATUS_ON); + TestActionTimer actionTimer = new TestActionTimer(); + TestCallback callback = new TestCallback(); + DeviceSelectActionFromPlayback action = createDeviceSelectActionFromPlayback(actionTimer, + callback, /*isCec20=*/true); + mHdmiControlService.setActiveSource(ADDR_PLAYBACK_3, PHYSICAL_ADDRESS_PLAYBACK_3, + "testDeviceSelectFromPlayback"); + action.start(); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + mNativeWrapper.clearResultMessages(); + assertThat(actionTimer.getState()).isEqualTo( + STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_ROUTING_CHANGE); + action.handleTimerEvent(STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_ROUTING_CHANGE); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(SET_STREAM_PATH); + action.processCommand(ACTIVE_SOURCE); + assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS); + } + + @Test + public void testDeviceSelect_DeviceInPowerOnStatus_Cec20() { + mHdmiControlService.getHdmiCecNetwork().updateDevicePowerStatus(ADDR_PLAYBACK_2, + HdmiControlManager.POWER_STATUS_ON); + TestActionTimer actionTimer = new TestActionTimer(); + TestCallback callback = new TestCallback(); + DeviceSelectActionFromPlayback action = createDeviceSelectActionFromPlayback(actionTimer, + callback, /*isCec20=*/true); + mHdmiControlService.setActiveSource(ADDR_PLAYBACK_3, PHYSICAL_ADDRESS_PLAYBACK_3, + "testDeviceSelectFromPlayback"); + action.start(); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + action.processCommand(ACTIVE_SOURCE); + assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS); + } + + @Test + public void testDeviceSelect_DeviceInPowerUnknownStatus_Cec20() { + mHdmiControlService.getHdmiCecNetwork().updateDevicePowerStatus(ADDR_PLAYBACK_2, + HdmiControlManager.POWER_STATUS_UNKNOWN); + TestActionTimer actionTimer = new TestActionTimer(); + TestCallback callback = new TestCallback(); + DeviceSelectActionFromPlayback action = createDeviceSelectActionFromPlayback(actionTimer, + callback, /*isCec20=*/true); + mHdmiControlService.setActiveSource(ADDR_PLAYBACK_3, PHYSICAL_ADDRESS_PLAYBACK_3, + "testDeviceSelectFromPlayback"); + action.start(); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + mNativeWrapper.clearResultMessages(); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_REPORT_POWER_STATUS); + action.processCommand(REPORT_POWER_STATUS_ON); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + action.processCommand(ACTIVE_SOURCE); + assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS); + } + + @Test + public void testDeviceSelect_DeviceInStandbyStatus_Cec20() { + mHdmiControlService.getHdmiCecNetwork().updateDevicePowerStatus(ADDR_PLAYBACK_2, + HdmiControlManager.POWER_STATUS_STANDBY); + mHdmiControlService.setActiveSource(ADDR_PLAYBACK_3, PHYSICAL_ADDRESS_PLAYBACK_3, + "testDeviceSelectFromPlayback"); + TestActionTimer actionTimer = new TestActionTimer(); + TestCallback callback = new TestCallback(); + DeviceSelectActionFromPlayback action = createDeviceSelectActionFromPlayback(actionTimer, + callback, /*isCec20=*/true); + action.start(); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + mNativeWrapper.clearResultMessages(); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_REPORT_POWER_STATUS); + action.processCommand(REPORT_POWER_STATUS_STANDBY); + mTestLooper.dispatchAll(); + mNativeWrapper.clearResultMessages(); + assertThat(actionTimer.getState()).isEqualTo(STATE_WAIT_FOR_DEVICE_POWER_ON); + action.handleTimerEvent(STATE_WAIT_FOR_DEVICE_POWER_ON); + action.processCommand(REPORT_POWER_STATUS_ON); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(ROUTING_CHANGE); + action.processCommand(ACTIVE_SOURCE); + assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS); + } +}