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.ex.camera2.portability;
  
  import static android.hardware.camera2.CaptureRequest.*;
  
  import android.graphics.Matrix;
  import android.graphics.Rect;
  import android.graphics.RectF;
  import android.hardware.camera2.CameraAccessException;
  import android.hardware.camera2.CameraDevice;
  import android.hardware.camera2.params.MeteringRectangle;
  import android.location.Location;
  import android.util.Range;
  
  import com.android.ex.camera2.portability.CameraCapabilities.FlashMode;
  import com.android.ex.camera2.portability.CameraCapabilities.FocusMode;
  import com.android.ex.camera2.portability.CameraCapabilities.SceneMode;
  import com.android.ex.camera2.portability.CameraCapabilities.WhiteBalance;
  import com.android.ex.camera2.portability.debug.Log;
  import com.android.ex.camera2.utils.Camera2RequestSettingsSet;
  
  import java.util.List;
  import java.util.Objects;
  
  /**
   * The subclass of {@link CameraSettings} for Android Camera 2 API.
   */
  public class AndroidCamera2Settings extends CameraSettings {
      private static final Log.Tag TAG = new Log.Tag("AndCam2Set");
  
      private final Builder mTemplateSettings;
      private final Camera2RequestSettingsSet mRequestSettings;
      /** Sensor's active array bounds. */
      private final Rect mActiveArray;
      /** Crop rectangle for digital zoom (measured WRT the active array). */
      private final Rect mCropRectangle;
      /** Bounds of visible preview portion (measured WRT the active array). */
      private Rect mVisiblePreviewRectangle;
  
      /**
       * Create a settings representation that answers queries of unspecified
       * options in the same way as the provided template would.
       *
       * <p>The default settings provided by the given template are only ever used
       * for reporting back to the client app (i.e. when it queries an option
       * it didn't explicitly set first). {@link Camera2RequestSettingsSet}s
       * generated by an instance of this class will have any settings not
       * modified using one of that instance's mutators forced to default, so that
       * their effective values when submitting a capture request will be those of
       * the template that is provided to the camera framework at that time.</p>
       *
       * @param camera Device from which to draw default settings
       *               (non-{@code null}).
       * @param template Specific template to use for the defaults.
       * @param activeArray Boundary coordinates of the sensor's active array
       *                    (non-{@code null}).
       * @param preview Dimensions of preview streams.
       * @param photo Dimensions of captured images.
       *
       * @throws IllegalArgumentException If {@code camera} or {@code activeArray}
       *                                  is {@code null}.
       * @throws CameraAccessException Upon internal framework/driver failure.
       */
      public AndroidCamera2Settings(CameraDevice camera, int template, Rect activeArray,
                                    Size preview, Size photo) throws CameraAccessException {
          if (camera == null) {
              throw new NullPointerException("camera must not be null");
          }
          if (activeArray == null) {
              throw new NullPointerException("activeArray must not be null");
          }
  
          mTemplateSettings = camera.createCaptureRequest(template);
          mRequestSettings = new Camera2RequestSettingsSet();
          mActiveArray = activeArray;
          mCropRectangle = new Rect(0, 0, activeArray.width(), activeArray.height());
  
          mSizesLocked = false;
  
          Range<Integer> previewFpsRange = mTemplateSettings.get(CONTROL_AE_TARGET_FPS_RANGE);
          if (previewFpsRange != null) {
              setPreviewFpsRange(previewFpsRange.getLower(), previewFpsRange.getUpper());
          }
          setPreviewSize(preview);
          // TODO: mCurrentPreviewFormat
          setPhotoSize(photo);
          mJpegCompressQuality = queryTemplateDefaultOrMakeOneUp(JPEG_QUALITY, (byte) 0);
          // TODO: mCurrentPhotoFormat
          // NB: We're assuming that templates won't be zoomed in by default.
          mCurrentZoomRatio = CameraCapabilities.ZOOM_RATIO_UNZOOMED;
          // TODO: mCurrentZoomIndex
          mExposureCompensationIndex =
                  queryTemplateDefaultOrMakeOneUp(CONTROL_AE_EXPOSURE_COMPENSATION, 0);
  
          mCurrentFlashMode = flashModeFromRequest();
          Integer currentFocusMode = mTemplateSettings.get(CONTROL_AF_MODE);
          if (currentFocusMode != null) {
              mCurrentFocusMode = AndroidCamera2Capabilities.focusModeFromInt(currentFocusMode);
          }
          Integer currentSceneMode = mTemplateSettings.get(CONTROL_SCENE_MODE);
          if (currentSceneMode != null) {
              mCurrentSceneMode = AndroidCamera2Capabilities.sceneModeFromInt(currentSceneMode);
          }
          Integer whiteBalance = mTemplateSettings.get(CONTROL_AWB_MODE);
          if (whiteBalance != null) {
              mWhiteBalance = AndroidCamera2Capabilities.whiteBalanceFromInt(whiteBalance);
          }
  
          mVideoStabilizationEnabled = queryTemplateDefaultOrMakeOneUp(
                          CONTROL_VIDEO_STABILIZATION_MODE, CONTROL_VIDEO_STABILIZATION_MODE_OFF) ==
                  CONTROL_VIDEO_STABILIZATION_MODE_ON;
          mAutoExposureLocked = queryTemplateDefaultOrMakeOneUp(CONTROL_AE_LOCK, false);
          mAutoWhiteBalanceLocked = queryTemplateDefaultOrMakeOneUp(CONTROL_AWB_LOCK, false);
          // TODO: mRecordingHintEnabled
          // TODO: mGpsData
          android.util.Size exifThumbnailSize = mTemplateSettings.get(JPEG_THUMBNAIL_SIZE);
          if (exifThumbnailSize != null) {
              mExifThumbnailSize =
                      new Size(exifThumbnailSize.getWidth(), exifThumbnailSize.getHeight());
          }
      }
  
      public AndroidCamera2Settings(AndroidCamera2Settings other) {
          super(other);
          mTemplateSettings = other.mTemplateSettings;
          mRequestSettings = new Camera2RequestSettingsSet(other.mRequestSettings);
          mActiveArray = other.mActiveArray;
          mCropRectangle = new Rect(other.mCropRectangle);
      }
  
      @Override
      public CameraSettings copy() {
          return new AndroidCamera2Settings(this);
      }
  
      private <T> T queryTemplateDefaultOrMakeOneUp(Key<T> key, T defaultDefault) {
          T val = mTemplateSettings.get(key);
          if (val != null) {
              return val;
          } else {
              // Spoof the default so matchesTemplateDefault excludes this key from generated sets.
              // This approach beats a simple sentinel because it provides basic boolean support.
              mTemplateSettings.set(key, defaultDefault);
              return defaultDefault;
          }
      }
  
      private FlashMode flashModeFromRequest() {
          Integer autoExposure = mTemplateSettings.get(CONTROL_AE_MODE);
          if (autoExposure != null) {
              switch (autoExposure) {
                  case CONTROL_AE_MODE_ON:
                      return FlashMode.OFF;
                  case CONTROL_AE_MODE_ON_AUTO_FLASH:
                      return FlashMode.AUTO;
                  case CONTROL_AE_MODE_ON_ALWAYS_FLASH: {
                      if (mTemplateSettings.get(FLASH_MODE) == FLASH_MODE_TORCH) {
                          return FlashMode.TORCH;
                      } else {
                          return FlashMode.ON;
                      }
                  }
                  case CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE:
                      return FlashMode.RED_EYE;
              }
          }
          return null;
      }
  
      @Override
      public void setZoomRatio(float ratio) {
          super.setZoomRatio(ratio);
  
          // Compute the crop rectangle to be passed to the framework
          mCropRectangle.set(0, 0,
                  toIntConstrained(
                          mActiveArray.width() / mCurrentZoomRatio, 0, mActiveArray.width()),
                  toIntConstrained(
                          mActiveArray.height() / mCurrentZoomRatio, 0, mActiveArray.height()));
          mCropRectangle.offsetTo((mActiveArray.width() - mCropRectangle.width()) / 2,
                  (mActiveArray.height() - mCropRectangle.height()) / 2);
  
          // Compute the effective crop rectangle to be used for computing focus/metering coordinates
          mVisiblePreviewRectangle =
                  effectiveCropRectFromRequested(mCropRectangle, mCurrentPreviewSize);
      }
  
      private boolean matchesTemplateDefault(Key<?> setting) {
          if (setting == CONTROL_AE_REGIONS) {
              return mMeteringAreas.size() == 0;
          } else if (setting == CONTROL_AF_REGIONS) {
              return mFocusAreas.size() == 0;
          } else if (setting == CONTROL_AE_TARGET_FPS_RANGE) {
              Range<Integer> defaultFpsRange = mTemplateSettings.get(CONTROL_AE_TARGET_FPS_RANGE);
              return (mPreviewFpsRangeMin == 0 && mPreviewFpsRangeMax == 0) ||
                      (defaultFpsRange != null && mPreviewFpsRangeMin == defaultFpsRange.getLower() &&
                              mPreviewFpsRangeMax == defaultFpsRange.getUpper());
          } else if (setting == JPEG_QUALITY) {
              return Objects.equals(mJpegCompressQuality,
                      mTemplateSettings.get(JPEG_QUALITY));
          } else if (setting == CONTROL_AE_EXPOSURE_COMPENSATION) {
              return Objects.equals(mExposureCompensationIndex,
                      mTemplateSettings.get(CONTROL_AE_EXPOSURE_COMPENSATION));
          } else if (setting == CONTROL_VIDEO_STABILIZATION_MODE) {
              Integer videoStabilization = mTemplateSettings.get(CONTROL_VIDEO_STABILIZATION_MODE);
              return (videoStabilization != null &&
                      (mVideoStabilizationEnabled && videoStabilization ==
                              CONTROL_VIDEO_STABILIZATION_MODE_ON) ||
                      (!mVideoStabilizationEnabled && videoStabilization ==
                              CONTROL_VIDEO_STABILIZATION_MODE_OFF));
          } else if (setting == CONTROL_AE_LOCK) {
              return Objects.equals(mAutoExposureLocked, mTemplateSettings.get(CONTROL_AE_LOCK));
          } else if (setting == CONTROL_AWB_LOCK) {
              return Objects.equals(mAutoWhiteBalanceLocked, mTemplateSettings.get(CONTROL_AWB_LOCK));
          } else if (setting == JPEG_THUMBNAIL_SIZE) {
              if (mExifThumbnailSize == null) {
                  // It doesn't matter if this is true or false since setting this
                  // to null in the request settings will use the default anyway.
                  return false;
              }
              android.util.Size defaultThumbnailSize = mTemplateSettings.get(JPEG_THUMBNAIL_SIZE);
              return (mExifThumbnailSize.width() == 0 && mExifThumbnailSize.height() == 0) ||
                      (defaultThumbnailSize != null &&
                              mExifThumbnailSize.width() == defaultThumbnailSize.getWidth() &&
                              mExifThumbnailSize.height() == defaultThumbnailSize.getHeight());
          }
          Log.w(TAG, "Settings implementation checked default of unhandled option key");
          // Since this class isn't equipped to handle it, claim it matches the default to prevent
          // updateRequestSettingOrForceToDefault from going with the user-provided preference
          return true;
      }
  
      private <T> void updateRequestSettingOrForceToDefault(Key<T> setting, T possibleChoice) {
          mRequestSettings.set(setting, matchesTemplateDefault(setting) ? null : possibleChoice);
      }
  
      public Camera2RequestSettingsSet getRequestSettings() {
          updateRequestSettingOrForceToDefault(CONTROL_AE_REGIONS,
                  legacyAreasToMeteringRectangles(mMeteringAreas));
          updateRequestSettingOrForceToDefault(CONTROL_AF_REGIONS,
                  legacyAreasToMeteringRectangles(mFocusAreas));
          updateRequestSettingOrForceToDefault(CONTROL_AE_TARGET_FPS_RANGE,
                  new Range(mPreviewFpsRangeMin, mPreviewFpsRangeMax));
          // TODO: mCurrentPreviewFormat
          updateRequestSettingOrForceToDefault(JPEG_QUALITY, mJpegCompressQuality);
          // TODO: mCurrentPhotoFormat
          mRequestSettings.set(SCALER_CROP_REGION, mCropRectangle);
          // TODO: mCurrentZoomIndex
          updateRequestSettingOrForceToDefault(CONTROL_AE_EXPOSURE_COMPENSATION,
                  mExposureCompensationIndex);
          updateRequestFlashMode();
          updateRequestFocusMode();
          updateRequestSceneMode();
          updateRequestWhiteBalance();
          updateRequestSettingOrForceToDefault(CONTROL_VIDEO_STABILIZATION_MODE,
                  mVideoStabilizationEnabled ?
                          CONTROL_VIDEO_STABILIZATION_MODE_ON : CONTROL_VIDEO_STABILIZATION_MODE_OFF);
          // OIS shouldn't be on if software video stabilization is.
          mRequestSettings.set(LENS_OPTICAL_STABILIZATION_MODE,
                  mVideoStabilizationEnabled ? LENS_OPTICAL_STABILIZATION_MODE_OFF :
                          null);
          updateRequestSettingOrForceToDefault(CONTROL_AE_LOCK, mAutoExposureLocked);
          updateRequestSettingOrForceToDefault(CONTROL_AWB_LOCK, mAutoWhiteBalanceLocked);
          // TODO: mRecordingHintEnabled
          updateRequestGpsData();
          if (mExifThumbnailSize != null) {
              updateRequestSettingOrForceToDefault(JPEG_THUMBNAIL_SIZE,
                      new android.util.Size(
                              mExifThumbnailSize.width(), mExifThumbnailSize.height()));
          } else {
              updateRequestSettingOrForceToDefault(JPEG_THUMBNAIL_SIZE, null);
          }
  
          return mRequestSettings;
      }
  
      private MeteringRectangle[] legacyAreasToMeteringRectangles(
              List<android.hardware.Camera.Area> reference) {
          MeteringRectangle[] transformed = null;
          if (reference.size() > 0) {
              transformed = new MeteringRectangle[reference.size()];
              for (int index = 0; index < reference.size(); ++index) {
                  android.hardware.Camera.Area source = reference.get(index);
                  Rect rectangle = source.rect;
  
                  // Old API coordinates were [-1000,1000]; new ones are [0,ACTIVE_ARRAY_SIZE).
                  // We're also going from preview image--relative to sensor active array--relative.
                  double oldLeft = (rectangle.left + 1000) / 2000.0;
                  double oldTop = (rectangle.top + 1000) / 2000.0;
                  double oldRight = (rectangle.right + 1000) / 2000.0;
                  double oldBottom = (rectangle.bottom + 1000) / 2000.0;
                  int left = mCropRectangle.left + toIntConstrained(
                          mCropRectangle.width() * oldLeft, 0, mCropRectangle.width() - 1);
                  int top = mCropRectangle.top + toIntConstrained(
                          mCropRectangle.height() * oldTop, 0, mCropRectangle.height() - 1);
                  int right = mCropRectangle.left + toIntConstrained(
                          mCropRectangle.width() * oldRight, 0, mCropRectangle.width() - 1);
                  int bottom = mCropRectangle.top + toIntConstrained(
                          mCropRectangle.height() * oldBottom, 0, mCropRectangle.height() - 1);
                  transformed[index] = new MeteringRectangle(left, top, right - left, bottom - top,
                          source.weight);
              }
          }
          return transformed;
      }
  
      private int toIntConstrained(double original, int min, int max) {
          original = Math.max(original, min);
          original = Math.min(original, max);
          return (int) original;
      }
  
      private void updateRequestFlashMode() {
          Integer aeMode = null;
          Integer flashMode = null;
          if (mCurrentFlashMode != null) {
              switch (mCurrentFlashMode) {
                  case AUTO: {
                      aeMode = CONTROL_AE_MODE_ON_AUTO_FLASH;
                      break;
                  }
                  case OFF: {
                      aeMode = CONTROL_AE_MODE_ON;
                      flashMode = FLASH_MODE_OFF;
                      break;
                  }
                  case ON: {
                      aeMode = CONTROL_AE_MODE_ON_ALWAYS_FLASH;
                      flashMode = FLASH_MODE_SINGLE;
                      break;
                  }
                  case TORCH: {
                      flashMode = FLASH_MODE_TORCH;
                      break;
                  }
                  case RED_EYE: {
                      aeMode = CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE;
                      break;
                  }
                  default: {
                      Log.w(TAG, "Unable to convert to API 2 flash mode: " + mCurrentFlashMode);
                      break;
                  }
              }
          }
          mRequestSettings.set(CONTROL_AE_MODE, aeMode);
          mRequestSettings.set(FLASH_MODE, flashMode);
      }
  
      private void updateRequestFocusMode() {
          Integer mode = null;
          if (mCurrentFocusMode != null) {
              switch (mCurrentFocusMode) {
                  case AUTO: {
                      mode = CONTROL_AF_MODE_AUTO;
                      break;
                  }
                  case CONTINUOUS_PICTURE: {
                      mode = CONTROL_AF_MODE_CONTINUOUS_PICTURE;
                      break;
                  }
                  case CONTINUOUS_VIDEO: {
                      mode = CONTROL_AF_MODE_CONTINUOUS_VIDEO;
                      break;
                  }
                  case EXTENDED_DOF: {
                      mode = CONTROL_AF_MODE_EDOF;
                      break;
                  }
                  case FIXED: {
                      mode = CONTROL_AF_MODE_OFF;
                      break;
                  }
                  // TODO: We cannot support INFINITY
                  case MACRO: {
                      mode = CONTROL_AF_MODE_MACRO;
                      break;
                  }
                  default: {
                      Log.w(TAG, "Unable to convert to API 2 focus mode: " + mCurrentFocusMode);
                      break;
                  }
              }
          }
          mRequestSettings.set(CONTROL_AF_MODE, mode);
      }
  
      private void updateRequestSceneMode() {
          Integer mode = null;
          if (mCurrentSceneMode != null) {
              switch (mCurrentSceneMode) {
                  case AUTO: {
                      mode = CONTROL_SCENE_MODE_DISABLED;
                      break;
                  }
                  case ACTION: {
                      mode = CONTROL_SCENE_MODE_ACTION;
                      break;
                  }
                  case BARCODE: {
                      mode = CONTROL_SCENE_MODE_BARCODE;
                      break;
                  }
                  case BEACH: {
                      mode = CONTROL_SCENE_MODE_BEACH;
                      break;
                  }
                  case CANDLELIGHT: {
                      mode = CONTROL_SCENE_MODE_CANDLELIGHT;
                      break;
                  }
                  case FIREWORKS: {
                      mode = CONTROL_SCENE_MODE_FIREWORKS;
                      break;
                  }
                  case HDR: {
                      mode = CONTROL_SCENE_MODE_HDR;
                      break;
                  }
                  case LANDSCAPE: {
                      mode = CONTROL_SCENE_MODE_LANDSCAPE;
                      break;
                  }
                  case NIGHT: {
                      mode = CONTROL_SCENE_MODE_NIGHT;
                      break;
                  }
                  // TODO: We cannot support NIGHT_PORTRAIT
                  case PARTY: {
                      mode = CONTROL_SCENE_MODE_PARTY;
                      break;
                  }
                  case PORTRAIT: {
                      mode = CONTROL_SCENE_MODE_PORTRAIT;
                      break;
                  }
                  case SNOW: {
                      mode = CONTROL_SCENE_MODE_SNOW;
                      break;
                  }
                  case SPORTS: {
                      mode = CONTROL_SCENE_MODE_SPORTS;
                      break;
                  }
                  case STEADYPHOTO: {
                      mode = CONTROL_SCENE_MODE_STEADYPHOTO;
                      break;
                  }
                  case SUNSET: {
                      mode = CONTROL_SCENE_MODE_SUNSET;
                      break;
                  }
                  case THEATRE: {
                      mode = CONTROL_SCENE_MODE_THEATRE;
                      break;
                  }
                  default: {
                      Log.w(TAG, "Unable to convert to API 2 scene mode: " + mCurrentSceneMode);
                      break;
                  }
              }
          }
          mRequestSettings.set(CONTROL_SCENE_MODE, mode);
      }
  
      private void updateRequestWhiteBalance() {
          Integer mode = null;
          if (mWhiteBalance != null) {
              switch (mWhiteBalance) {
                  case AUTO: {
                      mode = CONTROL_AWB_MODE_AUTO;
                      break;
                  }
                  case CLOUDY_DAYLIGHT: {
                      mode = CONTROL_AWB_MODE_CLOUDY_DAYLIGHT;
                      break;
                  }
                  case DAYLIGHT: {
                      mode = CONTROL_AWB_MODE_DAYLIGHT;
                      break;
                  }
                  case FLUORESCENT: {
                      mode = CONTROL_AWB_MODE_FLUORESCENT;
                      break;
                  }
                  case INCANDESCENT: {
                      mode = CONTROL_AWB_MODE_INCANDESCENT;
                      break;
                  }
                  case SHADE: {
                      mode = CONTROL_AWB_MODE_SHADE;
                      break;
                  }
                  case TWILIGHT: {
                      mode = CONTROL_AWB_MODE_TWILIGHT;
                      break;
                  }
                  case WARM_FLUORESCENT: {
                      mode = CONTROL_AWB_MODE_WARM_FLUORESCENT;
                      break;
                  }
                  default: {
                      Log.w(TAG, "Unable to convert to API 2 white balance: " + mWhiteBalance);
                      break;
                  }
              }
          }
          mRequestSettings.set(CONTROL_AWB_MODE, mode);
      }
  
      private void updateRequestGpsData() {
          if (mGpsData == null || mGpsData.processingMethod == null) {
              // It's a hack since we always use GPS time stamp but does
              // not use other fields sometimes. Setting processing
              // method to null means the other fields should not be used.
              mRequestSettings.set(JPEG_GPS_LOCATION, null);
          } else {
              Location location = new Location(mGpsData.processingMethod);
              location.setTime(mGpsData.timeStamp);
              location.setAltitude(mGpsData.altitude);
              location.setLatitude(mGpsData.latitude);
              location.setLongitude(mGpsData.longitude);
              mRequestSettings.set(JPEG_GPS_LOCATION, location);
          }
      }
  
      /**
       * Calculate the effective crop rectangle for this preview viewport;
       * assumes the preview is centered to the sensor and scaled to fit across one of the dimensions
       * without skewing.
       *
       * <p>Assumes the zoom level of the provided desired crop rectangle.</p>
       *
       * @param requestedCrop Desired crop rectangle, in active array space.
       * @param previewSize Size of the preview buffer render target, in pixels (not in sensor space).
       * @return A rectangle that serves as the preview stream's effective crop region (unzoomed), in
       *          sensor space.
       *
       * @throws NullPointerException
       *          If any of the args were {@code null}.
       */
      private static Rect effectiveCropRectFromRequested(Rect requestedCrop, Size previewSize) {
          float aspectRatioArray = requestedCrop.width() * 1.0f / requestedCrop.height();
          float aspectRatioPreview = previewSize.width() * 1.0f / previewSize.height();
  
          float cropHeight, cropWidth;
          if (aspectRatioPreview < aspectRatioArray) {
              // The new width must be smaller than the height, so scale the width by AR
              cropHeight = requestedCrop.height();
              cropWidth = cropHeight * aspectRatioPreview;
          } else {
              // The new height must be smaller (or equal) than the width, so scale the height by AR
              cropWidth = requestedCrop.width();
              cropHeight = cropWidth / aspectRatioPreview;
          }
  
          Matrix translateMatrix = new Matrix();
          RectF cropRect = new RectF(/*left*/0, /*top*/0, cropWidth, cropHeight);
  
          // Now center the crop rectangle so its center is in the center of the active array
          translateMatrix.setTranslate(requestedCrop.exactCenterX(), requestedCrop.exactCenterY());
          translateMatrix.postTranslate(-cropRect.centerX(), -cropRect.centerY());
  
          translateMatrix.mapRect(/*inout*/cropRect);
  
          // Round the rect corners towards the nearest integer values
          Rect result = new Rect();
          cropRect.roundOut(result);
          return result;
      }
  }
  