content: /*
   * Copyright (C) 2018 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 static com.android.internal.os.RoSystemProperties.PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH;
  
  import android.hardware.hdmi.HdmiControlManager;
  import android.hardware.hdmi.HdmiPortInfo;
  import android.hardware.hdmi.IHdmiControlCallback;
  import android.os.SystemProperties;
  import android.util.Slog;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.server.hdmi.Constants.LocalActivePort;
  import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
  
  import java.util.List;
  
  /**
   * Represent a logical source device residing in Android system.
   */
  abstract class HdmiCecLocalDeviceSource extends HdmiCecLocalDevice {
  
      private static final String TAG = "HdmiCecLocalDeviceSource";
  
      // Indicate if current device is Active Source or not
      @VisibleForTesting
      protected boolean mIsActiveSource = false;
  
      // Device has cec switch functionality or not.
      // Default is false.
      protected boolean mIsSwitchDevice = SystemProperties.getBoolean(
              PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH, false);
  
      // Routing port number used for Routing Control.
      // This records the default routing port or the previous valid routing port.
      // Default is HOME input.
      // Note that we don't save active path here because for source device,
      // new Active Source physical address might not match the active path
      @GuardedBy("mLock")
      @LocalActivePort
      private int mRoutingPort = Constants.CEC_SWITCH_HOME;
  
      // This records the current input of the device.
      // When device is switched to ARC input, mRoutingPort does not record it
      // since it's not an HDMI port used for Routing Control.
      // mLocalActivePort will record whichever input we switch to to keep tracking on
      // the current input status of the device.
      // This can help prevent duplicate switching and provide status information.
      @GuardedBy("mLock")
      @LocalActivePort
      protected int mLocalActivePort = Constants.CEC_SWITCH_HOME;
  
      // Whether the Routing Coutrol feature is enabled or not. False by default.
      @GuardedBy("mLock")
      protected boolean mRoutingControlFeatureEnabled;
  
      protected HdmiCecLocalDeviceSource(HdmiControlService service, int deviceType) {
          super(service, deviceType);
      }
  
      @Override
      @ServiceThreadOnly
      void onHotplug(int portId, boolean connected) {
          assertRunOnServiceThread();
          if (mService.getPortInfo(portId).getType() == HdmiPortInfo.PORT_OUTPUT) {
              mCecMessageCache.flushAll();
          }
          // We'll not clear mIsActiveSource on the hotplug event to pass CETC 11.2.2-2 ~ 3.
          if (connected) {
              mService.wakeUp();
          }
      }
  
      @Override
      @ServiceThreadOnly
      protected void sendStandby(int deviceId) {
          assertRunOnServiceThread();
  
          // Send standby to TV only for now
          int targetAddress = Constants.ADDR_TV;
          mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(mAddress, targetAddress));
      }
  
      @ServiceThreadOnly
      void oneTouchPlay(IHdmiControlCallback callback) {
          assertRunOnServiceThread();
          List<OneTouchPlayAction> actions = getActions(OneTouchPlayAction.class);
          if (!actions.isEmpty()) {
              Slog.i(TAG, "oneTouchPlay already in progress");
              actions.get(0).addCallback(callback);
              return;
          }
          OneTouchPlayAction action = OneTouchPlayAction.create(this, Constants.ADDR_TV,
                  callback);
          if (action == null) {
              Slog.w(TAG, "Cannot initiate oneTouchPlay");
              invokeCallback(callback, HdmiControlManager.RESULT_EXCEPTION);
              return;
          }
          addAndStartAction(action);
      }
  
      @ServiceThreadOnly
      protected boolean handleActiveSource(HdmiCecMessage message) {
          assertRunOnServiceThread();
          int logicalAddress = message.getSource();
          int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
          ActiveSource activeSource = ActiveSource.of(logicalAddress, physicalAddress);
          if (!getActiveSource().equals(activeSource)) {
              setActiveSource(activeSource);
          }
          setIsActiveSource(physicalAddress == mService.getPhysicalAddress());
          updateDevicePowerStatus(logicalAddress, HdmiControlManager.POWER_STATUS_ON);
          if (isRoutingControlFeatureEnabled()) {
              switchInputOnReceivingNewActivePath(physicalAddress);
          }
          return true;
      }
  
      @Override
      @ServiceThreadOnly
      protected boolean handleRequestActiveSource(HdmiCecMessage message) {
          assertRunOnServiceThread();
          maySendActiveSource(message.getSource());
          return true;
      }
  
      @Override
      @ServiceThreadOnly
      protected boolean handleSetStreamPath(HdmiCecMessage message) {
          assertRunOnServiceThread();
          int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
          // If current device is the target path, set to Active Source.
          // If the path is under the current device, should switch
          if (physicalAddress == mService.getPhysicalAddress() && mService.isPlaybackDevice()) {
              setAndBroadcastActiveSource(message, physicalAddress);
          }
          switchInputOnReceivingNewActivePath(physicalAddress);
          return true;
      }
  
      @Override
      @ServiceThreadOnly
      protected boolean handleRoutingChange(HdmiCecMessage message) {
          assertRunOnServiceThread();
          if (!isRoutingControlFeatureEnabled()) {
              mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
              return true;
          }
          int newPath = HdmiUtils.twoBytesToInt(message.getParams(), 2);
          // if the current device is a pure playback device
          if (!mIsSwitchDevice
                  && newPath == mService.getPhysicalAddress()
                  && mService.isPlaybackDevice()) {
              setAndBroadcastActiveSource(message, newPath);
          }
          handleRoutingChangeAndInformation(newPath, message);
          return true;
      }
  
      @Override
      @ServiceThreadOnly
      protected boolean handleRoutingInformation(HdmiCecMessage message) {
          assertRunOnServiceThread();
          if (!isRoutingControlFeatureEnabled()) {
              mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
              return true;
          }
          int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
          // if the current device is a pure playback device
          if (!mIsSwitchDevice
                  && physicalAddress == mService.getPhysicalAddress()
                  && mService.isPlaybackDevice()) {
              setAndBroadcastActiveSource(message, physicalAddress);
          }
          handleRoutingChangeAndInformation(physicalAddress, message);
          return true;
      }
  
      // Method to switch Input with the new Active Path.
      // All the devices with Switch functionality should implement this.
      protected void switchInputOnReceivingNewActivePath(int physicalAddress) {
          // do nothing
      }
  
      // Source device with Switch functionality should implement this method.
      // TODO(): decide which type will handle the routing when multi device type is supported
      protected void handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message) {
          // do nothing
      }
  
      // Update the power status of the devices connected to the current device.
      // This only works if the current device is a switch and keeps tracking the device info
      // of the device connected to it.
      protected void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) {
          // do nothing
      }
  
      // Active source claiming needs to be handled in Service
      // since service can decide who will be the active source when the device supports
      // multiple device types in this method.
      // This method should only be called when the device can be the active source.
      protected void setAndBroadcastActiveSource(HdmiCecMessage message, int physicalAddress) {
          mService.setAndBroadcastActiveSource(
                  physicalAddress, getDeviceInfo().getDeviceType(), message.getSource());
      }
  
      @ServiceThreadOnly
      void setIsActiveSource(boolean on) {
          assertRunOnServiceThread();
          mIsActiveSource = on;
      }
  
      protected void wakeUpIfActiveSource() {
          if (!mIsActiveSource) {
              return;
          }
          // Wake up the device
          mService.wakeUp();
          return;
      }
  
      protected void maySendActiveSource(int dest) {
          if (mIsActiveSource) {
              mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(
                      mAddress, mService.getPhysicalAddress()));
          }
      }
  
      /**
       * Set {@link #mRoutingPort} to a specific {@link LocalActivePort} to record the current active
       * CEC Routing Control related port.
       *
       * @param portId The portId of the new routing port.
       */
      @VisibleForTesting
      protected void setRoutingPort(@LocalActivePort int portId) {
          synchronized (mLock) {
              mRoutingPort = portId;
          }
      }
  
      /**
       * Get {@link #mRoutingPort}. This is useful when the device needs to route to the last valid
       * routing port.
       */
      @LocalActivePort
      protected int getRoutingPort() {
          synchronized (mLock) {
              return mRoutingPort;
          }
      }
  
      /**
       * Get {@link #mLocalActivePort}. This is useful when device needs to know the current active
       * port.
       */
      @LocalActivePort
      protected int getLocalActivePort() {
          synchronized (mLock) {
              return mLocalActivePort;
          }
      }
  
      /**
       * Set {@link #mLocalActivePort} to a specific {@link LocalActivePort} to record the current
       * active port.
       *
       * <p>It does not have to be a Routing Control related port. For example it can be
       * set to {@link Constants#CEC_SWITCH_ARC} but this port is System Audio related.
       *
       * @param activePort The portId of the new active port.
       */
      protected void setLocalActivePort(@LocalActivePort int activePort) {
          synchronized (mLock) {
              mLocalActivePort = activePort;
          }
      }
  
      boolean isRoutingControlFeatureEnabled() {
          synchronized (mLock) {
              return mRoutingControlFeatureEnabled;
          }
      }
  
      // Check if the device is trying to switch to the same input that is active right now.
      // This can help avoid redundant port switching.
      protected boolean isSwitchingToTheSameInput(@LocalActivePort int activePort) {
          return activePort == getLocalActivePort();
      }
  }
  