/*
|
* 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.HdmiControlManager;
|
import android.hardware.hdmi.HdmiDeviceInfo;
|
import android.util.Slog;
|
|
import com.android.internal.util.Preconditions;
|
import com.android.server.hdmi.HdmiControlService.DevicePollingCallback;
|
|
import java.io.UnsupportedEncodingException;
|
import java.util.ArrayList;
|
import java.util.List;
|
|
/**
|
* Feature action that handles device discovery sequences.
|
* Device discovery is launched when device is woken from "Standby" state
|
* or enabled "Control for Hdmi" from disabled state.
|
*
|
* <p>Device discovery goes through the following steps.
|
* <ol>
|
* <li>Poll all non-local devices by sending <Polling Message>
|
* <li>Gather "Physical address" and "device type" of all acknowledged devices
|
* <li>Gather "OSD (display) name" of all acknowledge devices
|
* <li>Gather "Vendor id" of all acknowledge devices
|
* </ol>
|
* We attempt to get OSD name/vendor ID up to 5 times in case the communication fails.
|
*/
|
final class DeviceDiscoveryAction extends HdmiCecFeatureAction {
|
private static final String TAG = "DeviceDiscoveryAction";
|
|
// State in which the action is waiting for device polling.
|
private static final int STATE_WAITING_FOR_DEVICE_POLLING = 1;
|
// State in which the action is waiting for gathering physical address of non-local devices.
|
private static final int STATE_WAITING_FOR_PHYSICAL_ADDRESS = 2;
|
// State in which the action is waiting for gathering osd name of non-local devices.
|
private static final int STATE_WAITING_FOR_OSD_NAME = 3;
|
// State in which the action is waiting for gathering vendor id of non-local devices.
|
private static final int STATE_WAITING_FOR_VENDOR_ID = 4;
|
// State in which the action is waiting for devices to be ready.
|
private static final int STATE_WAITING_FOR_DEVICES = 5;
|
// State in which the action is waiting for gathering power status of non-local devices.
|
private static final int STATE_WAITING_FOR_POWER = 6;
|
|
/**
|
* Interface used to report result of device discovery.
|
*/
|
interface DeviceDiscoveryCallback {
|
/**
|
* Called when device discovery is done.
|
*
|
* @param deviceInfos a list of all non-local devices. It can be empty list.
|
*/
|
void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos);
|
}
|
|
// An internal container used to keep track of device information during
|
// this action.
|
private static final class DeviceInfo {
|
private final int mLogicalAddress;
|
|
private int mPhysicalAddress = Constants.INVALID_PHYSICAL_ADDRESS;
|
private int mPortId = Constants.INVALID_PORT_ID;
|
private int mVendorId = Constants.UNKNOWN_VENDOR_ID;
|
private int mPowerStatus = HdmiControlManager.POWER_STATUS_UNKNOWN;
|
private String mDisplayName = "";
|
private int mDeviceType = HdmiDeviceInfo.DEVICE_INACTIVE;
|
|
private DeviceInfo(int logicalAddress) {
|
mLogicalAddress = logicalAddress;
|
}
|
|
private HdmiDeviceInfo toHdmiDeviceInfo() {
|
return new HdmiDeviceInfo(mLogicalAddress, mPhysicalAddress, mPortId, mDeviceType,
|
mVendorId, mDisplayName, mPowerStatus);
|
}
|
}
|
|
private final ArrayList<DeviceInfo> mDevices = new ArrayList<>();
|
private final DeviceDiscoveryCallback mCallback;
|
private int mProcessedDeviceCount = 0;
|
private int mTimeoutRetry = 0;
|
private boolean mIsTvDevice = localDevice().mService.isTvDevice();
|
private final int mDelayPeriod;
|
|
/**
|
* Constructor.
|
*
|
* @param source an instance of {@link HdmiCecLocalDevice}.
|
* @param delay delay action for this period between query Physical Address and polling
|
*/
|
DeviceDiscoveryAction(HdmiCecLocalDevice source, DeviceDiscoveryCallback callback, int delay) {
|
super(source);
|
mCallback = Preconditions.checkNotNull(callback);
|
mDelayPeriod = delay;
|
}
|
|
/**
|
* Constructor.
|
*
|
* @param source an instance of {@link HdmiCecLocalDevice}.
|
*/
|
DeviceDiscoveryAction(HdmiCecLocalDevice source, DeviceDiscoveryCallback callback) {
|
this(source, callback, 0);
|
}
|
|
@Override
|
boolean start() {
|
mDevices.clear();
|
mState = STATE_WAITING_FOR_DEVICE_POLLING;
|
|
pollDevices(new DevicePollingCallback() {
|
@Override
|
public void onPollingFinished(List<Integer> ackedAddress) {
|
if (ackedAddress.isEmpty()) {
|
Slog.v(TAG, "No device is detected.");
|
wrapUpAndFinish();
|
return;
|
}
|
|
Slog.v(TAG, "Device detected: " + ackedAddress);
|
allocateDevices(ackedAddress);
|
if (mDelayPeriod > 0) {
|
startToDelayAction();
|
} else {
|
startPhysicalAddressStage();
|
}
|
}
|
}, Constants.POLL_ITERATION_REVERSE_ORDER
|
| Constants.POLL_STRATEGY_REMOTES_DEVICES, HdmiConfig.DEVICE_POLLING_RETRY);
|
return true;
|
}
|
|
private void allocateDevices(List<Integer> addresses) {
|
for (Integer i : addresses) {
|
DeviceInfo info = new DeviceInfo(i);
|
mDevices.add(info);
|
}
|
}
|
|
private void startToDelayAction() {
|
Slog.v(TAG, "Waiting for connected devices to be ready");
|
mState = STATE_WAITING_FOR_DEVICES;
|
|
checkAndProceedStage();
|
}
|
|
private void startPhysicalAddressStage() {
|
Slog.v(TAG, "Start [Physical Address Stage]:" + mDevices.size());
|
mProcessedDeviceCount = 0;
|
mState = STATE_WAITING_FOR_PHYSICAL_ADDRESS;
|
|
checkAndProceedStage();
|
}
|
|
private boolean verifyValidLogicalAddress(int address) {
|
return address >= Constants.ADDR_TV && address < Constants.ADDR_UNREGISTERED;
|
}
|
|
private void queryPhysicalAddress(int address) {
|
if (!verifyValidLogicalAddress(address)) {
|
checkAndProceedStage();
|
return;
|
}
|
|
mActionTimer.clearTimerMessage();
|
|
// Check cache first and send request if not exist.
|
if (mayProcessMessageIfCached(address, Constants.MESSAGE_REPORT_PHYSICAL_ADDRESS)) {
|
return;
|
}
|
sendCommand(HdmiCecMessageBuilder.buildGivePhysicalAddress(getSourceAddress(), address));
|
addTimer(mState, HdmiConfig.TIMEOUT_MS);
|
}
|
|
private void delayActionWithTimePeriod(int timeDelay) {
|
mActionTimer.clearTimerMessage();
|
addTimer(mState, timeDelay);
|
}
|
|
private void startOsdNameStage() {
|
Slog.v(TAG, "Start [Osd Name Stage]:" + mDevices.size());
|
mProcessedDeviceCount = 0;
|
mState = STATE_WAITING_FOR_OSD_NAME;
|
|
checkAndProceedStage();
|
}
|
|
private void queryOsdName(int address) {
|
if (!verifyValidLogicalAddress(address)) {
|
checkAndProceedStage();
|
return;
|
}
|
|
mActionTimer.clearTimerMessage();
|
|
if (mayProcessMessageIfCached(address, Constants.MESSAGE_SET_OSD_NAME)) {
|
return;
|
}
|
sendCommand(HdmiCecMessageBuilder.buildGiveOsdNameCommand(getSourceAddress(), address));
|
addTimer(mState, HdmiConfig.TIMEOUT_MS);
|
}
|
|
private void startVendorIdStage() {
|
Slog.v(TAG, "Start [Vendor Id Stage]:" + mDevices.size());
|
|
mProcessedDeviceCount = 0;
|
mState = STATE_WAITING_FOR_VENDOR_ID;
|
|
checkAndProceedStage();
|
}
|
|
private void queryVendorId(int address) {
|
if (!verifyValidLogicalAddress(address)) {
|
checkAndProceedStage();
|
return;
|
}
|
|
mActionTimer.clearTimerMessage();
|
|
if (mayProcessMessageIfCached(address, Constants.MESSAGE_DEVICE_VENDOR_ID)) {
|
return;
|
}
|
sendCommand(
|
HdmiCecMessageBuilder.buildGiveDeviceVendorIdCommand(getSourceAddress(), address));
|
addTimer(mState, HdmiConfig.TIMEOUT_MS);
|
}
|
|
private void startPowerStatusStage() {
|
Slog.v(TAG, "Start [Power Status Stage]:" + mDevices.size());
|
mProcessedDeviceCount = 0;
|
mState = STATE_WAITING_FOR_POWER;
|
|
checkAndProceedStage();
|
}
|
|
private void queryPowerStatus(int address) {
|
if (!verifyValidLogicalAddress(address)) {
|
checkAndProceedStage();
|
return;
|
}
|
|
mActionTimer.clearTimerMessage();
|
|
if (mayProcessMessageIfCached(address, Constants.MESSAGE_REPORT_POWER_STATUS)) {
|
return;
|
}
|
sendCommand(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(getSourceAddress(), address));
|
addTimer(mState, HdmiConfig.TIMEOUT_MS);
|
}
|
|
private boolean mayProcessMessageIfCached(int address, int opcode) {
|
HdmiCecMessage message = getCecMessageCache().getMessage(address, opcode);
|
if (message != null) {
|
processCommand(message);
|
return true;
|
}
|
return false;
|
}
|
|
@Override
|
boolean processCommand(HdmiCecMessage cmd) {
|
switch (mState) {
|
case STATE_WAITING_FOR_PHYSICAL_ADDRESS:
|
if (cmd.getOpcode() == Constants.MESSAGE_REPORT_PHYSICAL_ADDRESS) {
|
handleReportPhysicalAddress(cmd);
|
return true;
|
}
|
return false;
|
case STATE_WAITING_FOR_OSD_NAME:
|
if (cmd.getOpcode() == Constants.MESSAGE_SET_OSD_NAME) {
|
handleSetOsdName(cmd);
|
return true;
|
} else if ((cmd.getOpcode() == Constants.MESSAGE_FEATURE_ABORT) &&
|
((cmd.getParams()[0] & 0xFF) == Constants.MESSAGE_GIVE_OSD_NAME)) {
|
handleSetOsdName(cmd);
|
return true;
|
}
|
return false;
|
case STATE_WAITING_FOR_VENDOR_ID:
|
if (cmd.getOpcode() == Constants.MESSAGE_DEVICE_VENDOR_ID) {
|
handleVendorId(cmd);
|
return true;
|
} else if ((cmd.getOpcode() == Constants.MESSAGE_FEATURE_ABORT) &&
|
((cmd.getParams()[0] & 0xFF) == Constants.MESSAGE_GIVE_DEVICE_VENDOR_ID)) {
|
handleVendorId(cmd);
|
return true;
|
}
|
return false;
|
case STATE_WAITING_FOR_POWER:
|
if (cmd.getOpcode() == Constants.MESSAGE_REPORT_POWER_STATUS) {
|
handleReportPowerStatus(cmd);
|
return true;
|
} else if ((cmd.getOpcode() == Constants.MESSAGE_FEATURE_ABORT)
|
&& ((cmd.getParams()[0] & 0xFF) == Constants.MESSAGE_REPORT_POWER_STATUS)) {
|
handleReportPowerStatus(cmd);
|
return true;
|
}
|
return false;
|
case STATE_WAITING_FOR_DEVICE_POLLING:
|
// Fall through.
|
default:
|
return false;
|
}
|
}
|
|
private void handleReportPhysicalAddress(HdmiCecMessage cmd) {
|
Preconditions.checkState(mProcessedDeviceCount < mDevices.size());
|
|
DeviceInfo current = mDevices.get(mProcessedDeviceCount);
|
if (current.mLogicalAddress != cmd.getSource()) {
|
Slog.w(TAG, "Unmatched address[expected:" + current.mLogicalAddress + ", actual:" +
|
cmd.getSource());
|
return;
|
}
|
|
byte params[] = cmd.getParams();
|
current.mPhysicalAddress = HdmiUtils.twoBytesToInt(params);
|
current.mPortId = getPortId(current.mPhysicalAddress);
|
current.mDeviceType = params[2] & 0xFF;
|
current.mDisplayName = HdmiUtils.getDefaultDeviceName(current.mDeviceType);
|
|
// This is to manager CEC device separately in case they don't have address.
|
if (mIsTvDevice) {
|
tv().updateCecSwitchInfo(current.mLogicalAddress, current.mDeviceType,
|
current.mPhysicalAddress);
|
}
|
increaseProcessedDeviceCount();
|
checkAndProceedStage();
|
}
|
|
private int getPortId(int physicalAddress) {
|
return mIsTvDevice ? tv().getPortId(physicalAddress)
|
: source().getPortId(physicalAddress);
|
}
|
|
private void handleSetOsdName(HdmiCecMessage cmd) {
|
Preconditions.checkState(mProcessedDeviceCount < mDevices.size());
|
|
DeviceInfo current = mDevices.get(mProcessedDeviceCount);
|
if (current.mLogicalAddress != cmd.getSource()) {
|
Slog.w(TAG, "Unmatched address[expected:" + current.mLogicalAddress + ", actual:" +
|
cmd.getSource());
|
return;
|
}
|
|
String displayName = null;
|
try {
|
if (cmd.getOpcode() == Constants.MESSAGE_FEATURE_ABORT) {
|
displayName = HdmiUtils.getDefaultDeviceName(current.mLogicalAddress);
|
} else {
|
displayName = new String(cmd.getParams(), "US-ASCII");
|
}
|
} catch (UnsupportedEncodingException e) {
|
Slog.w(TAG, "Failed to decode display name: " + cmd.toString());
|
// If failed to get display name, use the default name of device.
|
displayName = HdmiUtils.getDefaultDeviceName(current.mLogicalAddress);
|
}
|
current.mDisplayName = displayName;
|
increaseProcessedDeviceCount();
|
checkAndProceedStage();
|
}
|
|
private void handleVendorId(HdmiCecMessage cmd) {
|
Preconditions.checkState(mProcessedDeviceCount < mDevices.size());
|
|
DeviceInfo current = mDevices.get(mProcessedDeviceCount);
|
if (current.mLogicalAddress != cmd.getSource()) {
|
Slog.w(TAG, "Unmatched address[expected:" + current.mLogicalAddress + ", actual:" +
|
cmd.getSource());
|
return;
|
}
|
|
if (cmd.getOpcode() != Constants.MESSAGE_FEATURE_ABORT) {
|
byte[] params = cmd.getParams();
|
int vendorId = HdmiUtils.threeBytesToInt(params);
|
current.mVendorId = vendorId;
|
}
|
|
increaseProcessedDeviceCount();
|
checkAndProceedStage();
|
}
|
|
private void handleReportPowerStatus(HdmiCecMessage cmd) {
|
Preconditions.checkState(mProcessedDeviceCount < mDevices.size());
|
|
DeviceInfo current = mDevices.get(mProcessedDeviceCount);
|
if (current.mLogicalAddress != cmd.getSource()) {
|
Slog.w(TAG, "Unmatched address[expected:" + current.mLogicalAddress + ", actual:"
|
+ cmd.getSource());
|
return;
|
}
|
|
if (cmd.getOpcode() != Constants.MESSAGE_FEATURE_ABORT) {
|
byte[] params = cmd.getParams();
|
int powerStatus = params[0] & 0xFF;
|
current.mPowerStatus = powerStatus;
|
}
|
|
increaseProcessedDeviceCount();
|
checkAndProceedStage();
|
}
|
|
private void increaseProcessedDeviceCount() {
|
mProcessedDeviceCount++;
|
mTimeoutRetry = 0;
|
}
|
|
private void removeDevice(int index) {
|
mDevices.remove(index);
|
}
|
|
private void wrapUpAndFinish() {
|
Slog.v(TAG, "---------Wrap up Device Discovery:[" + mDevices.size() + "]---------");
|
ArrayList<HdmiDeviceInfo> result = new ArrayList<>();
|
for (DeviceInfo info : mDevices) {
|
HdmiDeviceInfo cecDeviceInfo = info.toHdmiDeviceInfo();
|
Slog.v(TAG, " DeviceInfo: " + cecDeviceInfo);
|
result.add(cecDeviceInfo);
|
}
|
Slog.v(TAG, "--------------------------------------------");
|
mCallback.onDeviceDiscoveryDone(result);
|
finish();
|
// Process any commands buffered while device discovery action was in progress.
|
if (mIsTvDevice) {
|
tv().processAllDelayedMessages();
|
}
|
}
|
|
private void checkAndProceedStage() {
|
if (mDevices.isEmpty()) {
|
wrapUpAndFinish();
|
return;
|
}
|
|
// If finished current stage, move on to next stage.
|
if (mProcessedDeviceCount == mDevices.size()) {
|
mProcessedDeviceCount = 0;
|
switch (mState) {
|
case STATE_WAITING_FOR_PHYSICAL_ADDRESS:
|
startOsdNameStage();
|
return;
|
case STATE_WAITING_FOR_OSD_NAME:
|
startVendorIdStage();
|
return;
|
case STATE_WAITING_FOR_VENDOR_ID:
|
startPowerStatusStage();
|
return;
|
case STATE_WAITING_FOR_POWER:
|
wrapUpAndFinish();
|
return;
|
default:
|
return;
|
}
|
} else {
|
sendQueryCommand();
|
}
|
}
|
|
private void sendQueryCommand() {
|
int address = mDevices.get(mProcessedDeviceCount).mLogicalAddress;
|
switch (mState) {
|
case STATE_WAITING_FOR_DEVICES:
|
delayActionWithTimePeriod(mDelayPeriod);
|
return;
|
case STATE_WAITING_FOR_PHYSICAL_ADDRESS:
|
queryPhysicalAddress(address);
|
return;
|
case STATE_WAITING_FOR_OSD_NAME:
|
queryOsdName(address);
|
return;
|
case STATE_WAITING_FOR_VENDOR_ID:
|
queryVendorId(address);
|
return;
|
case STATE_WAITING_FOR_POWER:
|
queryPowerStatus(address);
|
return;
|
default:
|
return;
|
}
|
}
|
|
@Override
|
void handleTimerEvent(int state) {
|
if (mState == STATE_NONE || mState != state) {
|
return;
|
}
|
|
if (mState == STATE_WAITING_FOR_DEVICES) {
|
startPhysicalAddressStage();
|
return;
|
}
|
if (++mTimeoutRetry < HdmiConfig.TIMEOUT_RETRY) {
|
sendQueryCommand();
|
return;
|
}
|
mTimeoutRetry = 0;
|
Slog.v(TAG, "Timeout[State=" + mState + ", Processed=" + mProcessedDeviceCount);
|
if (mState != STATE_WAITING_FOR_POWER && mState != STATE_WAITING_FOR_OSD_NAME) {
|
// We don't need to remove the device info if the power status is unknown.
|
// Some device does not have preferred OSD name and does not respond to Give OSD name.
|
// Like LG TV. We can give it default device name and not remove it.
|
removeDevice(mProcessedDeviceCount);
|
} else {
|
increaseProcessedDeviceCount();
|
}
|
checkAndProceedStage();
|
}
|
}
|