From d42a7a322b7adf532ae0b70cb9eb1df7e62a8f2d Mon Sep 17 00:00:00 2001 From: Jungshik Jang Date: Tue, 27 May 2014 13:27:36 +0900 Subject: [PATCH] DO NOT MERGE: Implement device discovery sequence. When device discover is launched it goes through the following step 1. clear all existing devices 2. send of all logical addresses excecpt one of local device 3. Once got all allocated logical addresses, gather physical address of them 4. Once got physical address of them, gather display name of them 5. Once got display names, gather vendor id of them 5. Once got vendor id of them, register all gathered info to internal device info list. Change-Id: Ic9aca3b15d88ac7650f10b6d0bfa9c97923975e8 --- .../server/hdmi/DeviceDiscoveryAction.java | 361 ++++++++++++++++++ .../server/hdmi/HdmiCecController.java | 35 +- .../server/hdmi/HdmiCecMessageBuilder.java | 11 + .../server/hdmi/HdmiControlService.java | 33 ++ 4 files changed, 434 insertions(+), 6 deletions(-) create mode 100644 services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java diff --git a/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java b/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java new file mode 100644 index 0000000000000..d46cc7bf25d00 --- /dev/null +++ b/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java @@ -0,0 +1,361 @@ +/* + * 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.HdmiCec; +import android.hardware.hdmi.HdmiCecDeviceInfo; +import android.hardware.hdmi.HdmiCecMessage; +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 TV device is woken from "Standby" state + * or enabled "Control for Hdmi" from disabled state. + * + *

Device discovery goes through the following steps. + *

    + *
  1. Poll all non-local devices by sending <Polling Message> + *
  2. Gather "Physical address" and "device type" of all acknowledged devices + *
  3. Gather "OSD (display) name" of all acknowledge devices + *
  4. Gather "Vendor id" of all acknowledge devices + *
+ */ +final class DeviceDiscoveryAction extends FeatureAction { + 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; + + private static final int DEVICE_POLLING_RETRY = 1; + + // TODO: Move this to common place + private static final int INVALID_PHYSICAL_ADDRESS = 0xFFFF; + + /** + * 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 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 = INVALID_PHYSICAL_ADDRESS; + private int mVendorId = HdmiCec.UNKNOWN_VENDOR_ID; + private String mDisplayName = ""; + private int mDeviceType = HdmiCec.DEVICE_INACTIVE; + + private DeviceInfo(int logicalAddress) { + mLogicalAddress = logicalAddress; + } + + private HdmiCecDeviceInfo toHdmiCecDeviceInfo() { + return new HdmiCecDeviceInfo(mLogicalAddress, mPhysicalAddress, mDeviceType, mVendorId, + mDisplayName); + } + } + + private final ArrayList mDevices = new ArrayList<>(); + private final DeviceDiscoveryCallback mCallback; + private int mProcessedDeviceCount = 0; + + /** + * @Constructor + * + * @param service + * @param sourceAddress + */ + DeviceDiscoveryAction(HdmiControlService service, int sourceAddress, + DeviceDiscoveryCallback callback) { + super(service, sourceAddress); + mCallback = Preconditions.checkNotNull(callback); + } + + @Override + boolean start() { + mDevices.clear(); + mState = STATE_WAITING_FOR_DEVICE_POLLING; + + mService.pollDevices(new DevicePollingCallback() { + @Override + public void onPollingFinished(List ackedAddress) { + if (ackedAddress.isEmpty()) { + Slog.i(TAG, "No device is detected."); + finish(); + return; + } + + Slog.i(TAG, "Device detected: " + ackedAddress); + allocateDevices(ackedAddress); + startPhysicalAddressStage(); + } + }, DEVICE_POLLING_RETRY); + return true; + } + + private void allocateDevices(List addresses) { + for (Integer i : addresses) { + DeviceInfo info = new DeviceInfo(i); + mDevices.add(info); + } + } + + private void startPhysicalAddressStage() { + mProcessedDeviceCount = 0; + mState = STATE_WAITING_FOR_PHYSICAL_ADDRESS; + + checkAndProceedStage(); + } + + private boolean verifyValidLogicalAddress(int address) { + return address >= HdmiCec.ADDR_TV && address < HdmiCec.ADDR_UNREGISTERED; + } + + private void queryPhysicalAddress(int address) { + if (!verifyValidLogicalAddress(address)) { + checkAndProceedStage(); + return; + } + + mActionTimer.clearTimerMessage(); + sendCommand(HdmiCecMessageBuilder.buildGivePhysicalAddress(mSourceAddress, address)); + addTimer(mState, TIMEOUT_MS); + } + + private void startOsdNameStage() { + mProcessedDeviceCount = 0; + mState = STATE_WAITING_FOR_OSD_NAME; + + checkAndProceedStage(); + } + + private void queryOsdName(int address) { + if (!verifyValidLogicalAddress(address)) { + checkAndProceedStage(); + return; + } + + mActionTimer.clearTimerMessage(); + sendCommand(HdmiCecMessageBuilder.buildGiveOsdNameCommand(mSourceAddress, address)); + addTimer(mState, TIMEOUT_MS); + } + + private void startVendorIdStage() { + mProcessedDeviceCount = 0; + mState = STATE_WAITING_FOR_VENDOR_ID; + + checkAndProceedStage(); + } + + private void queryVendorId(int address) { + if (!verifyValidLogicalAddress(address)) { + checkAndProceedStage(); + return; + } + + mActionTimer.clearTimerMessage(); + sendCommand(HdmiCecMessageBuilder.buildGiveDeviceVendorIdCommand(mSourceAddress, address)); + addTimer(mState, TIMEOUT_MS); + } + + @Override + boolean processCommand(HdmiCecMessage cmd) { + switch (mState) { + case STATE_WAITING_FOR_PHYSICAL_ADDRESS: + if (cmd.getOpcode() == HdmiCec.MESSAGE_REPORT_PHYSICAL_ADDRESS) { + handleReportPhysicalAddress(cmd); + return true; + } + return false; + case STATE_WAITING_FOR_OSD_NAME: + if (cmd.getOpcode() == HdmiCec.MESSAGE_SET_OSD_NAME) { + handleSetOsdName(cmd); + return true; + } + return false; + case STATE_WAITING_FOR_VENDOR_ID: + if (cmd.getOpcode() == HdmiCec.MESSAGE_DEVICE_VENDOR_ID) { + handleVendorId(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(); + if (params.length == 3) { + current.mPhysicalAddress = ((params[0] & 0xFF) << 8) | (params[1] & 0xFF); + current.mDeviceType = params[2] & 0xFF; + + increaseProcessedDeviceCount(); + checkAndProceedStage(); + } else { + // Physical address is a critical element in device info. + // If failed, remove device from device list and proceed to the next device. + removeDevice(mProcessedDeviceCount); + checkAndProceedStage(); + } + } + + 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 { + 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 = HdmiCec.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; + } + + byte[] params = cmd.getParams(); + if (params.length == 3) { + int vendorId = ((params[0] & 0xFF) << 16) + | ((params[1] & 0xFF) << 8) + | (params[2] & 0xFF); + current.mVendorId = vendorId; + } else { + Slog.w(TAG, "Invalid vendor id: " + cmd.toString()); + } + increaseProcessedDeviceCount(); + checkAndProceedStage(); + } + + private void increaseProcessedDeviceCount() { + mProcessedDeviceCount++; + } + + private void removeDevice(int index) { + mDevices.remove(index); + } + + private void wrapUpAndFinish() { + ArrayList result = new ArrayList<>(); + for (DeviceInfo info : mDevices) { + HdmiCecDeviceInfo cecDeviceInfo = info.toHdmiCecDeviceInfo(); + result.add(cecDeviceInfo); + } + mCallback.onDeviceDiscoveryDone(result); + finish(); + } + + 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: + wrapUpAndFinish(); + return; + default: + return; + } + } else { + int address = mDevices.get(mProcessedDeviceCount).mLogicalAddress; + switch (mState) { + 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); + default: + return; + } + } + } + + @Override + void handleTimerEvent(int state) { + if (mState == STATE_NONE || mState != state) { + return; + } + + removeDevice(mProcessedDeviceCount); + checkAndProceedStage(); + } +} diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java index 19fdaf455a61b..292f1cd49ee68 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecController.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java @@ -260,6 +260,16 @@ final class HdmiCecController { return deviceInfo; } + /** + * Clear all device info. + * + *

Declared as package-private. accessed by {@link HdmiControlService} only. + */ + void clearDeviceInfoList() { + assertRunOnServiceThread(); + mDeviceInfos.clear(); + } + /** * Return a list of all {@link HdmiCecDeviceInfo}. * @@ -267,12 +277,7 @@ final class HdmiCecController { */ List getDeviceInfoList() { assertRunOnServiceThread(); - - List deviceInfoList = new ArrayList<>(mDeviceInfos.size()); - for (int i = 0; i < mDeviceInfos.size(); ++i) { - deviceInfoList.add(mDeviceInfos.valueAt(i)); - } - return deviceInfoList; + return sparseArrayToList(mDeviceInfos); } /** @@ -389,6 +394,24 @@ final class HdmiCecController { runDevicePolling(pollingCandidates, retryCount, callback); } + /** + * Return a list of all {@link HdmiCecLocalDevice}s. + * + *

Declared as package-private. accessed by {@link HdmiControlService} only. + */ + List getLocalDeviceList() { + assertRunOnServiceThread(); + return sparseArrayToList(mLocalDevices); + } + + private static List sparseArrayToList(SparseArray array) { + ArrayList list = new ArrayList<>(); + for (int i = 0; i < array.size(); ++i) { + list.add(array.valueAt(i)); + } + return list; + } + private boolean isAllocatedLocalDeviceAddress(int address) { for (int i = 0; i < mLocalDevices.size(); ++i) { if (mLocalDevices.valueAt(i).isAddressOf(address)) { diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java index 1da363b74ef1b..9a76734943d66 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java @@ -72,6 +72,17 @@ public class HdmiCecMessageBuilder { return buildCommand(src, dest, HdmiCec.MESSAGE_FEATURE_ABORT, params); } + /** + * Build <Give Physical Address> command. + * + * @param src source address of command + * @param dest destination address of command + * @return newly created {@link HdmiCecMessage} + */ + static HdmiCecMessage buildGivePhysicalAddress(int src, int dest) { + return buildCommand(src, dest, HdmiCec.MESSAGE_GIVE_PHYSICAL_ADDRESS); + } + /** * Build <Give Osd Name> command. * diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 475852f8edd28..2fc5e8829fb02 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -33,6 +33,7 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.server.SystemService; +import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback; import java.util.ArrayList; import java.util.Iterator; @@ -411,6 +412,38 @@ public final class HdmiControlService extends SystemService { } } + void addCecDevice(HdmiCecDeviceInfo info) { + mCecController.addDeviceInfo(info); + } + + // Launch device discovery sequence. + // It starts with clearing the existing device info list. + // Note that it assumes that logical address of all local devices is already allocated. + void launchDeviceDiscovery() { + // At first, clear all existing device infos. + mCecController.clearDeviceInfoList(); + + // TODO: check whether TV is one of local devices. + DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, HdmiCec.ADDR_TV, + new DeviceDiscoveryCallback() { + @Override + public void onDeviceDiscoveryDone(List deviceInfos) { + for (HdmiCecDeviceInfo info : deviceInfos) { + mCecController.addDeviceInfo(info); + } + + // Add device info of all local devices. + for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { + mCecController.addDeviceInfo(device.getDeviceInfo()); + } + + // TODO: start hot-plug detection sequence here. + // addAndStartAction(new HotplugDetectionAction()); + } + }); + addAndStartAction(action); + } + private void enforceAccessPermission() { getContext().enforceCallingOrSelfPermission(PERMISSION, TAG); }