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