RoutingControlAction.java revision a062a9339add79a84862a34e363e3e454a6ec435
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.server.hdmi; 18 19import android.annotation.Nullable; 20import android.hardware.hdmi.HdmiCec; 21import android.hardware.hdmi.HdmiCecDeviceInfo; 22import android.hardware.hdmi.HdmiCecMessage; 23import android.hardware.hdmi.IHdmiControlCallback; 24import android.os.RemoteException; 25import android.util.Slog; 26 27import com.android.server.hdmi.HdmiControlService.SendMessageCallback; 28 29/** 30 * Feature action for routing control. Exchanges routing-related commands with other devices 31 * to determine the new active source. 32 * 33 * <p>This action is initiated by various cases: 34 * <ul> 35 * <li> Manual TV input switching 36 * <li> Routing change of a CEC switch other than TV 37 * <li> New CEC device at the tail of the active routing path 38 * <li> Removed CEC device from the active routing path 39 * <li> Routing at CEC enable time 40 * </ul> 41 */ 42final class RoutingControlAction extends FeatureAction { 43 private static final String TAG = "RoutingControlAction"; 44 45 // State in which we wait for <Routing Information> to arrive. If timed out, we use the 46 // latest routing path to set the new active source. 47 private final static int STATE_WAIT_FOR_ROUTING_INFORMATION = 1; 48 49 // State in which we wait for <Report Power Status> in response to <Give Device Power Status> 50 // we have sent. If the response tells us the device power is on, we send <Set Stream Path> 51 // to make it the active source. Otherwise we do not send <Set Stream Path>, and possibly 52 // just show the blank screen. 53 private final static int STATE_WAIT_FOR_REPORT_POWER_STATUS = 2; 54 55 // Time out in millseconds used for <Routing Information> 56 private static final int TIMEOUT_ROUTING_INFORMATION_MS = 1000; 57 58 // Time out in milliseconds used for <Report Power Status> 59 private static final int TIMEOUT_REPORT_POWER_STATUS_MS = 1000; 60 61 @Nullable private final IHdmiControlCallback mCallback; 62 63 // The latest routing path. Updated by each <Routing Information> from CEC switches. 64 private int mCurrentRoutingPath; 65 66 RoutingControlAction(HdmiCecLocalDevice localDevice, int path, IHdmiControlCallback callback) { 67 super(localDevice); 68 mCallback = callback; 69 mCurrentRoutingPath = path; 70 } 71 72 @Override 73 public boolean start() { 74 mState = STATE_WAIT_FOR_ROUTING_INFORMATION; 75 addTimer(mState, TIMEOUT_ROUTING_INFORMATION_MS); 76 return true; 77 } 78 79 @Override 80 public boolean processCommand(HdmiCecMessage cmd) { 81 int opcode = cmd.getOpcode(); 82 byte[] params = cmd.getParams(); 83 if (mState == STATE_WAIT_FOR_ROUTING_INFORMATION 84 && opcode == HdmiCec.MESSAGE_ROUTING_INFORMATION) { 85 // Keep updating the physicalAddress as we receive <Routing Information>. 86 // If the routing path doesn't belong to the currently active one, we should 87 // ignore it since it might have come from other routing change sequence. 88 int routingPath = HdmiUtils.twoBytesToInt(params); 89 if (isInActiveRoutingPath(mCurrentRoutingPath, routingPath)) { 90 return true; 91 } 92 mCurrentRoutingPath = routingPath; 93 // Stop possible previous routing change sequence if in progress. 94 removeAction(RoutingControlAction.class); 95 addTimer(mState, TIMEOUT_ROUTING_INFORMATION_MS); 96 return true; 97 } else if (mState == STATE_WAIT_FOR_REPORT_POWER_STATUS 98 && opcode == HdmiCec.MESSAGE_REPORT_POWER_STATUS) { 99 handleReportPowerStatus(cmd.getParams()[0]); 100 return true; 101 } 102 return false; 103 } 104 105 private void handleReportPowerStatus(int devicePowerStatus) { 106 int tvPowerStatus = getTvPowerStatus(); 107 if (isPowerStatusOnOrTransientToOn(tvPowerStatus)) { 108 if (isPowerStatusOnOrTransientToOn(devicePowerStatus)) { 109 sendSetStreamPath(); 110 } else { 111 // The whole action should be stopped here if the device is in standby mode. 112 // We don't attempt to wake it up by sending <Set Stream Path>. 113 } 114 invokeCallback(HdmiCec.RESULT_SUCCESS); 115 finish(); 116 } else { 117 // TV is going into standby mode. 118 // TODO: Figure out what to do. 119 } 120 } 121 122 private int getTvPowerStatus() { 123 // TODO: Obtain TV power status. 124 return HdmiCec.POWER_STATUS_ON; 125 } 126 127 private static boolean isPowerStatusOnOrTransientToOn(int status) { 128 return status == HdmiCec.POWER_STATUS_ON || status == HdmiCec.POWER_STATUS_TRANSIENT_TO_ON; 129 } 130 131 private void sendSetStreamPath() { 132 sendCommand(HdmiCecMessageBuilder.buildSetStreamPath(getSourceAddress(), 133 mCurrentRoutingPath)); 134 } 135 136 private static boolean isInActiveRoutingPath(int activePath, int newPath) { 137 // Check each nibble of the currently active path and the new path till the position 138 // where the active nibble is not zero. For (activePath, newPath), 139 // (1.1.0.0, 1.0.0.0) -> true, new path is a parent 140 // (1.2.1.0, 1.2.1.2) -> true, new path is a descendant 141 // (1.1.0.0, 1.2.0.0) -> false, new path is a sibling 142 // (1.0.0.0, 2.0.0.0) -> false, in a completely different path 143 for (int i = 12; i >= 0; i -= 4) { 144 int nibbleActive = (activePath >> i) & 0xF; 145 if (nibbleActive == 0) { 146 break; 147 } 148 int nibbleNew = (newPath >> i) & 0xF; 149 if (nibbleNew == 0) { 150 break; 151 } 152 if (nibbleActive != nibbleNew) { 153 return false; 154 } 155 } 156 return true; 157 } 158 159 @Override 160 public void handleTimerEvent(int timeoutState) { 161 if (mState != timeoutState || mState == STATE_NONE) { 162 Slog.w("CEC", "Timer in a wrong state. Ignored."); 163 return; 164 } 165 switch (timeoutState) { 166 case STATE_WAIT_FOR_ROUTING_INFORMATION: 167 HdmiCecDeviceInfo device = tv().getDeviceInfoByPath(mCurrentRoutingPath); 168 if (device == null) { 169 maybeChangeActiveInput(tv().pathToPortId(mCurrentRoutingPath)); 170 } else { 171 // TODO: Also check followings and then proceed: 172 // if routing change was neither triggered by TV at CEC enable time, nor 173 // at the detection of new device at the end of the active routing path, nor 174 // by TV power on with HDMI input as the active signal source. 175 int deviceLogicalAddress = device.getLogicalAddress(); 176 queryDevicePowerStatus(deviceLogicalAddress, new SendMessageCallback() { 177 @Override 178 public void onSendCompleted(int error) { 179 handlDevicePowerStatusAckResult(error == HdmiCec.RESULT_SUCCESS); 180 } 181 }); 182 } 183 return; 184 case STATE_WAIT_FOR_REPORT_POWER_STATUS: 185 int tvPowerStatus = getTvPowerStatus(); 186 if (isPowerStatusOnOrTransientToOn(tvPowerStatus)) { 187 if (!maybeChangeActiveInput(localDevice().pathToPortId(mCurrentRoutingPath))) { 188 sendSetStreamPath(); 189 } 190 } 191 invokeCallback(HdmiCec.RESULT_SUCCESS); 192 finish(); 193 return; 194 } 195 } 196 197 // Called whenever an HDMI input of the TV shall become the active input. 198 private boolean maybeChangeActiveInput(int path) { 199 if (localDevice().getActivePortId() == localDevice().pathToPortId(path)) { 200 return false; 201 } 202 // TODO: Remember the currently active input 203 // if PAP/PIP is active, move the focus to the right window, otherwise switch 204 // the port. 205 // Show the OSD input change banner. 206 return true; 207 } 208 209 private void queryDevicePowerStatus(int address, SendMessageCallback callback) { 210 sendCommand(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(getSourceAddress(), address), 211 callback); 212 } 213 214 private void handlDevicePowerStatusAckResult(boolean acked) { 215 if (acked) { 216 mState = STATE_WAIT_FOR_REPORT_POWER_STATUS; 217 addTimer(mState, TIMEOUT_REPORT_POWER_STATUS_MS); 218 } else { 219 maybeChangeActiveInput(localDevice().pathToPortId(mCurrentRoutingPath)); 220 } 221 } 222 223 private void invokeCallback(int result) { 224 if (mCallback == null) { 225 return; 226 } 227 try { 228 mCallback.onComplete(result); 229 } catch (RemoteException e) { 230 // Do nothing. 231 } 232 } 233} 234