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.hardware.hdmi.HdmiDeviceInfo;
20import android.util.Slog;
21
22import com.android.server.hdmi.HdmiControlService.DevicePollingCallback;
23
24import java.util.BitSet;
25import java.util.List;
26
27/**
28 * Feature action that handles hot-plug detection mechanism.
29 * Hot-plug event is initiated by timer after device discovery action.
30 *
31 * <p>Check all devices every 15 secs except for system audio.
32 * If system audio is on, check hot-plug for audio system every 5 secs.
33 * For other devices, keep 15 secs period.
34 */
35// Seq #3
36final class HotplugDetectionAction extends HdmiCecFeatureAction {
37    private static final String TAG = "HotPlugDetectionAction";
38
39    private static final int POLLING_INTERVAL_MS = 5000;
40    private static final int TIMEOUT_COUNT = 3;
41    private static final int AVR_COUNT_MAX = 3;
42
43    // State in which waits for next polling
44    private static final int STATE_WAIT_FOR_NEXT_POLLING = 1;
45
46    // All addresses except for broadcast (unregistered address).
47    private static final int NUM_OF_ADDRESS = Constants.ADDR_SPECIFIC_USE
48            - Constants.ADDR_TV + 1;
49
50    private int mTimeoutCount = 0;
51
52    // Counter used to ensure the connection to AVR is stable. Occasional failure to get
53    // polling response from AVR despite its presence leads to unstable status flipping.
54    // This is a workaround to deal with it, by removing the device only if the removal
55    // is detected {@code AVR_COUNT_MAX} times in a row.
56    private int mAvrStatusCount = 0;
57
58    /**
59     * Constructor
60     *
61     * @param source {@link HdmiCecLocalDevice} instance
62     */
63    HotplugDetectionAction(HdmiCecLocalDevice source) {
64        super(source);
65    }
66
67    @Override
68    boolean start() {
69        Slog.v(TAG, "Hot-plug dection started.");
70
71        mState = STATE_WAIT_FOR_NEXT_POLLING;
72        mTimeoutCount = 0;
73
74        // Start timer without polling.
75        // The first check for all devices will be initiated 15 seconds later.
76        addTimer(mState, POLLING_INTERVAL_MS);
77        return true;
78    }
79
80    @Override
81    boolean processCommand(HdmiCecMessage cmd) {
82        // No-op
83        return false;
84    }
85
86    @Override
87    void handleTimerEvent(int state) {
88        if (mState != state) {
89            return;
90        }
91
92        if (mState == STATE_WAIT_FOR_NEXT_POLLING) {
93            mTimeoutCount = (mTimeoutCount + 1) % TIMEOUT_COUNT;
94            pollDevices();
95        }
96    }
97
98    /**
99     * Start device polling immediately.
100     */
101    void pollAllDevicesNow() {
102        // Clear existing timer to avoid overlapped execution
103        mActionTimer.clearTimerMessage();
104
105        mTimeoutCount = 0;
106        mState = STATE_WAIT_FOR_NEXT_POLLING;
107        pollAllDevices();
108
109        addTimer(mState, POLLING_INTERVAL_MS);
110    }
111
112    // This method is called every 5 seconds.
113    private void pollDevices() {
114        // All device check called every 15 seconds.
115        if (mTimeoutCount == 0) {
116            pollAllDevices();
117        } else {
118            if (tv().isSystemAudioActivated()) {
119                pollAudioSystem();
120            }
121        }
122
123        addTimer(mState, POLLING_INTERVAL_MS);
124    }
125
126    private void pollAllDevices() {
127        Slog.v(TAG, "Poll all devices.");
128
129        pollDevices(new DevicePollingCallback() {
130            @Override
131            public void onPollingFinished(List<Integer> ackedAddress) {
132                checkHotplug(ackedAddress, false);
133            }
134        }, Constants.POLL_ITERATION_IN_ORDER
135                | Constants.POLL_STRATEGY_REMOTES_DEVICES, HdmiConfig.HOTPLUG_DETECTION_RETRY);
136    }
137
138    private void pollAudioSystem() {
139        Slog.v(TAG, "Poll audio system.");
140
141        pollDevices(new DevicePollingCallback() {
142            @Override
143            public void onPollingFinished(List<Integer> ackedAddress) {
144                checkHotplug(ackedAddress, true);
145            }
146        }, Constants.POLL_ITERATION_IN_ORDER
147                | Constants.POLL_STRATEGY_SYSTEM_AUDIO, HdmiConfig.HOTPLUG_DETECTION_RETRY);
148    }
149
150    private void checkHotplug(List<Integer> ackedAddress, boolean audioOnly) {
151        BitSet currentInfos = infoListToBitSet(tv().getDeviceInfoList(false), audioOnly);
152        BitSet polledResult = addressListToBitSet(ackedAddress);
153
154        // At first, check removed devices.
155        BitSet removed = complement(currentInfos, polledResult);
156        int index = -1;
157        while ((index = removed.nextSetBit(index + 1)) != -1) {
158            if (index == Constants.ADDR_AUDIO_SYSTEM) {
159                HdmiDeviceInfo avr = tv().getAvrDeviceInfo();
160                if (avr != null && tv().isConnected(avr.getPortId())) {
161                    ++mAvrStatusCount;
162                    Slog.w(TAG, "Ack not returned from AVR. count: " + mAvrStatusCount);
163                    if (mAvrStatusCount < AVR_COUNT_MAX) {
164                        continue;
165                    }
166                }
167            }
168            Slog.v(TAG, "Remove device by hot-plug detection:" + index);
169            removeDevice(index);
170        }
171
172        // Reset the counter if the ack is returned from AVR.
173        if (!removed.get(Constants.ADDR_AUDIO_SYSTEM)) {
174            mAvrStatusCount = 0;
175        }
176
177        // Next, check added devices.
178        BitSet added = complement(polledResult, currentInfos);
179        index = -1;
180        while ((index = added.nextSetBit(index + 1)) != -1) {
181            Slog.v(TAG, "Add device by hot-plug detection:" + index);
182            addDevice(index);
183        }
184    }
185
186    private static BitSet infoListToBitSet(List<HdmiDeviceInfo> infoList, boolean audioOnly) {
187        BitSet set = new BitSet(NUM_OF_ADDRESS);
188        for (HdmiDeviceInfo info : infoList) {
189            if (audioOnly) {
190                if (info.getDeviceType() == HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) {
191                    set.set(info.getLogicalAddress());
192                }
193            } else {
194                set.set(info.getLogicalAddress());
195            }
196        }
197        return set;
198    }
199
200    private static BitSet addressListToBitSet(List<Integer> list) {
201        BitSet set = new BitSet(NUM_OF_ADDRESS);
202        for (Integer value : list) {
203            set.set(value);
204        }
205        return set;
206    }
207
208    // A - B = A & ~B
209    private static BitSet complement(BitSet first, BitSet second) {
210        // Need to clone it so that it doesn't touch original set.
211        BitSet clone = (BitSet) first.clone();
212        clone.andNot(second);
213        return clone;
214    }
215
216    private void addDevice(int addedAddress) {
217        // Sending <Give Physical Address> will initiate new device action.
218        sendCommand(HdmiCecMessageBuilder.buildGivePhysicalAddress(getSourceAddress(),
219                addedAddress));
220    }
221
222    private void removeDevice(int removedAddress) {
223        mayChangeRoutingPath(removedAddress);
224        mayCancelDeviceSelect(removedAddress);
225        mayCancelOneTouchRecord(removedAddress);
226        mayDisableSystemAudioAndARC(removedAddress);
227
228        tv().removeCecDevice(removedAddress);
229    }
230
231    private void mayChangeRoutingPath(int address) {
232        HdmiDeviceInfo info = tv().getCecDeviceInfo(address);
233        if (info != null) {
234            tv().handleRemoveActiveRoutingPath(info.getPhysicalAddress());
235        }
236    }
237
238    private void mayCancelDeviceSelect(int address) {
239        List<DeviceSelectAction> actions = getActions(DeviceSelectAction.class);
240        if (actions.isEmpty()) {
241            return;
242        }
243
244        // Should have only one Device Select Action
245        DeviceSelectAction action = actions.get(0);
246        if (action.getTargetAddress() == address) {
247            removeAction(DeviceSelectAction.class);
248        }
249    }
250
251    private void mayCancelOneTouchRecord(int address) {
252        List<OneTouchRecordAction> actions = getActions(OneTouchRecordAction.class);
253        for (OneTouchRecordAction action : actions) {
254            if (action.getRecorderAddress() == address) {
255                removeAction(action);
256            }
257        }
258    }
259
260    private void mayDisableSystemAudioAndARC(int address) {
261        if (HdmiUtils.getTypeFromAddress(address) != HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) {
262            return;
263        }
264
265        // Turn off system audio mode and update settings.
266        tv().setSystemAudioMode(false, true);
267        if (tv().isArcEstablished()) {
268            tv().setAudioReturnChannel(false);
269            addAndStartAction(new RequestArcTerminationAction(localDevice(), address));
270        }
271    }
272}
273