content: /*
   * 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 &lt;Polling Message&gt;
   *   <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();
      }
  }
  