/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.hdmi;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.util.Slog;
import com.android.server.hdmi.HdmiControlService.DevicePollingCallback;
import java.util.BitSet;
import java.util.List;
/**
* Feature action that handles hot-plug detection mechanism.
* Hot-plug event is initiated by timer after device discovery action.
*
* <p>Check all devices every 15 secs except for system audio.
* If system audio is on, check hot-plug for audio system every 5 secs.
* For other devices, keep 15 secs period.
*/
// Seq #3
final class HotplugDetectionAction extends HdmiCecFeatureAction {
private static final String TAG = "HotPlugDetectionAction";
private static final int POLLING_INTERVAL_MS = 5000;
private static final int TIMEOUT_COUNT = 3;
private static final int AVR_COUNT_MAX = 3;
// State in which waits for next polling
private static final int STATE_WAIT_FOR_NEXT_POLLING = 1;
// All addresses except for broadcast (unregistered address).
private static final int NUM_OF_ADDRESS = Constants.ADDR_SPECIFIC_USE
- Constants.ADDR_TV + 1;
private int mTimeoutCount = 0;
// Counter used to ensure the connection to AVR is stable. Occasional failure to get
// polling response from AVR despite its presence leads to unstable status flipping.
// This is a workaround to deal with it, by removing the device only if the removal
// is detected {@code AVR_COUNT_MAX} times in a row.
private int mAvrStatusCount = 0;
/**
* Constructor
*
* @param source {@link HdmiCecLocalDevice} instance
*/
HotplugDetectionAction(HdmiCecLocalDevice source) {
super(source);
}
@Override
boolean start() {
Slog.v(TAG, "Hot-plug dection started.");
mState = STATE_WAIT_FOR_NEXT_POLLING;
mTimeoutCount = 0;
// Start timer without polling.
// The first check for all devices will be initiated 15 seconds later.
addTimer(mState, POLLING_INTERVAL_MS);
return true;
}
@Override
boolean processCommand(HdmiCecMessage cmd) {
// No-op
return false;
}
@Override
void handleTimerEvent(int state) {
if (mState != state) {
return;
}
if (mState == STATE_WAIT_FOR_NEXT_POLLING) {
mTimeoutCount = (mTimeoutCount + 1) % TIMEOUT_COUNT;
pollDevices();
}
}
/**
* Start device polling immediately.
*/
void pollAllDevicesNow() {
// Clear existing timer to avoid overlapped execution
mActionTimer.clearTimerMessage();
mTimeoutCount = 0;
mState = STATE_WAIT_FOR_NEXT_POLLING;
pollAllDevices();
addTimer(mState, POLLING_INTERVAL_MS);
}
// This method is called every 5 seconds.
private void pollDevices() {
// All device check called every 15 seconds.
if (mTimeoutCount == 0) {
pollAllDevices();
} else {
if (tv().isSystemAudioActivated()) {
pollAudioSystem();
}
}
addTimer(mState, POLLING_INTERVAL_MS);
}
private void pollAllDevices() {
Slog.v(TAG, "Poll all devices.");
pollDevices(new DevicePollingCallback() {
@Override
public void onPollingFinished(List<Integer> ackedAddress) {
checkHotplug(ackedAddress, false);
}
}, Constants.POLL_ITERATION_IN_ORDER
| Constants.POLL_STRATEGY_REMOTES_DEVICES, HdmiConfig.HOTPLUG_DETECTION_RETRY);
}
private void pollAudioSystem() {
Slog.v(TAG, "Poll audio system.");
pollDevices(new DevicePollingCallback() {
@Override
public void onPollingFinished(List<Integer> ackedAddress) {
checkHotplug(ackedAddress, true);
}
}, Constants.POLL_ITERATION_IN_ORDER
| Constants.POLL_STRATEGY_SYSTEM_AUDIO, HdmiConfig.HOTPLUG_DETECTION_RETRY);
}
private void checkHotplug(List<Integer> ackedAddress, boolean audioOnly) {
BitSet currentInfos = infoListToBitSet(tv().getDeviceInfoList(false), audioOnly);
BitSet polledResult = addressListToBitSet(ackedAddress);
// At first, check removed devices.
BitSet removed = complement(currentInfos, polledResult);
int index = -1;
while ((index = removed.nextSetBit(index + 1)) != -1) {
if (index == Constants.ADDR_AUDIO_SYSTEM) {
HdmiDeviceInfo avr = tv().getAvrDeviceInfo();
if (avr != null && tv().isConnected(avr.getPortId())) {
++mAvrStatusCount;
Slog.w(TAG, "Ack not returned from AVR. count: " + mAvrStatusCount);
if (mAvrStatusCount < AVR_COUNT_MAX) {
continue;
}
}
}
Slog.v(TAG, "Remove device by hot-plug detection:" + index);
removeDevice(index);
}
// Reset the counter if the ack is returned from AVR.
if (!removed.get(Constants.ADDR_AUDIO_SYSTEM)) {
mAvrStatusCount = 0;
}
// Next, check added devices.
BitSet added = complement(polledResult, currentInfos);
index = -1;
while ((index = added.nextSetBit(index + 1)) != -1) {
Slog.v(TAG, "Add device by hot-plug detection:" + index);
addDevice(index);
}
}
private static BitSet infoListToBitSet(List<HdmiDeviceInfo> infoList, boolean audioOnly) {
BitSet set = new BitSet(NUM_OF_ADDRESS);
for (HdmiDeviceInfo info : infoList) {
if (audioOnly) {
if (info.getDeviceType() == HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) {
set.set(info.getLogicalAddress());
}
} else {
set.set(info.getLogicalAddress());
}
}
return set;
}
private static BitSet addressListToBitSet(List<Integer> list) {
BitSet set = new BitSet(NUM_OF_ADDRESS);
for (Integer value : list) {
set.set(value);
}
return set;
}
// A - B = A & ~B
private static BitSet complement(BitSet first, BitSet second) {
// Need to clone it so that it doesn't touch original set.
BitSet clone = (BitSet) first.clone();
clone.andNot(second);
return clone;
}
private void addDevice(int addedAddress) {
// Sending <Give Physical Address> will initiate new device action.
sendCommand(HdmiCecMessageBuilder.buildGivePhysicalAddress(getSourceAddress(),
addedAddress));
}
private void removeDevice(int removedAddress) {
mayChangeRoutingPath(removedAddress);
mayCancelDeviceSelect(removedAddress);
mayCancelOneTouchRecord(removedAddress);
mayDisableSystemAudioAndARC(removedAddress);
tv().removeCecDevice(removedAddress);
}
private void mayChangeRoutingPath(int address) {
HdmiDeviceInfo info = tv().getCecDeviceInfo(address);
if (info != null) {
tv().handleRemoveActiveRoutingPath(info.getPhysicalAddress());
}
}
private void mayCancelDeviceSelect(int address) {
List<DeviceSelectAction> actions = getActions(DeviceSelectAction.class);
if (actions.isEmpty()) {
return;
}
// Should have only one Device Select Action
DeviceSelectAction action = actions.get(0);
if (action.getTargetAddress() == address) {
removeAction(DeviceSelectAction.class);
}
}
private void mayCancelOneTouchRecord(int address) {
List<OneTouchRecordAction> actions = getActions(OneTouchRecordAction.class);
for (OneTouchRecordAction action : actions) {
if (action.getRecorderAddress() == address) {
removeAction(action);
}
}
}
private void mayDisableSystemAudioAndARC(int address) {
if (HdmiUtils.getTypeFromAddress(address) != HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) {
return;
}
// Turn off system audio mode and update settings.
tv().setSystemAudioMode(false, true);
if (tv().isArcEstablished()) {
tv().setAudioReturnChannel(false);
addAndStartAction(new RequestArcTerminationAction(localDevice(), address));
}
}
}