From dcf47e903a0a9ed34a6be1e016a845c6f3460891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Col=C8=9Ba?= Date: Tue, 17 Aug 2021 11:16:51 +0000 Subject: [PATCH] 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 " Change-Id: I53c776a042170980b61e823ee67230e1870fa21d --- .../hdmi/DeviceSelectActionFromPlayback.java | 251 ++++++++++ .../server/hdmi/HdmiCecLocalDevice.java | 14 + .../hdmi/HdmiCecLocalDevicePlayback.java | 33 ++ .../server/hdmi/HdmiCecLocalDeviceTv.java | 6 +- .../server/hdmi/HdmiControlService.java | 27 +- .../DeviceSelectActionFromPlaybackTest.java | 428 ++++++++++++++++++ 6 files changed, 743 insertions(+), 16 deletions(-) create mode 100644 services/core/java/com/android/server/hdmi/DeviceSelectActionFromPlayback.java create mode 100644 services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java 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); + } +}