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