content: /*
   * Copyright (C) 2019 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.display.whitebalance;
  
  import android.annotation.NonNull;
  import android.util.Slog;
  import android.util.Spline;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.Preconditions;
  import com.android.server.LocalServices;
  import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
  import com.android.server.display.utils.History;
  
  import java.io.PrintWriter;
  
  /**
   * The DisplayWhiteBalanceController drives display white-balance (automatically correcting the
   * display color temperature depending on the ambient color temperature).
   *
   * The DisplayWhiteBalanceController:
   * - Uses the AmbientColorTemperatureSensor to detect changes in the ambient color temperature;
   * - Uses the AmbientColorTemperatureFilter to average these changes over time, filter out the
   *   noise, and arrive at an estimate of the actual ambient color temperature;
   * - Uses the DisplayWhiteBalanceThrottler to decide whether the display color tempearture should
   *   be updated, suppressing changes that are too frequent or too minor.
   */
  public class DisplayWhiteBalanceController implements
          AmbientSensor.AmbientBrightnessSensor.Callbacks,
          AmbientSensor.AmbientColorTemperatureSensor.Callbacks {
  
      protected static final String TAG = "DisplayWhiteBalanceController";
      protected boolean mLoggingEnabled;
  
      private boolean mEnabled;
  
      // To decouple the DisplayPowerController from the DisplayWhiteBalanceController, the DPC
      // implements Callbacks and passes itself to the DWBC so it can call back into it without
      // knowing about it.
      private Callbacks mCallbacks;
  
      private AmbientSensor.AmbientBrightnessSensor mBrightnessSensor;
  
      @VisibleForTesting
      AmbientFilter mBrightnessFilter;
      private AmbientSensor.AmbientColorTemperatureSensor mColorTemperatureSensor;
  
      @VisibleForTesting
      AmbientFilter mColorTemperatureFilter;
      private DisplayWhiteBalanceThrottler mThrottler;
  
      private final float mLowLightAmbientColorTemperature;
      private final float mHighLightAmbientColorTemperature;
  
      private float mAmbientColorTemperature;
  
      @VisibleForTesting
      float mPendingAmbientColorTemperature;
      private float mLastAmbientColorTemperature;
  
      private ColorDisplayServiceInternal mColorDisplayServiceInternal;
  
      // The most recent ambient color temperature values are kept for debugging purposes.
      private static final int HISTORY_SIZE = 50;
      private History mAmbientColorTemperatureHistory;
  
      // Override the ambient color temperature for debugging purposes.
      private float mAmbientColorTemperatureOverride;
  
      // A piecewise linear relationship between ambient and display color temperatures.
      private Spline.LinearSpline mAmbientToDisplayColorTemperatureSpline;
  
      // In very low or very high brightness conditions ambient EQ should to set to a default
      // instead of using mAmbientToDisplayColorTemperatureSpline. However, setting ambient EQ
      // based on thresholds can cause the display to rapidly change color temperature. To solve
      // this, mLowLightAmbientBrightnessToBiasSpline and mHighLightAmbientBrightnessToBiasSpline
      // are used to smoothly interpolate from ambient color temperature to the defaults.
      // A piecewise linear relationship between low light brightness and low light bias.
      private Spline.LinearSpline mLowLightAmbientBrightnessToBiasSpline;
  
      // A piecewise linear relationship between high light brightness and high light bias.
      private Spline.LinearSpline mHighLightAmbientBrightnessToBiasSpline;
  
      private float mLatestAmbientColorTemperature;
      private float mLatestAmbientBrightness;
      private float mLatestLowLightBias;
      private float mLatestHighLightBias;
  
      /**
       * @param brightnessSensor
       *      The sensor used to detect changes in the ambient brightness.
       * @param brightnessFilter
       *      The filter used to avergae ambient brightness changes over time, filter out the noise
       *      and arrive at an estimate of the actual ambient brightness.
       * @param colorTemperatureSensor
       *      The sensor used to detect changes in the ambient color temperature.
       * @param colorTemperatureFilter
       *      The filter used to average ambient color temperature changes over time, filter out the
       *      noise and arrive at an estimate of the actual ambient color temperature.
       * @param throttler
       *      The throttler used to determine whether the new display color temperature should be
       *      updated or not.
       * @param lowLightAmbientBrightnesses
       *      The ambient brightness used to map the ambient brightnesses to the biases used to
       *      interpolate to lowLightAmbientColorTemperature.
       * @param lowLightAmbientBiases
       *      The biases used to map the ambient brightnesses to the biases used to interpolate to
       *      lowLightAmbientColorTemperature.
       * @param lowLightAmbientColorTemperature
       *      The ambient color temperature to which we interpolate to based on the low light curve.
       * @param highLightAmbientBrightnesses
       *      The ambient brightness used to map the ambient brightnesses to the biases used to
       *      interpolate to highLightAmbientColorTemperature.
       * @param highLightAmbientBiases
       *      The biases used to map the ambient brightnesses to the biases used to interpolate to
       *      highLightAmbientColorTemperature.
       * @param highLightAmbientColorTemperature
       *      The ambient color temperature to which we interpolate to based on the high light curve.
       * @param ambientColorTemperatures
       *      The ambient color tempeartures used to map the ambient color temperature to the display
       *      color temperature (or null if no mapping is necessary).
       * @param displayColorTemperatures
       *      The display color temperatures used to map the ambient color temperature to the display
       *      color temperature (or null if no mapping is necessary).
       *
       * @throws NullPointerException
       *      - brightnessSensor is null;
       *      - brightnessFilter is null;
       *      - colorTemperatureSensor is null;
       *      - colorTemperatureFilter is null;
       *      - throttler is null.
       */
      public DisplayWhiteBalanceController(
              @NonNull AmbientSensor.AmbientBrightnessSensor brightnessSensor,
              @NonNull AmbientFilter brightnessFilter,
              @NonNull AmbientSensor.AmbientColorTemperatureSensor colorTemperatureSensor,
              @NonNull AmbientFilter colorTemperatureFilter,
              @NonNull DisplayWhiteBalanceThrottler throttler,
              float[] lowLightAmbientBrightnesses, float[] lowLightAmbientBiases,
              float lowLightAmbientColorTemperature,
              float[] highLightAmbientBrightnesses, float[] highLightAmbientBiases,
              float highLightAmbientColorTemperature,
              float[] ambientColorTemperatures, float[] displayColorTemperatures) {
          validateArguments(brightnessSensor, brightnessFilter, colorTemperatureSensor,
                  colorTemperatureFilter, throttler);
          mLoggingEnabled = false;
          mEnabled = false;
          mCallbacks = null;
          mBrightnessSensor = brightnessSensor;
          mBrightnessFilter = brightnessFilter;
          mColorTemperatureSensor = colorTemperatureSensor;
          mColorTemperatureFilter = colorTemperatureFilter;
          mThrottler = throttler;
          mLowLightAmbientColorTemperature = lowLightAmbientColorTemperature;
          mHighLightAmbientColorTemperature = highLightAmbientColorTemperature;
          mAmbientColorTemperature = -1.0f;
          mPendingAmbientColorTemperature = -1.0f;
          mLastAmbientColorTemperature = -1.0f;
          mAmbientColorTemperatureHistory = new History(HISTORY_SIZE);
          mAmbientColorTemperatureOverride = -1.0f;
  
          try {
              mLowLightAmbientBrightnessToBiasSpline = new Spline.LinearSpline(
                      lowLightAmbientBrightnesses, lowLightAmbientBiases);
          } catch (Exception e) {
              Slog.e(TAG, "failed to create low light ambient brightness to bias spline.", e);
              mLowLightAmbientBrightnessToBiasSpline = null;
          }
          if (mLowLightAmbientBrightnessToBiasSpline != null) {
              if (mLowLightAmbientBrightnessToBiasSpline.interpolate(0.0f) != 0.0f ||
                      mLowLightAmbientBrightnessToBiasSpline.interpolate(Float.POSITIVE_INFINITY)
                      != 1.0f) {
                  Slog.d(TAG, "invalid low light ambient brightness to bias spline, "
                          + "bias must begin at 0.0 and end at 1.0.");
                  mLowLightAmbientBrightnessToBiasSpline = null;
              }
          }
  
          try {
              mHighLightAmbientBrightnessToBiasSpline = new Spline.LinearSpline(
                      highLightAmbientBrightnesses, highLightAmbientBiases);
          } catch (Exception e) {
              Slog.e(TAG, "failed to create high light ambient brightness to bias spline.", e);
              mHighLightAmbientBrightnessToBiasSpline = null;
          }
          if (mHighLightAmbientBrightnessToBiasSpline != null) {
              if (mHighLightAmbientBrightnessToBiasSpline.interpolate(0.0f) != 0.0f ||
                      mHighLightAmbientBrightnessToBiasSpline.interpolate(Float.POSITIVE_INFINITY)
                      != 1.0f) {
                  Slog.d(TAG, "invalid high light ambient brightness to bias spline, "
                          + "bias must begin at 0.0 and end at 1.0.");
                  mHighLightAmbientBrightnessToBiasSpline = null;
              }
          }
  
          if (mLowLightAmbientBrightnessToBiasSpline != null &&
                  mHighLightAmbientBrightnessToBiasSpline != null) {
              if (lowLightAmbientBrightnesses[lowLightAmbientBrightnesses.length - 1] >
                      highLightAmbientBrightnesses[0]) {
                  Slog.d(TAG, "invalid low light and high light ambient brightness to bias spline "
                          + "combination, defined domains must not intersect.");
                  mLowLightAmbientBrightnessToBiasSpline = null;
                  mHighLightAmbientBrightnessToBiasSpline = null;
              }
          }
  
          try {
              mAmbientToDisplayColorTemperatureSpline = new Spline.LinearSpline(
                      ambientColorTemperatures, displayColorTemperatures);
          } catch (Exception e) {
              Slog.e(TAG, "failed to create ambient to display color temperature spline.", e);
              mAmbientToDisplayColorTemperatureSpline = null;
          }
  
          mColorDisplayServiceInternal = LocalServices.getService(ColorDisplayServiceInternal.class);
      }
  
      /**
       * Enable/disable the controller.
       *
       * @param enabled
       *      Whether the controller should be on/off.
       *
       * @return Whether the method succeeded or not.
       */
      public boolean setEnabled(boolean enabled) {
          if (enabled) {
              return enable();
          } else {
              return disable();
          }
      }
  
      /**
       * Set an object to call back to when the display color temperature should be updated.
       *
       * @param callbacks
       *      The object to call back to.
       *
       * @return Whether the method succeeded or not.
       */
      public boolean setCallbacks(Callbacks callbacks) {
          if (mCallbacks == callbacks) {
              return false;
          }
          mCallbacks = callbacks;
          return true;
      }
  
      /**
       * Enable/disable logging.
       *
       * @param loggingEnabled
       *      Whether logging should be on/off.
       *
       * @return Whether the method succeeded or not.
       */
      public boolean setLoggingEnabled(boolean loggingEnabled) {
          if (mLoggingEnabled == loggingEnabled) {
              return false;
          }
          mLoggingEnabled = loggingEnabled;
          mBrightnessSensor.setLoggingEnabled(loggingEnabled);
          mBrightnessFilter.setLoggingEnabled(loggingEnabled);
          mColorTemperatureSensor.setLoggingEnabled(loggingEnabled);
          mColorTemperatureFilter.setLoggingEnabled(loggingEnabled);
          mThrottler.setLoggingEnabled(loggingEnabled);
          return true;
      }
  
      /**
       * Set the ambient color temperature override.
       *
       * This is only applied when the ambient color temperature changes or is updated (in which case
       * it overrides the ambient color temperature estimate); in other words, it doesn't necessarily
       * change the display color temperature immediately.
       *
       * @param ambientColorTemperatureOverride
       *      The ambient color temperature override.
       *
       * @return Whether the method succeeded or not.
       */
      public boolean setAmbientColorTemperatureOverride(float ambientColorTemperatureOverride) {
          if (mAmbientColorTemperatureOverride == ambientColorTemperatureOverride) {
              return false;
          }
          mAmbientColorTemperatureOverride = ambientColorTemperatureOverride;
          return true;
      }
  
      /**
       * Dump the state.
       *
       * @param writer
       *      The writer used to dump the state.
       */
      public void dump(PrintWriter writer) {
          writer.println("DisplayWhiteBalanceController");
          writer.println("  mLoggingEnabled=" + mLoggingEnabled);
          writer.println("  mEnabled=" + mEnabled);
          writer.println("  mCallbacks=" + mCallbacks);
          mBrightnessSensor.dump(writer);
          mBrightnessFilter.dump(writer);
          mColorTemperatureSensor.dump(writer);
          mColorTemperatureFilter.dump(writer);
          mThrottler.dump(writer);
          writer.println("  mLowLightAmbientColorTemperature=" + mLowLightAmbientColorTemperature);
          writer.println("  mHighLightAmbientColorTemperature=" + mHighLightAmbientColorTemperature);
          writer.println("  mAmbientColorTemperature=" + mAmbientColorTemperature);
          writer.println("  mPendingAmbientColorTemperature=" + mPendingAmbientColorTemperature);
          writer.println("  mLastAmbientColorTemperature=" + mLastAmbientColorTemperature);
          writer.println("  mAmbientColorTemperatureHistory=" + mAmbientColorTemperatureHistory);
          writer.println("  mAmbientColorTemperatureOverride=" + mAmbientColorTemperatureOverride);
          writer.println("  mAmbientToDisplayColorTemperatureSpline="
                  + mAmbientToDisplayColorTemperatureSpline);
          writer.println("  mLowLightAmbientBrightnessToBiasSpline="
                  + mLowLightAmbientBrightnessToBiasSpline);
          writer.println("  mHighLightAmbientBrightnessToBiasSpline="
                  + mHighLightAmbientBrightnessToBiasSpline);
      }
  
      @Override // AmbientSensor.AmbientBrightnessSensor.Callbacks
      public void onAmbientBrightnessChanged(float value) {
          final long time = System.currentTimeMillis();
          mBrightnessFilter.addValue(time, value);
          updateAmbientColorTemperature();
      }
  
      @Override // AmbientSensor.AmbientColorTemperatureSensor.Callbacks
      public void onAmbientColorTemperatureChanged(float value) {
          final long time = System.currentTimeMillis();
          mColorTemperatureFilter.addValue(time, value);
          updateAmbientColorTemperature();
      }
  
      /**
       * Updates the ambient color temperature.
       */
      public void updateAmbientColorTemperature() {
          final long time = System.currentTimeMillis();
          float ambientColorTemperature = mColorTemperatureFilter.getEstimate(time);
          mLatestAmbientColorTemperature = ambientColorTemperature;
  
          if (mAmbientToDisplayColorTemperatureSpline != null && ambientColorTemperature != -1.0f) {
              ambientColorTemperature =
                  mAmbientToDisplayColorTemperatureSpline.interpolate(ambientColorTemperature);
          }
  
          float ambientBrightness = mBrightnessFilter.getEstimate(time);
          mLatestAmbientBrightness = ambientBrightness;
  
          if (ambientColorTemperature != -1.0f &&
                  mLowLightAmbientBrightnessToBiasSpline != null) {
              float bias = mLowLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
              ambientColorTemperature =
                      bias * ambientColorTemperature + (1.0f - bias)
                      * mLowLightAmbientColorTemperature;
              mLatestLowLightBias = bias;
          }
          if (ambientColorTemperature != -1.0f &&
                  mHighLightAmbientBrightnessToBiasSpline != null) {
              float bias = mHighLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
              ambientColorTemperature =
                      (1.0f - bias) * ambientColorTemperature + bias
                      * mHighLightAmbientColorTemperature;
              mLatestHighLightBias = bias;
          }
  
          if (mAmbientColorTemperatureOverride != -1.0f) {
              if (mLoggingEnabled) {
                  Slog.d(TAG, "override ambient color temperature: " + ambientColorTemperature
                          + " => " + mAmbientColorTemperatureOverride);
              }
              ambientColorTemperature = mAmbientColorTemperatureOverride;
          }
  
          // When the display color temperature needs to be updated, we call DisplayPowerController to
          // call our updateColorTemperature. The reason we don't call it directly is that we want
          // all changes to the system to happen in a predictable order in DPC's main loop
          // (updatePowerState).
          if (ambientColorTemperature == -1.0f || mThrottler.throttle(ambientColorTemperature)) {
              return;
          }
  
          if (mLoggingEnabled) {
              Slog.d(TAG, "pending ambient color temperature: " + ambientColorTemperature);
          }
          mPendingAmbientColorTemperature = ambientColorTemperature;
          if (mCallbacks != null) {
              mCallbacks.updateWhiteBalance();
          }
      }
  
      /**
       * Updates the display color temperature.
       */
      public void updateDisplayColorTemperature() {
          float ambientColorTemperature = -1.0f;
  
          // If both the pending and the current ambient color temperatures are -1, it means the DWBC
          // was just enabled, and we use the last ambient color temperature until new sensor events
          // give us a better estimate.
          if (mAmbientColorTemperature == -1.0f && mPendingAmbientColorTemperature == -1.0f) {
              ambientColorTemperature = mLastAmbientColorTemperature;
          }
  
          // Otherwise, we use the pending ambient color temperature, but only if it's non-trivial
          // and different than the current one.
          if (mPendingAmbientColorTemperature != -1.0f
                  && mPendingAmbientColorTemperature != mAmbientColorTemperature) {
              ambientColorTemperature = mPendingAmbientColorTemperature;
          }
  
          if (ambientColorTemperature == -1.0f) {
              return;
          }
  
          mAmbientColorTemperature = ambientColorTemperature;
          if (mLoggingEnabled) {
              Slog.d(TAG, "ambient color temperature: " + mAmbientColorTemperature);
          }
          mPendingAmbientColorTemperature = -1.0f;
          mAmbientColorTemperatureHistory.add(mAmbientColorTemperature);
          Slog.d(TAG, "Display cct: " + mAmbientColorTemperature
                  + " Latest ambient cct: " + mLatestAmbientColorTemperature
                  + " Latest ambient lux: " + mLatestAmbientBrightness
                  + " Latest low light bias: " + mLatestLowLightBias
                  + " Latest high light bias: " + mLatestHighLightBias);
          mColorDisplayServiceInternal.setDisplayWhiteBalanceColorTemperature(
                  (int) mAmbientColorTemperature);
          mLastAmbientColorTemperature = mAmbientColorTemperature;
      }
  
      /**
       * The DisplayWhiteBalanceController decouples itself from its parent (DisplayPowerController)
       * by providing this interface to implement (and a method to set its callbacks object), and
       * calling these methods.
       */
      public interface Callbacks {
  
          /**
           * Called whenever the display white-balance state has changed.
           *
           * Usually, this means the estimated ambient color temperature has changed enough, and the
           * display color temperature should be updated; but it is also called if settings change.
           */
          void updateWhiteBalance();
      }
  
      private void validateArguments(AmbientSensor.AmbientBrightnessSensor brightnessSensor,
              AmbientFilter brightnessFilter,
              AmbientSensor.AmbientColorTemperatureSensor colorTemperatureSensor,
              AmbientFilter colorTemperatureFilter,
              DisplayWhiteBalanceThrottler throttler) {
          Preconditions.checkNotNull(brightnessSensor, "brightnessSensor must not be null");
          Preconditions.checkNotNull(brightnessFilter, "brightnessFilter must not be null");
          Preconditions.checkNotNull(colorTemperatureSensor,
                  "colorTemperatureSensor must not be null");
          Preconditions.checkNotNull(colorTemperatureFilter,
                  "colorTemperatureFilter must not be null");
          Preconditions.checkNotNull(throttler, "throttler cannot be null");
      }
  
      private boolean enable() {
          if (mEnabled) {
              return false;
          }
          if (mLoggingEnabled) {
              Slog.d(TAG, "enabling");
          }
          mEnabled = true;
          mBrightnessSensor.setEnabled(true);
          mColorTemperatureSensor.setEnabled(true);
          return true;
      }
  
      private boolean disable() {
          if (!mEnabled) {
              return false;
          }
          if (mLoggingEnabled) {
              Slog.d(TAG, "disabling");
          }
          mEnabled = false;
          mBrightnessSensor.setEnabled(false);
          mBrightnessFilter.clear();
          mColorTemperatureSensor.setEnabled(false);
          mColorTemperatureFilter.clear();
          mThrottler.clear();
          mAmbientColorTemperature = -1.0f;
          mPendingAmbientColorTemperature = -1.0f;
          mColorDisplayServiceInternal.resetDisplayWhiteBalanceColorTemperature();
          return true;
      }
  
  }
  