/* * 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. * *

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 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 ackedAddress) { checkHotplug(ackedAddress, true); } }, Constants.POLL_ITERATION_IN_ORDER | Constants.POLL_STRATEGY_SYSTEM_AUDIO, HdmiConfig.HOTPLUG_DETECTION_RETRY); } private void checkHotplug(List 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 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 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 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 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 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; } tv().setSystemAudioMode(false); if (tv().isArcEstablished()) { tv().enableAudioReturnChannel(false); addAndStartAction(new RequestArcTerminationAction(localDevice(), address)); } } }