| /* | |
| * 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; | |
| } | |
| tv().setSystemAudioMode(false); | |
| if (tv().isArcEstablished()) { | |
| tv().enableAudioReturnChannel(false); | |
| addAndStartAction(new RequestArcTerminationAction(localDevice(), address)); | |
| } | |
| } | |
| } |