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 
17 package com.android.server.hdmi;
18 
19 import android.hardware.hdmi.HdmiDeviceInfo;
20 import android.util.Slog;
21 
22 import com.android.server.hdmi.HdmiControlService.DevicePollingCallback;
23 
24 import java.util.BitSet;
25 import 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
36 final 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      */
HotplugDetectionAction(HdmiCecLocalDevice source)63     HotplugDetectionAction(HdmiCecLocalDevice source) {
64         super(source);
65     }
66 
67     @Override
start()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
processCommand(HdmiCecMessage cmd)81     boolean processCommand(HdmiCecMessage cmd) {
82         // No-op
83         return false;
84     }
85 
86     @Override
handleTimerEvent(int state)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      */
pollAllDevicesNow()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.
pollDevices()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 
pollAllDevices()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 
pollAudioSystem()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 
checkHotplug(List<Integer> ackedAddress, boolean audioOnly)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 
infoListToBitSet(List<HdmiDeviceInfo> infoList, boolean audioOnly)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 
addressListToBitSet(List<Integer> list)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
complement(BitSet first, BitSet second)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 
addDevice(int addedAddress)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 
removeDevice(int removedAddress)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 
mayChangeRoutingPath(int address)231     private void mayChangeRoutingPath(int address) {
232         HdmiDeviceInfo info = tv().getCecDeviceInfo(address);
233         if (info != null) {
234             tv().handleRemoveActiveRoutingPath(info.getPhysicalAddress());
235         }
236     }
237 
mayCancelDeviceSelect(int address)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 
mayCancelOneTouchRecord(int address)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 
mayDisableSystemAudioAndARC(int address)260     private void mayDisableSystemAudioAndARC(int address) {
261         if (HdmiUtils.getTypeFromAddress(address) != HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) {
262             return;
263         }
264 
265         tv().setSystemAudioMode(false);
266         if (tv().isArcEstablished()) {
267             tv().enableAudioReturnChannel(false);
268             addAndStartAction(new RequestArcTerminationAction(localDevice(), address));
269         }
270     }
271 }
272