/*
 * Copyright (C) 2012-2015 Mozilla Foundation
 *
 * 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.
 */

#include "GonkCameraControl.h"
#include <time.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <libgen.h>
#include "base/basictypes.h"
#include "Layers.h"
#ifdef MOZ_WIDGET_GONK
#include <media/mediaplayer.h>
#include <media/MediaProfiles.h>
#include "GrallocImages.h"
#include "imgIEncoder.h"
#include "libyuv.h"
#include "nsNetUtil.h" // for NS_ReadInputStreamToBuffer
#endif
#include "nsNetCID.h" // for NS_STREAMTRANSPORTSERVICE_CONTRACTID
#include "nsAutoPtr.h" // for nsAutoArrayPtr
#include "nsCOMPtr.h"
#include "nsMemory.h"
#include "nsThread.h"
#include "nsITimer.h"
#include "mozilla/FileUtils.h"
#include "mozilla/Services.h"
#include "mozilla/unused.h"
#include "mozilla/ipc/FileDescriptorUtils.h"
#include "nsAlgorithm.h"
#include "nsPrintfCString.h"
#include "GonkCameraHwMgr.h"
#include "GonkRecorderProfiles.h"
#include "CameraCommon.h"
#include "GonkCameraParameters.h"
#include "DeviceStorageFileDescriptor.h"

using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::layers;
using namespace mozilla::gfx;
using namespace mozilla::ipc;
using namespace android;

#define RETURN_IF_NO_CAMERA_HW()                                          \
  do {                                                                    \
    if (!mCameraHw.get()) {                                               \
      NS_WARNING("Camera hardware is not initialized");                   \
      DOM_CAMERA_LOGE("%s:%d : mCameraHw is null\n", __func__, __LINE__); \
      return NS_ERROR_NOT_INITIALIZED;                                    \
    }                                                                     \
  } while(0)

static const unsigned long kAutoFocusCompleteTimeoutMs = 1000;
static const int32_t kAutoFocusCompleteTimeoutLimit = 3;

// Construct nsGonkCameraControl on the main thread.
nsGonkCameraControl::nsGonkCameraControl(uint32_t aCameraId)
  : mCameraId(aCameraId)
  , mLastThumbnailSize({0, 0})
  , mPreviewFps(30)
  , mResumePreviewAfterTakingPicture(false) // XXXmikeh - see bug 950102
  , mFlashSupported(false)
  , mLuminanceSupported(false)
  , mAutoFlashModeOverridden(false)
  , mSeparateVideoAndPreviewSizesSupported(false)
  , mDeferConfigUpdate(0)
#ifdef MOZ_WIDGET_GONK
  , mRecorder(nullptr)
#endif
  , mRecorderMonitor("GonkCameraControl::mRecorder.Monitor")
  , mVideoFile(nullptr)
  , mCapturePoster(false)
  , mAutoFocusPending(false)
  , mAutoFocusCompleteExpired(0)
  , mPrevFacesDetected(0)
  , mReentrantMonitor("GonkCameraControl::OnTakePicture.Monitor")
{
  // Constructor runs on the main thread...
  DOM_CAMERA_LOGT("%s:%d : this=%p\n", __func__, __LINE__, this);
  mImageContainer = LayerManager::CreateImageContainer();

  mAutoFocusCompleteTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
  if (NS_WARN_IF(!mAutoFocusCompleteTimer)) {
    mAutoFocusCompleteExpired = kAutoFocusCompleteTimeoutLimit;
  }
}

nsresult
nsGonkCameraControl::StartImpl(const Configuration* aInitialConfig)
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);

  nsresult rv = StartInternal(aInitialConfig);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    OnHardwareStateChange(CameraControlListener::kHardwareOpenFailed,
                          NS_ERROR_NOT_AVAILABLE);
  }
  return rv;
}

nsresult
nsGonkCameraControl::StartInternal(const Configuration* aInitialConfig)
{
  /**
   * For initialization, we try to return the camera control to the upper
   * upper layer (i.e. the DOM) as quickly as possible. To do this, the
   * camera is initialized in the following stages:
   *
   *  0. Initialize() initializes the hardware;
   *  1. SetConfigurationInternal() does the minimal configuration
   *     required so that we can start the preview -and- report a valid
   *     configuration to the upper layer;
   *  2. OnHardwareStateChange() reports that the hardware is ready,
   *     which the upper (e.g. DOM) layer can (and does) use to return
   *     the camera control object;
   *  3. StartPreviewImpl() starts the flow of preview frames from the
   *     camera hardware.
   *
   * The intent of the above flow is to let the Main Thread do as much work
   * up-front as possible without waiting for blocking Camera Thread calls
   * to complete.
   */
  nsresult rv = Initialize();
  switch (rv) {
    case NS_ERROR_ALREADY_INITIALIZED:
    case NS_OK:
      break;

    default:
      return rv;
  }

  if (aInitialConfig) {
    Configuration config;
    rv = ValidateConfiguration(*aInitialConfig, config);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    rv = SetConfigurationInternal(config);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      // The initial configuration failed, close up the hardware
      StopInternal();
      return rv;
    }
  }

  OnHardwareStateChange(CameraControlListener::kHardwareOpen, NS_OK);

  if (aInitialConfig) {
    rv = StartPreviewInternal();
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    OnConfigurationChange();
    OnPreviewStateChange(CameraControlListener::kPreviewStarted);
  }
  return NS_OK;
}

nsresult
nsGonkCameraControl::Initialize()
{
  if (mCameraHw.get()) {
    DOM_CAMERA_LOGI("Camera %d already connected (this=%p)\n", mCameraId, this);
    return NS_ERROR_ALREADY_INITIALIZED;
  }

  mCameraHw = GonkCameraHardware::Connect(this, mCameraId);
  if (!mCameraHw.get()) {
    DOM_CAMERA_LOGE("Failed to connect to camera %d (this=%p)\n", mCameraId, this);
    return NS_ERROR_NOT_INITIALIZED;
  }

  DOM_CAMERA_LOGI("Initializing camera %d (this=%p, mCameraHw=%p)\n", mCameraId, this, mCameraHw.get());
  mCurrentConfiguration.mRecorderProfile.Truncate();
  mRequestedPreviewSize.width = UINT32_MAX;
  mRequestedPreviewSize.height = UINT32_MAX;

  // Initialize our camera configuration database.
  mCameraHw->PullParameters(mParams);

  // Set preferred preview frame format.
  mParams.Set(CAMERA_PARAM_PREVIEWFORMAT, NS_LITERAL_STRING("yuv420sp"));
  // Turn off any normal pictures returned by the HDR scene mode
  mParams.Set(CAMERA_PARAM_SCENEMODE_HDR_RETURNNORMALPICTURE, false);
  PushParametersImpl();

  // Grab any other settings we'll need later.
  mParams.Get(CAMERA_PARAM_PICTURE_FILEFORMAT, mFileFormat);
  mParams.Get(CAMERA_PARAM_THUMBNAILSIZE, mLastThumbnailSize);

  // The emulator's camera returns -1 for these values; bump them up to 0
  int areas;
  mParams.Get(CAMERA_PARAM_SUPPORTED_MAXMETERINGAREAS, areas);
  mCurrentConfiguration.mMaxMeteringAreas = areas != -1 ? areas : 0;
  mParams.Get(CAMERA_PARAM_SUPPORTED_MAXFOCUSAREAS, areas);
  mCurrentConfiguration.mMaxFocusAreas = areas != -1 ? areas : 0;

  mParams.Get(CAMERA_PARAM_PICTURE_SIZE, mCurrentConfiguration.mPictureSize);
  mParams.Get(CAMERA_PARAM_PREVIEWSIZE, mCurrentConfiguration.mPreviewSize);

  nsString luminance; // check for support
  mParams.Get(CAMERA_PARAM_LUMINANCE, luminance);
  mLuminanceSupported = !luminance.IsEmpty();

  nsString flashMode;
  mParams.Get(CAMERA_PARAM_FLASHMODE, flashMode);
  mFlashSupported = !flashMode.IsEmpty();

  double quality; // informational only
  mParams.Get(CAMERA_PARAM_PICTURE_QUALITY, quality);

  DOM_CAMERA_LOGI(" - maximum metering areas:        %u\n", mCurrentConfiguration.mMaxMeteringAreas);
  DOM_CAMERA_LOGI(" - maximum focus areas:           %u\n", mCurrentConfiguration.mMaxFocusAreas);
  DOM_CAMERA_LOGI(" - default picture size:          %u x %u\n",
    mCurrentConfiguration.mPictureSize.width, mCurrentConfiguration.mPictureSize.height);
  DOM_CAMERA_LOGI(" - default picture file format:   %s\n",
    NS_ConvertUTF16toUTF8(mFileFormat).get());
  DOM_CAMERA_LOGI(" - default picture quality:       %f\n", quality);
  DOM_CAMERA_LOGI(" - default thumbnail size:        %u x %u\n",
    mLastThumbnailSize.width, mLastThumbnailSize.height);
  DOM_CAMERA_LOGI(" - default preview size:          %u x %u\n",
    mCurrentConfiguration.mPreviewSize.width, mCurrentConfiguration.mPreviewSize.height);
  DOM_CAMERA_LOGI(" - luminance reporting:           %ssupported\n",
    mLuminanceSupported ? "" : "NOT ");
  if (mFlashSupported) {
    DOM_CAMERA_LOGI(" - flash:                         supported, default mode '%s'\n",
      NS_ConvertUTF16toUTF8(flashMode).get());
  } else {
    DOM_CAMERA_LOGI(" - flash:                         NOT supported\n");
  }

  nsAutoTArray<Size, 16> sizes;
  mParams.Get(CAMERA_PARAM_SUPPORTED_VIDEOSIZES, sizes);
  if (sizes.Length() > 0) {
    mSeparateVideoAndPreviewSizesSupported = true;
    DOM_CAMERA_LOGI(" - support for separate preview and video sizes\n");
    mParams.Get(CAMERA_PARAM_VIDEOSIZE, mLastRecorderSize);
    DOM_CAMERA_LOGI(" - default video recorder size:   %u x %u\n",
      mLastRecorderSize.width, mLastRecorderSize.height);

    Size preferred;
    mParams.Get(CAMERA_PARAM_PREFERRED_PREVIEWSIZE_FOR_VIDEO, preferred);
    DOM_CAMERA_LOGI(" - preferred video preview size:  %u x %u\n",
      preferred.width, preferred.height);
  } else {
    mLastRecorderSize = mCurrentConfiguration.mPreviewSize;
  }

  nsAutoTArray<nsString, 8> modes;
  mParams.Get(CAMERA_PARAM_SUPPORTED_METERINGMODES, modes);
  if (!modes.IsEmpty()) {
    nsString mode;
    const char* kCenterWeighted = "center-weighted";

    mParams.Get(CAMERA_PARAM_METERINGMODE, mode);
    if (!mode.EqualsASCII(kCenterWeighted)) {
      nsTArray<nsString>::index_type i = modes.Length();
      while (i > 0) {
        --i;
        if (modes[i].EqualsASCII(kCenterWeighted)) {
          mParams.Set(CAMERA_PARAM_METERINGMODE, modes[i]);
          PushParametersImpl();
          break;
        }
      }
    }

    mParams.Get(CAMERA_PARAM_METERINGMODE, mode);
    DOM_CAMERA_LOGI(" - metering mode:                 '%s'\n",
      NS_ConvertUTF16toUTF8(mode).get());
  }

  return NS_OK;
}

nsGonkCameraControl::~nsGonkCameraControl()
{
  DOM_CAMERA_LOGT("%s:%d : this=%p, mCameraHw = %p\n", __func__, __LINE__, this, mCameraHw.get());

  StopImpl();
  DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);
}

nsresult
nsGonkCameraControl::ValidateConfiguration(const Configuration& aConfig, Configuration& aValidatedConfig)
{
  nsAutoTArray<Size, 16> supportedSizes;
  Get(CAMERA_PARAM_SUPPORTED_PICTURESIZES, supportedSizes);

  nsresult rv = GetSupportedSize(aConfig.mPictureSize, supportedSizes,
                                 aValidatedConfig.mPictureSize);
  if (NS_FAILED(rv)) {
    DOM_CAMERA_LOGW("Unable to find a picture size close to %ux%u\n",
      aConfig.mPictureSize.width, aConfig.mPictureSize.height);
    return NS_ERROR_INVALID_ARG;
  }

  rv = LoadRecorderProfiles();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  nsString profileName = aConfig.mRecorderProfile;
  if (profileName.IsEmpty()) {
    profileName.AssignASCII("default");
  }

  RecorderProfile* profile;
  if (!mRecorderProfiles.Get(profileName, &profile)) {
    DOM_CAMERA_LOGE("Recorder profile '%s' is not supported\n",
      NS_ConvertUTF16toUTF8(aConfig.mRecorderProfile).get());
    return NS_ERROR_INVALID_ARG;
  }

  if (mCurrentConfiguration.mMode == aConfig.mMode &&
      mCurrentConfiguration.mRecorderProfile.Equals(profile->GetName()) &&
      mRequestedPreviewSize.Equals(aConfig.mPreviewSize) &&
      mCurrentConfiguration.mPictureSize.Equals(aValidatedConfig.mPictureSize))
  {
    DOM_CAMERA_LOGI("Camera configuration is unchanged\n");
    return NS_ERROR_ALREADY_INITIALIZED;
  }

  aValidatedConfig.mMode = aConfig.mMode;
  aValidatedConfig.mPreviewSize = aConfig.mPreviewSize;
  aValidatedConfig.mRecorderProfile = profile->GetName();
  mRequestedPreviewSize = aConfig.mPreviewSize;
  return NS_OK;
}

nsresult
nsGonkCameraControl::SetConfigurationInternal(const Configuration& aConfig)
{
  DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);

  ICameraControlParameterSetAutoEnter set(this);

  nsresult rv;
  switch (aConfig.mMode) {
    case kPictureMode:
      rv = SetPictureConfiguration(aConfig);
      break;

    case kVideoMode:
      rv = SetVideoConfiguration(aConfig);
      break;

    default:
      MOZ_ASSERT_UNREACHABLE("Unanticipated camera mode in SetConfigurationInternal()");
      rv = NS_ERROR_FAILURE;
      break;
  }

  DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    mRequestedPreviewSize.width = UINT32_MAX;
    mRequestedPreviewSize.height = UINT32_MAX;
    return rv;
  }

  rv = Set(CAMERA_PARAM_RECORDINGHINT, aConfig.mMode == kVideoMode);
  if (NS_FAILED(rv)) {
    DOM_CAMERA_LOGE("Failed to set recording hint (0x%x)\n", rv);
  }

  mCurrentConfiguration.mMode = aConfig.mMode;
  mCurrentConfiguration.mRecorderProfile = aConfig.mRecorderProfile;

  if (aConfig.mMode == kPictureMode) {
    mCurrentConfiguration.mPictureSize = aConfig.mPictureSize;
  } else /* if config.mMode == kVideoMode */ {
    // The following is best-effort; we don't currently support taking
    // pictures while in video mode, but we should at least return
    // sane values to OnConfigurationChange() handlers...
    SetPictureSizeImpl(aConfig.mPictureSize);
  }
  return NS_OK;
}

nsresult
nsGonkCameraControl::SetConfigurationImpl(const Configuration& aConfig)
{
  DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);

  if (aConfig.mMode == kUnspecifiedMode) {
    DOM_CAMERA_LOGW("Can't set camera mode to 'unspecified', ignoring\n");
    return NS_ERROR_INVALID_ARG;
  }

  Configuration config;
  nsresult rv = ValidateConfiguration(aConfig, config);
  if (rv == NS_ERROR_ALREADY_INITIALIZED) {
    // Configuration did not change, so no need to stop/start the preview
    // or push parameters to the camera hardware
    OnConfigurationChange();
    return NS_OK;
  } else if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Stop any currently running preview
  rv = PausePreview();
  if (NS_FAILED(rv)) {
    DOM_CAMERA_LOGW("PausePreview() in SetConfigurationImpl() failed (0x%x)\n", rv);
    if (rv == NS_ERROR_NOT_INITIALIZED) {
      // If there no hardware available, nothing else we try will work,
      // so bail out here.
      return rv;
    }
  }

  DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);
  rv = SetConfigurationInternal(config);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    StopPreviewImpl();
    return rv;
  }

  // Restart the preview
  DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);
  rv = StartPreviewInternal();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    StopPreviewImpl();
    return rv;
  }

  // OnConfigurationChange() indicates the success case of this operation.
  // It must not be fired until all intermediate steps, including starting
  // the preview, have completed successfully.
  OnConfigurationChange();
  OnPreviewStateChange(CameraControlListener::kPreviewStarted);
  return NS_OK;
}

nsresult
nsGonkCameraControl::MaybeAdjustVideoSize()
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);
  MOZ_ASSERT(mSeparateVideoAndPreviewSizesSupported);

  const Size& preview = mCurrentConfiguration.mPreviewSize;

  // Some camera drivers will ignore our preview size if it's larger
  // than the currently set video recording size, so in picture mode, we
  // give preview size priority, and bump up the video size just in case.
  // This is done on a best-effort basis.

  if (preview.width <= mLastRecorderSize.width &&
      preview.height <= mLastRecorderSize.height) {
    DOM_CAMERA_LOGI("Video size %ux%u is suitable for preview size %ux%u\n",
      mLastRecorderSize.width, mLastRecorderSize.height,
      preview.width, preview.height);
    return NS_OK;
  }

  nsTArray<Size> sizes;
  nsresult rv = Get(CAMERA_PARAM_SUPPORTED_VIDEOSIZES, sizes);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  const uint32_t previewArea = preview.width * preview.height;
  uint32_t bestDelta = UINT32_MAX;
  bool foundBest = false;
  SizeIndex best;

  for (SizeIndex i = 0; i < sizes.Length(); ++i) {
    const Size& s = sizes[i];
    if (s.width < preview.width || s.height < preview.height) {
      continue;
    }

    const uint32_t area = s.width * s.height;
    const uint32_t delta = area - previewArea;
    if (delta < bestDelta) {
      bestDelta = delta;
      best = i;
      foundBest = true;
    }
  }

  if (!foundBest) {
    // If no candidate was found, the driver will be fine with a video size
    // smaller than the chosen preview size.
    DOM_CAMERA_LOGI("No video size candidate for preview size %ux%u (0x%x)\n",
      preview.width, preview.height, rv);
    return NS_OK;
  }

  DOM_CAMERA_LOGI("Adjusting video size upwards to %ux%u\n",
    sizes[best].width, sizes[best].height);
  rv = Set(CAMERA_PARAM_VIDEOSIZE, sizes[best]);
  if (NS_FAILED(rv)) {
    DOM_CAMERA_LOGW("Failed to adjust video size for preview size %ux%u (0x%x)\n",
      preview.width, preview.height, rv);
    return rv;
  }

  mLastRecorderSize = preview;
  return NS_OK;
}

nsresult
nsGonkCameraControl::SetPictureConfiguration(const Configuration& aConfig)
{
  DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);

  Size max({0, 0});
  nsresult rv = SelectCaptureAndPreviewSize(aConfig.mPreviewSize,
                                            aConfig.mPictureSize, max,
                                            CAMERA_PARAM_PICTURE_SIZE);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (mSeparateVideoAndPreviewSizesSupported) {
    MaybeAdjustVideoSize();
  }

  rv = UpdateThumbnailSize();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  mParams.Get(CAMERA_PARAM_PREVIEWFRAMERATE, mPreviewFps);

  DOM_CAMERA_LOGI("picture mode preview: wanted %ux%u, got %ux%u (%u fps)\n",
                  aConfig.mPreviewSize.width, aConfig.mPreviewSize.height,
                  mCurrentConfiguration.mPreviewSize.width,
                  mCurrentConfiguration.mPreviewSize.height,
                  mPreviewFps);

  return NS_OK;
}

// Parameter management.
nsresult
nsGonkCameraControl::PushParameters()
{
  uint32_t dcu = mDeferConfigUpdate;
  if (dcu > 0) {
    DOM_CAMERA_LOGI("Defering config update (nest level %u)\n", dcu);
    return NS_OK;
  }

  /**
   * If we're already on the camera thread, call PushParametersImpl()
   * directly, so that it executes synchronously.  Some callers
   * require this so that changes take effect immediately before
   * we can proceed.
   */
  if (NS_GetCurrentThread() != mCameraThread) {
    DOM_CAMERA_LOGT("%s:%d - dispatching to Camera Thread\n", __func__, __LINE__);
    nsCOMPtr<nsIRunnable> pushParametersTask =
      NS_NewRunnableMethod(this, &nsGonkCameraControl::PushParametersImpl);
    return mCameraThread->Dispatch(pushParametersTask, NS_DISPATCH_NORMAL);
  }

  DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);
  return PushParametersImpl();
}

void
nsGonkCameraControl::BeginBatchParameterSet()
{
  uint32_t dcu = ++mDeferConfigUpdate;
  if (dcu == 0) {
    NS_WARNING("Overflow weirdness incrementing mDeferConfigUpdate!");
    MOZ_CRASH();
  }
  DOM_CAMERA_LOGI("Begin deferring camera configuration updates (nest level %u)\n", dcu);
}

void
nsGonkCameraControl::EndBatchParameterSet()
{
  uint32_t dcu = mDeferConfigUpdate--;
  if (dcu == 0) {
    NS_WARNING("Underflow badness decrementing mDeferConfigUpdate!");
    MOZ_CRASH();
  }
  DOM_CAMERA_LOGI("End deferring camera configuration updates (nest level %u)\n", dcu);

  if (dcu == 1) {
    PushParameters();
  }
}

template<class T> nsresult
nsGonkCameraControl::SetAndPush(uint32_t aKey, const T& aValue)
{
  nsresult rv = mParams.Set(aKey, aValue);
  if (NS_FAILED(rv)) {
    DOM_CAMERA_LOGE("Camera parameter aKey=%d failed to set (0x%x)\n", aKey, rv);
    return rv;
  }
  return PushParameters();
}

// Array-of-Size parameter accessor.
nsresult
nsGonkCameraControl::Get(uint32_t aKey, nsTArray<Size>& aSizes)
{
  if (aKey == CAMERA_PARAM_SUPPORTED_VIDEOSIZES &&
      !mSeparateVideoAndPreviewSizesSupported) {
    aKey = CAMERA_PARAM_SUPPORTED_PREVIEWSIZES;
  }

  return mParams.Get(aKey, aSizes);
}

// Array-of-doubles parameter accessor.
nsresult
nsGonkCameraControl::Get(uint32_t aKey, nsTArray<double>& aValues)
{
  return mParams.Get(aKey, aValues);
}

// Array-of-nsString parameter accessor.
nsresult
nsGonkCameraControl::Get(uint32_t aKey, nsTArray<nsString>& aValues)
{
  return mParams.Get(aKey, aValues);
}

// nsString-valued parameter accessors
nsresult
nsGonkCameraControl::Set(uint32_t aKey, const nsAString& aValue)
{
  nsresult rv = mParams.Set(aKey, aValue);
  if (NS_FAILED(rv)) {
    return rv;
  }

  switch (aKey) {
    case CAMERA_PARAM_PICTURE_FILEFORMAT:
      // Picture format -- need to keep it for the TakePicture() callback.
      mFileFormat = aValue;
      break;

    case CAMERA_PARAM_FLASHMODE:
      // Explicit flash mode changes always win and stick.
      mAutoFlashModeOverridden = false;
      break;

    case CAMERA_PARAM_SCENEMODE:
      // Reset disabling normal pictures in HDR mode in conjunction with setting
      // scene mode because some drivers require they be changed together.
      mParams.Set(CAMERA_PARAM_SCENEMODE_HDR_RETURNNORMALPICTURE, false);
      break;
  }

  return PushParameters();
}

nsresult
nsGonkCameraControl::Get(uint32_t aKey, nsAString& aRet)
{
  return mParams.Get(aKey, aRet);
}

// Double-valued parameter accessors
nsresult
nsGonkCameraControl::Set(uint32_t aKey, double aValue)
{
  return SetAndPush(aKey, aValue);
}

nsresult
nsGonkCameraControl::Get(uint32_t aKey, double& aRet)
{
  return mParams.Get(aKey, aRet);
}

// Signed-64-bit parameter accessors.
nsresult
nsGonkCameraControl::Set(uint32_t aKey, int64_t aValue)
{
  return SetAndPush(aKey, aValue);
}

nsresult
nsGonkCameraControl::Get(uint32_t aKey, int64_t& aRet)
{
  return mParams.Get(aKey, aRet);
}

// Boolean parameter accessors.
nsresult
nsGonkCameraControl::Set(uint32_t aKey, bool aValue)
{
  return SetAndPush(aKey, aValue);
}

nsresult
nsGonkCameraControl::Get(uint32_t aKey, bool& aRet)
{
  return mParams.Get(aKey, aRet);
}

// Weighted-region parameter accessors.
nsresult
nsGonkCameraControl::Set(uint32_t aKey, const nsTArray<Region>& aRegions)
{
  return SetAndPush(aKey, aRegions);
}

nsresult
nsGonkCameraControl::Get(uint32_t aKey, nsTArray<Region>& aRegions)
{
  return mParams.Get(aKey, aRegions);
}

// Singleton-size parameter accessors.
nsresult
nsGonkCameraControl::Set(uint32_t aKey, const Size& aSize)
{
  switch (aKey) {
    case CAMERA_PARAM_PICTURE_SIZE:
      DOM_CAMERA_LOGI("setting picture size to %ux%u\n", aSize.width, aSize.height);
      return SetPictureSize(aSize);

    case CAMERA_PARAM_THUMBNAILSIZE:
      DOM_CAMERA_LOGI("setting thumbnail size to %ux%u\n", aSize.width, aSize.height);
      return SetThumbnailSize(aSize);

    default:
      return SetAndPush(aKey, aSize);
  }
}

nsresult
nsGonkCameraControl::Get(uint32_t aKey, Size& aSize)
{
  return mParams.Get(aKey, aSize);
}

// Signed int parameter accessors.
nsresult
nsGonkCameraControl::Set(uint32_t aKey, int aValue)
{
  if (aKey == CAMERA_PARAM_PICTURE_ROTATION) {
    RETURN_IF_NO_CAMERA_HW();
    aValue = RationalizeRotation(aValue + mCameraHw->GetSensorOrientation());
  }
  return SetAndPush(aKey, aValue);
}

nsresult
nsGonkCameraControl::Get(uint32_t aKey, int& aRet)
{
  if (aKey == CAMERA_PARAM_SENSORANGLE) {
    RETURN_IF_NO_CAMERA_HW();
    aRet = mCameraHw->GetSensorOrientation();
    return NS_OK;
  }

  return mParams.Get(aKey, aRet);
}

// GPS location parameter accessors.
nsresult
nsGonkCameraControl::SetLocation(const Position& aLocation)
{
  return SetAndPush(CAMERA_PARAM_PICTURE_LOCATION, aLocation);
}

nsresult
nsGonkCameraControl::StartPreviewInternal()
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);
  RETURN_IF_NO_CAMERA_HW();

  ReentrantMonitorAutoEnter mon(mReentrantMonitor);

  if (mPreviewState == CameraControlListener::kPreviewStarted) {
    DOM_CAMERA_LOGW("Camera preview already started, nothing to do\n");
    return NS_OK;
  }

  DOM_CAMERA_LOGI("Starting preview (this=%p)\n", this);

  if (mCameraHw->StartPreview() != OK) {
    DOM_CAMERA_LOGE("Failed to start camera preview\n");
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

nsresult
nsGonkCameraControl::StartPreviewImpl()
{
  nsresult rv = StartPreviewInternal();
  if (NS_SUCCEEDED(rv)) {
    OnPreviewStateChange(CameraControlListener::kPreviewStarted);
  }
  return rv;
}

nsresult
nsGonkCameraControl::StopPreviewImpl()
{
  RETURN_IF_NO_CAMERA_HW();

  DOM_CAMERA_LOGI("Stopping preview (this=%p)\n", this);

  mCameraHw->StopPreview();
  OnPreviewStateChange(CameraControlListener::kPreviewStopped);
  return NS_OK;
}

nsresult
nsGonkCameraControl::PausePreview()
{
  RETURN_IF_NO_CAMERA_HW();

  DOM_CAMERA_LOGI("Pausing preview (this=%p)\n", this);

  mCameraHw->StopPreview();
  OnPreviewStateChange(CameraControlListener::kPreviewPaused);
  return NS_OK;
}

nsresult
nsGonkCameraControl::AutoFocusImpl()
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);
  RETURN_IF_NO_CAMERA_HW();

  DOM_CAMERA_LOGI("Starting auto focus\n");

  if (mCameraHw->AutoFocus() != OK) {
    return NS_ERROR_FAILURE;
  }

  ReentrantMonitorAutoEnter mon(mReentrantMonitor);
  mAutoFocusPending = true;
  if (mAutoFocusCompleteTimer) {
    mAutoFocusCompleteTimer->Cancel();
  }
  return NS_OK;
}

nsresult
nsGonkCameraControl::StartFaceDetectionImpl()
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);
  RETURN_IF_NO_CAMERA_HW();

  DOM_CAMERA_LOGI("Starting face detection\n");

  if (mCameraHw->StartFaceDetection() != OK) {
    return NS_ERROR_FAILURE;
  }
  return NS_OK;
}

nsresult
nsGonkCameraControl::StopFaceDetectionImpl()
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);
  RETURN_IF_NO_CAMERA_HW();

  DOM_CAMERA_LOGI("Stopping face detection\n");

  if (mCameraHw->StopFaceDetection() != OK) {
    return NS_ERROR_FAILURE;
  }
  return NS_OK;
}

nsresult
nsGonkCameraControl::SetThumbnailSizeImpl(const Size& aSize)
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);

  /**
   * We keep a copy of the specified size so that if the picture size
   * changes, we can choose a new thumbnail size close to what was asked for
   * last time.
   */
  mLastThumbnailSize = aSize;

  /**
   * If either of width or height is zero, set the other to zero as well.
   * This should disable inclusion of a thumbnail in the final picture.
   */
  if (!aSize.width || !aSize.height) {
    DOM_CAMERA_LOGW("Requested thumbnail size %ux%u, disabling thumbnail\n",
      aSize.width, aSize.height);
    Size size = { 0, 0 };
    return SetAndPush(CAMERA_PARAM_THUMBNAILSIZE, size);
  }

  /**
   * Choose the supported thumbnail size that is closest to the specified size.
   * Some drivers will fail to take a picture if the thumbnail does not have
   * the same aspect ratio as the set picture size, so we need to enforce that
   * too.
   */
  int smallestDelta = INT_MAX;
  uint32_t smallestDeltaIndex = UINT32_MAX;
  int targetArea = aSize.width * aSize.height;

  nsAutoTArray<Size, 8> supportedSizes;
  Get(CAMERA_PARAM_SUPPORTED_JPEG_THUMBNAIL_SIZES, supportedSizes);

  for (uint32_t i = 0; i < supportedSizes.Length(); ++i) {
    int area = supportedSizes[i].width * supportedSizes[i].height;
    int delta = abs(area - targetArea);

    if (area != 0 &&
        delta < smallestDelta &&
        supportedSizes[i].width * mCurrentConfiguration.mPictureSize.height ==
          mCurrentConfiguration.mPictureSize.width * supportedSizes[i].height) {
      smallestDelta = delta;
      smallestDeltaIndex = i;
    }
  }

  if (smallestDeltaIndex == UINT32_MAX) {
    DOM_CAMERA_LOGW("Unable to find a thumbnail size close to %ux%u, disabling thumbnail\n",
      aSize.width, aSize.height);
    // If we are unable to find a thumbnail size with a suitable aspect ratio,
    // just disable the thumbnail altogether.
    Size size = { 0, 0 };
    return SetAndPush(CAMERA_PARAM_THUMBNAILSIZE, size);
  }

  Size size = supportedSizes[smallestDeltaIndex];
  DOM_CAMERA_LOGI("camera-param set thumbnail-size = %ux%u (requested %ux%u)\n",
    size.width, size.height, aSize.width, aSize.height);
  if (size.width > INT32_MAX || size.height > INT32_MAX) {
    DOM_CAMERA_LOGE("Supported thumbnail size is too big, no change\n");
    return NS_ERROR_FAILURE;
  }

  return SetAndPush(CAMERA_PARAM_THUMBNAILSIZE, size);
}

android::sp<android::GonkCameraHardware>
nsGonkCameraControl::GetCameraHw()
{
  return mCameraHw;
}

nsresult
nsGonkCameraControl::SetThumbnailSize(const Size& aSize)
{
  class SetThumbnailSize : public nsRunnable
  {
  public:
    SetThumbnailSize(nsGonkCameraControl* aCameraControl, const Size& aSize)
      : mCameraControl(aCameraControl)
      , mSize(aSize)
    {
      MOZ_COUNT_CTOR(SetThumbnailSize);
    }
    ~SetThumbnailSize() { MOZ_COUNT_DTOR(SetThumbnailSize); }

    NS_IMETHODIMP
    Run() override
    {
      nsresult rv = mCameraControl->SetThumbnailSizeImpl(mSize);
      if (NS_FAILED(rv)) {
        mCameraControl->OnUserError(CameraControlListener::kInSetThumbnailSize, rv);
      }
      return NS_OK;
    }

  protected:
    RefPtr<nsGonkCameraControl> mCameraControl;
    Size mSize;
  };

  if (NS_GetCurrentThread() == mCameraThread) {
    return SetThumbnailSizeImpl(aSize);
  }

  return mCameraThread->Dispatch(new SetThumbnailSize(this, aSize), NS_DISPATCH_NORMAL);
}

nsresult
nsGonkCameraControl::UpdateThumbnailSize()
{
  return SetThumbnailSize(mLastThumbnailSize);
}

nsresult
nsGonkCameraControl::SetPictureSizeImpl(const Size& aSize)
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);

  /**
   * Some drivers are less friendly about getting one of these set to zero,
   * so if either is not specified, ignore both and go with current or
   * default settings.
   */
  if (!aSize.width || !aSize.height) {
    DOM_CAMERA_LOGW("Ignoring requested picture size of %ux%u\n", aSize.width, aSize.height);
    return NS_ERROR_INVALID_ARG;
  }

  if (aSize.width == mCurrentConfiguration.mPictureSize.width &&
      aSize.height == mCurrentConfiguration.mPictureSize.height) {
    DOM_CAMERA_LOGI("Requested picture size %ux%u unchanged\n", aSize.width, aSize.height);
    return NS_OK;
  }

  nsAutoTArray<Size, 8> supportedSizes;
  Get(CAMERA_PARAM_SUPPORTED_PICTURESIZES, supportedSizes);

  Size best;
  nsresult rv = GetSupportedSize(aSize, supportedSizes, best);
  if (NS_FAILED(rv)) {
    DOM_CAMERA_LOGW("Unable to find a picture size close to %ux%u\n",
      aSize.width, aSize.height);
    return NS_ERROR_INVALID_ARG;
  }

  DOM_CAMERA_LOGI("camera-param set picture-size = %ux%u (requested %ux%u)\n",
    best.width, best.height, aSize.width, aSize.height);
  if (best.width > INT32_MAX || best.height > INT32_MAX) {
    DOM_CAMERA_LOGE("Supported picture size is too big, no change\n");
    return NS_ERROR_FAILURE;
  }

  rv = mParams.Set(CAMERA_PARAM_PICTURE_SIZE, best);
  if (NS_FAILED(rv)) {
    return rv;
  }

  mCurrentConfiguration.mPictureSize = best;

  // Finally, update the thumbnail size in case the picture aspect ratio changed.
  // Some drivers will fail to take a picture if the thumbnail size is not the
  // same aspect ratio as the picture size.
  return UpdateThumbnailSize();
}

int32_t
nsGonkCameraControl::RationalizeRotation(int32_t aRotation)
{
  int32_t r = aRotation;

  // The result of this operation is an angle from 0..270 degrees,
  // in steps of 90 degrees. Angles are rounded to the nearest
  // magnitude, so 45 will be rounded to 90, and -45 will be rounded
  // to -90 (not 0).
  if (r >= 0) {
    r += 45;
  } else {
    r -= 45;
  }
  r /= 90;
  r %= 4;
  r *= 90;
  if (r < 0) {
    r += 360;
  }

  return r;
}

nsresult
nsGonkCameraControl::SetPictureSize(const Size& aSize)
{
  class SetPictureSize : public nsRunnable
  {
  public:
    SetPictureSize(nsGonkCameraControl* aCameraControl, const Size& aSize)
      : mCameraControl(aCameraControl)
      , mSize(aSize)
    {
      MOZ_COUNT_CTOR(SetPictureSize);
    }
    ~SetPictureSize() { MOZ_COUNT_DTOR(SetPictureSize); }

    NS_IMETHODIMP
    Run() override
    {
      nsresult rv = mCameraControl->SetPictureSizeImpl(mSize);
      if (NS_FAILED(rv)) {
        mCameraControl->OnUserError(CameraControlListener::kInSetPictureSize, rv);
      }
      return NS_OK;
    }

  protected:
    RefPtr<nsGonkCameraControl> mCameraControl;
    Size mSize;
  };

  if (NS_GetCurrentThread() == mCameraThread) {
    return SetPictureSizeImpl(aSize);
  }

  return mCameraThread->Dispatch(new SetPictureSize(this, aSize), NS_DISPATCH_NORMAL);
}

nsresult
nsGonkCameraControl::TakePictureImpl()
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);
  RETURN_IF_NO_CAMERA_HW();

  if (mCameraHw->TakePicture() != OK) {
    return NS_ERROR_FAILURE;
  }

  // In Gonk, taking a picture implicitly stops the preview stream,
  // so we need to reflect that here.
  OnPreviewStateChange(CameraControlListener::kPreviewPaused);
  return NS_OK;
}

nsresult
nsGonkCameraControl::PushParametersImpl()
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);
  DOM_CAMERA_LOGI("Pushing camera parameters\n");
  RETURN_IF_NO_CAMERA_HW();

  if (mCameraHw->PushParameters(mParams) != OK) {
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

nsresult
nsGonkCameraControl::PullParametersImpl()
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);
  DOM_CAMERA_LOGI("Pulling camera parameters\n");
  RETURN_IF_NO_CAMERA_HW();

  nsresult rv = mCameraHw->PullParameters(mParams);
  mParams.Get(CAMERA_PARAM_THUMBNAILSIZE, mLastThumbnailSize);
  mParams.Get(CAMERA_PARAM_PICTURE_SIZE, mCurrentConfiguration.mPictureSize);
  mParams.Get(CAMERA_PARAM_PREVIEWSIZE, mCurrentConfiguration.mPreviewSize);
  mParams.Get(CAMERA_PARAM_VIDEOSIZE, mLastRecorderSize);

  return rv;
}

nsresult
nsGonkCameraControl::SetupRecordingFlash(bool aAutoEnableLowLightTorch)
{
  mAutoFlashModeOverridden = false;

  if (!aAutoEnableLowLightTorch || !mLuminanceSupported || !mFlashSupported) {
    return NS_OK;
  }

  DOM_CAMERA_LOGI("Luminance reporting and flash supported\n");

  nsresult rv = PullParametersImpl();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  nsString luminance;
  rv = mParams.Get(CAMERA_PARAM_LUMINANCE, luminance);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    // If we failed to get the luminance, assume it's "high"
    return NS_OK;
  }

  nsString flashMode;
  rv = mParams.Get(CAMERA_PARAM_FLASHMODE, flashMode);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    // If we failed to get the current flash mode, swallow the error
    return NS_OK;
  }

  if (luminance.EqualsASCII("low") && flashMode.EqualsASCII("auto")) {
    DOM_CAMERA_LOGI("Low luminance detected, turning on flash\n");
    rv = SetAndPush(CAMERA_PARAM_FLASHMODE, NS_LITERAL_STRING("torch"));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      // If we failed to turn on the flash, swallow the error
      return NS_OK;
    }

    mAutoFlashModeOverridden = true;
  }

  return NS_OK;
}

nsresult
nsGonkCameraControl::StartRecordingImpl(DeviceStorageFileDescriptor* aFileDescriptor,
                                        const StartRecordingOptions* aOptions)
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);

  ReentrantMonitorAutoEnter mon(mRecorderMonitor);

  NS_ENSURE_TRUE(!mCurrentConfiguration.mRecorderProfile.IsEmpty(), NS_ERROR_NOT_INITIALIZED);
#ifdef MOZ_WIDGET_GONK
  NS_ENSURE_FALSE(mRecorder, NS_ERROR_FAILURE);
#endif

  /**
   * Get the base path from device storage and append the app-specified
   * filename to it.  The filename may include a relative subpath
   * (e.g.) "DCIM/IMG_0001.jpg".
   *
   * The camera app needs to provide the file extension '.3gp' for now.
   * See bug 795202.
   */
  if (NS_WARN_IF(!aFileDescriptor)) {
    return NS_ERROR_INVALID_ARG;
  }
  nsAutoString fullPath;
  mVideoFile = aFileDescriptor->mDSFile;
  mVideoFile->GetFullPath(fullPath);
  DOM_CAMERA_LOGI("Video filename is '%s'\n",
                  NS_LossyConvertUTF16toASCII(fullPath).get());

  if (!mVideoFile->IsSafePath()) {
    DOM_CAMERA_LOGE("Invalid video file name\n");
    return NS_ERROR_INVALID_ARG;
  }

  // SetupRecording creates a dup of the file descriptor, so we need to
  // close the file descriptor when we leave this function. Also note, that
  // since we're already off the main thread, we don't need to dispatch this.
  // We just let the CloseFileRunnable destructor do the work.
  RefPtr<CloseFileRunnable> closer;
  if (aFileDescriptor->mFileDescriptor.IsValid()) {
    closer = new CloseFileRunnable(aFileDescriptor->mFileDescriptor);
  }
  nsresult rv;
  int fd = aFileDescriptor->mFileDescriptor.PlatformHandle();
  if (aOptions) {
    rv = SetupRecording(fd, aOptions->rotation, aOptions->maxFileSizeBytes,
                        aOptions->maxVideoLengthMs);
    if (NS_SUCCEEDED(rv)) {
      rv = SetupRecordingFlash(aOptions->autoEnableLowLightTorch);
    }
  } else {
    rv = SetupRecording(fd, 0, 0, 0);
  }
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

#ifdef MOZ_WIDGET_GONK
  if (mRecorder->start() != OK) {
    DOM_CAMERA_LOGE("mRecorder->start() failed\n");
    // important: we MUST destroy the recorder if start() fails!
    mRecorder = nullptr;
    // put the flash back to the 'auto' state
    if (mAutoFlashModeOverridden) {
      SetAndPush(CAMERA_PARAM_FLASHMODE, NS_LITERAL_STRING("auto"));
    }
    return NS_ERROR_FAILURE;
  }
#endif

  OnRecorderStateChange(CameraControlListener::kRecorderStarted);
  mCapturePoster = aOptions->createPoster;
  return NS_OK;
}

nsresult
nsGonkCameraControl::StopRecordingImpl()
{
  class RecordingComplete : public nsRunnable
  {
  public:
    RecordingComplete(already_AddRefed<DeviceStorageFile> aFile)
      : mFile(aFile)
    { }

    ~RecordingComplete() { }

    NS_IMETHODIMP
    Run()
    {
      MOZ_ASSERT(NS_IsMainThread());

      nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
      obs->NotifyObservers(mFile, "file-watcher-notify", MOZ_UTF16("modified"));
      return NS_OK;
    }

  private:
    RefPtr<DeviceStorageFile> mFile;
  };

  ReentrantMonitorAutoEnter mon(mRecorderMonitor);

#ifdef MOZ_WIDGET_GONK
  // nothing to do if we have no mRecorder
  if (!mRecorder) {
    return NS_OK;
  }

  mRecorder->stop();
  mRecorder = nullptr;
#else
  if (!mVideoFile) {
    return NS_OK;
  }
#endif
  if (mCapturePoster.exchange(false)) {
    OnPoster(nullptr, 0);
  }
  OnRecorderStateChange(CameraControlListener::kRecorderStopped);

  {
    ICameraControlParameterSetAutoEnter set(this);

    if (mAutoFlashModeOverridden) {
      nsresult rv = Set(CAMERA_PARAM_FLASHMODE, NS_LITERAL_STRING("auto"));
      if (NS_FAILED(rv)) {
        DOM_CAMERA_LOGE("Failed to set flash mode (0x%x)\n", rv);
      }
    }
  }

  // notify DeviceStorage that the new video file is closed and ready
  return NS_DispatchToMainThread(new RecordingComplete(mVideoFile.forget()));
}

nsresult
nsGonkCameraControl::PauseRecordingImpl()
{
  ReentrantMonitorAutoEnter mon(mRecorderMonitor);

#ifdef MOZ_WIDGET_GONK
  if (!mRecorder) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  int err = mRecorder->pause();
  switch (err) {
    case OK:
      break;
    case INVALID_OPERATION:
      return NS_ERROR_NOT_IMPLEMENTED;
    default:
      return NS_ERROR_FAILURE;
  }
#endif
  OnRecorderStateChange(CameraControlListener::kRecorderPaused);
  return NS_OK;
}

nsresult
nsGonkCameraControl::ResumeRecordingImpl()
{
  ReentrantMonitorAutoEnter mon(mRecorderMonitor);

#ifdef MOZ_WIDGET_GONK
  if (!mRecorder) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  if (mRecorder->resume() != OK) {
    return NS_ERROR_FAILURE;
  }
#endif
  OnRecorderStateChange(CameraControlListener::kRecorderResumed);
  return NS_OK;
}

nsresult
nsGonkCameraControl::ResumeContinuousFocusImpl()
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);
  RETURN_IF_NO_CAMERA_HW();

  DOM_CAMERA_LOGI("Resuming continuous autofocus\n");

  // see
  // http://developer.android.com/reference/android/hardware/Camera.Parameters.html#FOCUS_MODE_CONTINUOUS_PICTURE
  if (NS_WARN_IF(mCameraHw->CancelAutoFocus() != OK)) {
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

class AutoFocusMovingTimerCallback : public nsITimerCallback
{
public:
  NS_DECL_THREADSAFE_ISUPPORTS

  AutoFocusMovingTimerCallback(nsGonkCameraControl* aCameraControl)
    : mCameraControl(aCameraControl)
  { }

  NS_IMETHODIMP
  Notify(nsITimer* aTimer)
  {
    mCameraControl->OnAutoFocusComplete(true, true);
    return NS_OK;
  }

protected:
  virtual ~AutoFocusMovingTimerCallback()
  { }

  RefPtr<nsGonkCameraControl> mCameraControl;
};

NS_IMPL_ISUPPORTS(AutoFocusMovingTimerCallback, nsITimerCallback);

void
nsGonkCameraControl::OnAutoFocusMoving(bool aIsMoving)
{
  CameraControlImpl::OnAutoFocusMoving(aIsMoving);

  if (!aIsMoving) {
    /* Some drivers do not signal us with the status of the continuous auto focus
       operation, only the moving signal which comes first. As a result we need to
       arm a timer to detect the driver behaviour and if necessary generate the
       signal ourselves to update the application state. */
    int32_t expiredCount = 0;

    {
      ReentrantMonitorAutoEnter mon(mReentrantMonitor);

      if (mAutoFocusCompleteTimer) {
        mAutoFocusCompleteTimer->Cancel();

        if (!mAutoFocusPending) {
          RefPtr<nsITimerCallback> timerCb = new AutoFocusMovingTimerCallback(this);
          nsresult rv = mAutoFocusCompleteTimer->InitWithCallback(timerCb,
                                                                  kAutoFocusCompleteTimeoutMs,
                                                                  nsITimer::TYPE_ONE_SHOT);
          NS_WARN_IF(NS_FAILED(rv));
        }
        return;
      }

      if (!mAutoFocusPending) {
        expiredCount = mAutoFocusCompleteExpired;
      }
    }

    if (expiredCount == kAutoFocusCompleteTimeoutLimit) {
      OnAutoFocusComplete(true, true);
    }
  }
}

void
nsGonkCameraControl::OnAutoFocusComplete(bool aSuccess, bool aExpired)
{
  class AutoFocusComplete : public nsRunnable
  {
  public:
    AutoFocusComplete(nsGonkCameraControl* aCameraControl, bool aSuccess, bool aExpired)
      : mCameraControl(aCameraControl)
      , mSuccess(aSuccess)
      , mExpired(aExpired)
    { }

    NS_IMETHODIMP
    Run() override
    {
      mCameraControl->OnAutoFocusComplete(mSuccess, mExpired);
      return NS_OK;
    }

  protected:
    RefPtr<nsGonkCameraControl> mCameraControl;
    bool mSuccess;
    bool mExpired;
  };

  if (NS_GetCurrentThread() == mCameraThread) {
    {
      ReentrantMonitorAutoEnter mon(mReentrantMonitor);

      if (mAutoFocusPending) {
        mAutoFocusPending = false;
      } else if (mAutoFocusCompleteTimer) {
        if (aExpired) {
          NS_WARNING("Camera timed out waiting for OnAutoFocusComplete");
          ++mAutoFocusCompleteExpired;
        } else {
          mAutoFocusCompleteTimer->Cancel();
          --mAutoFocusCompleteExpired;
        }

        if (mAutoFocusCompleteExpired == kAutoFocusCompleteTimeoutLimit ||
            mAutoFocusCompleteExpired == -kAutoFocusCompleteTimeoutLimit)
        {
          mAutoFocusCompleteTimer = nullptr;
        }
      }
    }

    /**
     * Auto focusing can change some of the camera's parameters, so
     * we need to pull a new set before notifying any clients.
     */
    PullParametersImpl();
    CameraControlImpl::OnAutoFocusComplete(aSuccess);
    return;
  }

  /**
   * Because the callback needs to call PullParametersImpl(),
   * we need to dispatch this callback through the Camera Thread.
   */
  mCameraThread->Dispatch(new AutoFocusComplete(this, aSuccess, aExpired), NS_DISPATCH_NORMAL);
}

bool
FeatureDetected(int32_t feature[])
{
  /**
   * For information on what constitutes a valid feature, see:
   * http://androidxref.com/4.0.4/xref/system/core/include/system/camera.h#202
   *
   * Although the comments explicitly state that undetected features are
   * indicated using the value -2000, we conservatively include anything
   * outside the explicitly valid range of [-1000, 1000] as undetected
   * as well.
   */
  const int32_t kLowerFeatureBound = -1000;
  const int32_t kUpperFeatureBound = 1000;
  return (feature[0] >= kLowerFeatureBound && feature[0] <= kUpperFeatureBound) ||
         (feature[1] >= kLowerFeatureBound && feature[1] <= kUpperFeatureBound);
}

void
nsGonkCameraControl::OnFacesDetected(camera_frame_metadata_t* aMetaData)
{
  NS_ENSURE_TRUE_VOID(aMetaData);

  nsTArray<Face> faces;
  uint32_t numFaces = aMetaData->number_of_faces;
  if (numFaces == 0 && mPrevFacesDetected == 0) {
    return;
  }
  mPrevFacesDetected = numFaces;

  DOM_CAMERA_LOGI("Camera detected %d face(s)", numFaces);

  faces.SetCapacity(numFaces);

  for (uint32_t i = 0; i < numFaces; ++i) {
    Face* f = faces.AppendElement();

    f->id = aMetaData->faces[i].id;
    f->score = aMetaData->faces[i].score;
    if (f->score > 100) {
      f->score = 100;
    }
    f->bound.left = aMetaData->faces[i].rect[0];
    f->bound.top = aMetaData->faces[i].rect[1];
    f->bound.right = aMetaData->faces[i].rect[2];
    f->bound.bottom = aMetaData->faces[i].rect[3];
    DOM_CAMERA_LOGI("Camera face[%u] appended: id=%d, score=%d, bound=(%d, %d)-(%d, %d)\n",
      i, f->id, f->score, f->bound.left, f->bound.top, f->bound.right, f->bound.bottom);

    f->hasLeftEye = FeatureDetected(aMetaData->faces[i].left_eye);
    if (f->hasLeftEye) {
      f->leftEye.x = aMetaData->faces[i].left_eye[0];
      f->leftEye.y = aMetaData->faces[i].left_eye[1];
      DOM_CAMERA_LOGI("    Left eye detected at (%d, %d)\n",
        f->leftEye.x, f->leftEye.y);
    } else {
      DOM_CAMERA_LOGI("    No left eye detected\n");
    }

    f->hasRightEye = FeatureDetected(aMetaData->faces[i].right_eye);
    if (f->hasRightEye) {
      f->rightEye.x = aMetaData->faces[i].right_eye[0];
      f->rightEye.y = aMetaData->faces[i].right_eye[1];
      DOM_CAMERA_LOGI("    Right eye detected at (%d, %d)\n",
        f->rightEye.x, f->rightEye.y);
    } else {
      DOM_CAMERA_LOGI("    No right eye detected\n");
    }

    f->hasMouth = FeatureDetected(aMetaData->faces[i].mouth);
    if (f->hasMouth) {
      f->mouth.x = aMetaData->faces[i].mouth[0];
      f->mouth.y = aMetaData->faces[i].mouth[1];
      DOM_CAMERA_LOGI("    Mouth detected at (%d, %d)\n", f->mouth.x, f->mouth.y);
    } else {
      DOM_CAMERA_LOGI("    No mouth detected\n");
    }
  }

  CameraControlImpl::OnFacesDetected(faces);
}

void
nsGonkCameraControl::OnTakePictureComplete(uint8_t* aData, uint32_t aLength)
{
  ReentrantMonitorAutoEnter mon(mReentrantMonitor);

  nsString s(NS_LITERAL_STRING("image/"));
  s.Append(mFileFormat);
  DOM_CAMERA_LOGI("Got picture, type '%s', %u bytes\n", NS_ConvertUTF16toUTF8(s).get(), aLength);
  OnTakePictureComplete(aData, aLength, s);

  if (mResumePreviewAfterTakingPicture) {
    nsresult rv = StartPreview();
    if (NS_FAILED(rv)) {
      DOM_CAMERA_LOGE("Failed to restart camera preview (%x)\n", rv);
      OnPreviewStateChange(CameraControlListener::kPreviewStopped);
    }
  }

  DOM_CAMERA_LOGI("nsGonkCameraControl::OnTakePictureComplete() done\n");
}

void
nsGonkCameraControl::OnTakePictureError()
{
  CameraControlImpl::OnUserError(CameraControlListener::kInTakePicture,
                                 NS_ERROR_FAILURE);
}

nsresult
nsGonkCameraControl::GetSupportedSize(const Size& aSize,
                                      const nsTArray<Size>& aSupportedSizes,
                                      Size& best)
{
  nsresult rv = NS_ERROR_INVALID_ARG;
  best = aSize;
  uint32_t minSizeDelta = UINT32_MAX;
  uint32_t delta;

  if (aSupportedSizes.IsEmpty()) {
    // no valid sizes
    return rv;
  }

  if (!aSize.width && !aSize.height) {
    // no size specified, take the first supported size
    best = aSupportedSizes[0];
    return NS_OK;
  } else if (aSize.width && aSize.height) {
    // both height and width specified, find the supported size closest to
    // the requested size, looking for an exact match first
    for (SizeIndex i = 0; i < aSupportedSizes.Length(); ++i) {
      Size size = aSupportedSizes[i];
      if (size.width == aSize.width && size.height == aSize.height) {
        best = size;
        return NS_OK;
      }
    }

    // no exact match on dimensions--look for a match closest in area
    const uint32_t targetArea = aSize.width * aSize.height;
    for (SizeIndex i = 0; i < aSupportedSizes.Length(); i++) {
      Size size = aSupportedSizes[i];
      uint32_t delta =
        abs(static_cast<long int>(size.width * size.height - targetArea));
      if (delta < minSizeDelta) {
        minSizeDelta = delta;
        best = size;
        rv = NS_OK;
      }
    }
  } else if (!aSize.width) {
    // width not specified, find closest height match
    for (SizeIndex i = 0; i < aSupportedSizes.Length(); i++) {
      Size size = aSupportedSizes[i];
      delta = abs(static_cast<long int>(size.height - aSize.height));
      if (delta < minSizeDelta) {
        minSizeDelta = delta;
        best = size;
        rv = NS_OK;
      }
    }
  } else if (!aSize.height) {
    // height not specified, find closest width match
    for (SizeIndex i = 0; i < aSupportedSizes.Length(); i++) {
      Size size = aSupportedSizes[i];
      delta = abs(static_cast<long int>(size.width - aSize.width));
      if (delta < minSizeDelta) {
        minSizeDelta = delta;
        best = size;
        rv = NS_OK;
      }
    }
  }

  return rv;
}

nsresult
nsGonkCameraControl::SelectCaptureAndPreviewSize(const Size& aPreviewSize,
                                                 const Size& aCaptureSize,
                                                 const Size& aMaxSize,
                                                 uint32_t aCaptureSizeKey)
{
  MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);

  // At this point, we know the capture size has been validated and replaced
  // if necessary with the best matching supported value.
  DOM_CAMERA_LOGI("Select capture size %ux%u, preview size %ux%u, maximum size %ux%u\n",
                  aCaptureSize.width, aCaptureSize.height,
                  aPreviewSize.width, aPreviewSize.height,
                  aMaxSize.width, aMaxSize.height);

  nsAutoTArray<Size, 16> sizes;
  nsresult rv = Get(CAMERA_PARAM_SUPPORTED_PREVIEWSIZES, sizes);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // May optionally apply a ceiling to the preview size. Any supported preview
  // size with an area larger than the maximum will be ignored regardless of
  // aspect ratio or delta to requested preview size.
  uint32_t maxArea = aMaxSize.width * aMaxSize.height;
  if (maxArea == 0) {
    maxArea = UINT32_MAX;
  }

  const uint32_t previewArea = aPreviewSize.width * aPreviewSize.height;

  // We should select a preview size with the same aspect ratio as the capture
  // size and minimize the delta with the requested preview size. If we are
  // unable to find any supported preview sizes which match the aspect ratio
  // of the capture size, we fallback to only minimizing the delta with the
  // requested preview size.

  SizeIndex bestSizeMatch = 0; // initializers to keep warnings away
  SizeIndex bestSizeMatchWithAspectRatio = 0;
  bool foundSizeMatch = false;
  bool foundSizeMatchWithAspectRatio = false;

  uint32_t bestAreaDelta = UINT32_MAX;
  uint32_t bestAreaDeltaWithAspect = UINT32_MAX;

  for (SizeIndex i = 0; i < sizes.Length(); ++i) {
    const Size& s = sizes[i];

    // preview size must be smaller or equal to the capture size
    if (aCaptureSize.width < s.width || aCaptureSize.height < s.height) {
      continue;
    }

    const uint32_t area = s.width * s.height;
    if (area > maxArea) {
      continue;
    }

    const uint32_t delta = abs(static_cast<long int>(previewArea - area));
    if (s.width * aCaptureSize.height == aCaptureSize.width * s.height) {
      if (delta == 0) {
        // exact match, including aspect ratio--we can stop now
        bestSizeMatchWithAspectRatio = i;
        foundSizeMatchWithAspectRatio = true;
        break;
      } else if (delta < bestAreaDeltaWithAspect) {
        // aspect ratio match
        bestAreaDeltaWithAspect = delta;
        bestSizeMatchWithAspectRatio = i;
        foundSizeMatchWithAspectRatio = true;
      }
    } else if (delta < bestAreaDelta) {
      bestAreaDelta = delta;
      bestSizeMatch = i;
      foundSizeMatch = true;
    }
  }

  Size previewSize;
  if (foundSizeMatchWithAspectRatio) {
    previewSize = sizes[bestSizeMatchWithAspectRatio];
  } else if (foundSizeMatch) {
    DOM_CAMERA_LOGW("Unable to match a preview size with aspect ratio of capture size %ux%u\n",
      aCaptureSize.width, aCaptureSize.height);
    previewSize = sizes[bestSizeMatch];
  } else {
    DOM_CAMERA_LOGE("Unable to find a preview size for capture size %ux%u\n",
      aCaptureSize.width, aCaptureSize.height);
    return NS_ERROR_INVALID_ARG;
  }

  DOM_CAMERA_LOGI("Setting capture size to %ux%u, preview size to %ux%u\n",
                  aCaptureSize.width, aCaptureSize.height,
                  previewSize.width, previewSize.height);

  Size oldSize;
  rv = Get(CAMERA_PARAM_PREVIEWSIZE, oldSize);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = Set(CAMERA_PARAM_PREVIEWSIZE, previewSize);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  rv = Set(aCaptureSizeKey, aCaptureSize);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    Set(CAMERA_PARAM_PREVIEWSIZE, oldSize); // error, try to restore the original preview size
    return rv;
  }

  mCurrentConfiguration.mPreviewSize = previewSize;
  return NS_OK;
}

nsresult
nsGonkCameraControl::SetVideoConfiguration(const Configuration& aConfig)
{
  DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);

  RecorderProfile* profile;
  if (!mRecorderProfiles.Get(aConfig.mRecorderProfile, &profile)) {
    DOM_CAMERA_LOGE("Recorder profile '%s' is not supported\n",
      NS_ConvertUTF16toUTF8(aConfig.mRecorderProfile).get());
    return NS_ERROR_INVALID_ARG;
  }

  const RecorderProfile::Video& video(profile->GetVideo());
  const Size& size = video.GetSize();
  const uint32_t fps = video.GetFramesPerSecond();
  if (fps == 0 || fps > INT_MAX || size.width == 0 || size.height == 0) {
    DOM_CAMERA_LOGE("Can't configure video with fps=%u, width=%u, height=%u\n",
      fps, size.width, size.height);
    return NS_ERROR_FAILURE;
  }

  PullParametersImpl();

  {
    ICameraControlParameterSetAutoEnter set(this);
    nsresult rv;

    if (mSeparateVideoAndPreviewSizesSupported) {
      // The camera supports two video streams: a low(er) resolution preview
      // stream and and a potentially high(er) resolution stream for encoding.
      Size preferred;
      rv = Get(CAMERA_PARAM_PREFERRED_PREVIEWSIZE_FOR_VIDEO, preferred);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }

      rv = SelectCaptureAndPreviewSize(aConfig.mPreviewSize, size, preferred,
                                       CAMERA_PARAM_VIDEOSIZE);
      if (NS_FAILED(rv)) {
        DOM_CAMERA_LOGE("Failed to set video and preview sizes (0x%x)\n", rv);
        return rv;
      }
    } else {
      // The camera only supports a single video stream: in this case, we set
      // the preview size to be the desired video recording size, and ignore
      // the specified preview size.
      rv = Set(CAMERA_PARAM_PREVIEWSIZE, size);
      if (NS_FAILED(rv)) {
        DOM_CAMERA_LOGE("Failed to set video mode preview size (0x%x)\n", rv);
        return rv;
      }

      mCurrentConfiguration.mPreviewSize = size;
    }

    mLastRecorderSize = size;

    rv = Set(CAMERA_PARAM_PREVIEWFRAMERATE, static_cast<int>(fps));
    if (NS_FAILED(rv)) {
      DOM_CAMERA_LOGE("Failed to set video mode frame rate (0x%x)\n", rv);
      return rv;
    }
  }

  mPreviewFps = fps;
  return NS_OK;
}

#ifdef MOZ_WIDGET_GONK
class GonkRecorderListener : public IMediaRecorderClient
{
public:
  GonkRecorderListener(nsGonkCameraControl* aCameraControl)
    : mCameraControl(aCameraControl)
  {
    DOM_CAMERA_LOGT("%s:%d : this=%p, aCameraControl=%p\n",
      __func__, __LINE__, this, mCameraControl.get());
  }

  void notify(int msg, int ext1, int ext2)
  {
    if (mCameraControl) {
      mCameraControl->OnRecorderEvent(msg, ext1, ext2);
    }
  }

  IBinder* onAsBinder()
  {
    DOM_CAMERA_LOGE("onAsBinder() called, should NEVER get called!\n");
    return nullptr;
  }

protected:
  ~GonkRecorderListener() { }
  RefPtr<nsGonkCameraControl> mCameraControl;
};

void
nsGonkCameraControl::OnRecorderEvent(int msg, int ext1, int ext2)
{
  /**
   * Refer to base/include/media/mediarecorder.h for a complete list
   * of error and info message codes.  There are duplicate values
   * within the status/error code space, as determined by code inspection:
   *
   *    +------- msg
   *    | +----- ext1
   *    | | +--- ext2
   *    V V V
   *    1           MEDIA_RECORDER_EVENT_ERROR
   *      1         MEDIA_RECORDER_ERROR_UNKNOWN
   *        [3]     ERROR_MALFORMED
   *      100       mediaplayer.h::MEDIA_ERROR_SERVER_DIED
   *        0       <always zero>
   *    2           MEDIA_RECORDER_EVENT_INFO
   *      800       MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
   *        0       <always zero>
   *      801       MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED
   *        0       <always zero>
   *      1000      MEDIA_RECORDER_TRACK_INFO_COMPLETION_STATUS[1b]
   *        [3]     UNKNOWN_ERROR, etc.
   *    100         MEDIA_ERROR[4]
   *      100       mediaplayer.h::MEDIA_ERROR_SERVER_DIED
   *        0       <always zero>
   *    100         MEDIA_RECORDER_TRACK_EVENT_ERROR
   *      100       MEDIA_RECORDER_TRACK_ERROR_GENERAL[1a]
   *        [3]     UNKNOWN_ERROR, etc.
   *      200       MEDIA_RECORDER_ERROR_VIDEO_NO_SYNC_FRAME[2]
   *        ?       <unknown>
   *    101         MEDIA_RECORDER_TRACK_EVENT_INFO
   *      1000      MEDIA_RECORDER_TRACK_INFO_COMPLETION_STATUS[1a]
   *        [3]     UNKNOWN_ERROR, etc.
   *      N         see mediarecorder.h::media_recorder_info_type[5]
   *
   * 1. a) High 4 bits are the track number, the next 12 bits are reserved,
   *       and the final 16 bits are the actual error code (above).
   *    b) But not in this case.
   * 2. Never actually used in AOSP code?
   * 3. Specific error codes are from utils/Errors.h and/or
   *    include/media/stagefright/MediaErrors.h.
   * 4. Only in frameworks/base/media/libmedia/mediaplayer.cpp.
   * 5. These are mostly informational and we can ignore them; note that
   *    although the MEDIA_RECORDER_INFO_MAX_DURATION_REACHED and
   *    MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED values are defined in this
   *    enum, they are used with different ext1 codes.  /o\
   */
  int trackNum = CameraControlListener::kNoTrackNumber;

  switch (msg) {
    // Recorder-related events
    case MEDIA_RECORDER_EVENT_INFO:
      switch (ext1) {
        case MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED:
          DOM_CAMERA_LOGI("recorder-event : info: maximum file size reached\n");
          OnRecorderStateChange(CameraControlListener::kFileSizeLimitReached, ext2, trackNum);
          return;

        case MEDIA_RECORDER_INFO_MAX_DURATION_REACHED:
          DOM_CAMERA_LOGI("recorder-event : info: maximum video duration reached\n");
          OnRecorderStateChange(CameraControlListener::kVideoLengthLimitReached, ext2, trackNum);
          return;

        case MEDIA_RECORDER_TRACK_INFO_COMPLETION_STATUS:
          DOM_CAMERA_LOGI("recorder-event : info: track completed\n");
          OnRecorderStateChange(CameraControlListener::kTrackCompleted, ext2, trackNum);
          return;
      }
      break;

    case MEDIA_RECORDER_EVENT_ERROR:
      switch (ext1) {
        case MEDIA_RECORDER_ERROR_UNKNOWN:
          DOM_CAMERA_LOGE("recorder-event : recorder-error: %d (0x%08x)\n", ext2, ext2);
          OnRecorderStateChange(CameraControlListener::kMediaRecorderFailed, ext2, trackNum);
          return;

        case MEDIA_ERROR_SERVER_DIED:
          DOM_CAMERA_LOGE("recorder-event : recorder-error: server died\n");
          OnRecorderStateChange(CameraControlListener::kMediaServerFailed, ext2, trackNum);
          return;
      }
      break;

    // Track-related events, see note 1(a) above.
    case MEDIA_RECORDER_TRACK_EVENT_INFO:
      trackNum = (ext1 & 0xF0000000) >> 28;
      ext1 &= 0xFFFF;
      switch (ext1) {
        case MEDIA_RECORDER_TRACK_INFO_COMPLETION_STATUS:
          if (ext2 == OK) {
            DOM_CAMERA_LOGI("recorder-event : track-complete: track %d, %d (0x%08x)\n", trackNum, ext2, ext2);
            OnRecorderStateChange(CameraControlListener::kTrackCompleted, ext2, trackNum);
            return;
          }
          DOM_CAMERA_LOGE("recorder-event : track-error: track %d, %d (0x%08x)\n", trackNum, ext2, ext2);
          OnRecorderStateChange(CameraControlListener::kTrackFailed, ext2, trackNum);
          return;

        case MEDIA_RECORDER_TRACK_INFO_PROGRESS_IN_TIME:
          DOM_CAMERA_LOGI("recorder-event : track-info: progress in time: %d ms\n", ext2);
          return;
      }
      break;

    case MEDIA_RECORDER_TRACK_EVENT_ERROR:
      trackNum = (ext1 & 0xF0000000) >> 28;
      ext1 &= 0xFFFF;
      DOM_CAMERA_LOGE("recorder-event : track-error: track %d, %d (0x%08x)\n", trackNum, ext2, ext2);
      OnRecorderStateChange(CameraControlListener::kTrackFailed, ext2, trackNum);
      return;
  }

  // All unhandled cases wind up here
  DOM_CAMERA_LOGW("recorder-event : unhandled: msg=%d, ext1=%d, ext2=%d\n", msg, ext1, ext2);
}
#endif

nsresult
nsGonkCameraControl::SetupRecording(int aFd, int aRotation,
                                    uint64_t aMaxFileSizeBytes,
                                    uint64_t aMaxVideoLengthMs)
{
  RETURN_IF_NO_CAMERA_HW();

#ifdef MOZ_WIDGET_GONK
  // choosing a size big enough to hold the params
  const size_t SIZE = 256;
  char buffer[SIZE];

  ReentrantMonitorAutoEnter mon(mRecorderMonitor);

  mRecorder = new GonkRecorder();
  CHECK_SETARG_RETURN(mRecorder->init(), NS_ERROR_FAILURE);

  nsresult rv =
    GonkRecorderProfile::ConfigureRecorder(*mRecorder, mCameraId,
                                           mCurrentConfiguration.mRecorderProfile);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  CHECK_SETARG_RETURN(mRecorder->setCamera(mCameraHw), NS_ERROR_FAILURE);

  DOM_CAMERA_LOGI("maxVideoLengthMs=%llu\n", aMaxVideoLengthMs);
  const uint64_t kMaxVideoLengthMs = INT64_MAX / 1000;
  if (aMaxVideoLengthMs == 0) {
    aMaxVideoLengthMs = -1;
  } else if (aMaxVideoLengthMs > kMaxVideoLengthMs) {
    // GonkRecorder parameters are internally limited to signed 64-bit values,
    // and the time length limit is converted from milliseconds to microseconds,
    // so we limit this value to prevent any unexpected overflow weirdness.
    DOM_CAMERA_LOGW("maxVideoLengthMs capped to %lld\n", kMaxVideoLengthMs);
    aMaxVideoLengthMs = kMaxVideoLengthMs;
  }
  snprintf(buffer, SIZE, "max-duration=%lld", aMaxVideoLengthMs);
  CHECK_SETARG_RETURN(mRecorder->setParameters(String8(buffer)),
                      NS_ERROR_INVALID_ARG);

  DOM_CAMERA_LOGI("maxFileSizeBytes=%llu\n", aMaxFileSizeBytes);
  if (aMaxFileSizeBytes == 0) {
    aMaxFileSizeBytes = -1;
  } else if (aMaxFileSizeBytes > INT64_MAX) {
    // GonkRecorder parameters are internally limited to signed 64-bit values
    DOM_CAMERA_LOGW("maxFileSizeBytes capped to INT64_MAX\n");
    aMaxFileSizeBytes = INT64_MAX;
  }
  snprintf(buffer, SIZE, "max-filesize=%lld", aMaxFileSizeBytes);
  CHECK_SETARG_RETURN(mRecorder->setParameters(String8(buffer)),
                      NS_ERROR_INVALID_ARG);

  // adjust rotation by camera sensor offset
  mVideoRotation = aRotation;
  mVideoRotation += mCameraHw->GetSensorOrientation();
  mVideoRotation = RationalizeRotation(mVideoRotation);
  DOM_CAMERA_LOGI("setting video rotation to %d degrees (mapped from %d)\n",
                  mVideoRotation, aRotation);
  snprintf(buffer, SIZE, "video-param-rotation-angle-degrees=%d",
           mVideoRotation);
  CHECK_SETARG_RETURN(mRecorder->setParameters(String8(buffer)),
                      NS_ERROR_INVALID_ARG);

  CHECK_SETARG_RETURN(mRecorder->setListener(new GonkRecorderListener(this)),
                      NS_ERROR_FAILURE);

  // recording API needs file descriptor of output file
  CHECK_SETARG_RETURN(mRecorder->setOutputFile(aFd, 0, 0), NS_ERROR_FAILURE);
  CHECK_SETARG_RETURN(mRecorder->prepare(), NS_ERROR_FAILURE);
#endif

  return NS_OK;
}

nsresult
nsGonkCameraControl::StopInternal()
{
  DOM_CAMERA_LOGT("%s:%d : this=%p\n", __func__, __LINE__, this);

  // if we're recording, stop recording
  StopRecordingImpl();

  // stop the preview
  nsresult rv = StopPreviewImpl();

  // release the hardware handle
  if (mCameraHw.get()){
     mCameraHw->Close();
     mCameraHw.clear();
  }

  return rv;
}

nsresult
nsGonkCameraControl::StopImpl()
{
  DOM_CAMERA_LOGT("%s:%d : this=%p\n", __func__, __LINE__, this);

  nsresult rv = StopInternal();
  if (rv != NS_ERROR_NOT_INITIALIZED) {
    rv = NS_OK;
  }
  if (NS_SUCCEEDED(rv)) {
    OnHardwareStateChange(CameraControlListener::kHardwareClosed, NS_OK);
  }
  return rv;
}

nsresult
nsGonkCameraControl::LoadRecorderProfiles()
{
  if (mRecorderProfiles.Count() == 0) {
    nsTArray<RefPtr<RecorderProfile>> profiles;
    nsresult rv = GonkRecorderProfile::GetAll(mCameraId, profiles);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    nsTArray<Size> sizes;
    rv = Get(CAMERA_PARAM_SUPPORTED_VIDEOSIZES, sizes);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return NS_ERROR_NOT_AVAILABLE;
    }

    nsTArray<RecorderProfile>::size_type bestIndexMatch = 0;
    int bestAreaMatch = 0;
    uint32_t bestPriorityMatch = UINT32_MAX;

    // Limit profiles to those video sizes supported by the camera hardware...
    for (nsTArray<RecorderProfile>::size_type i = 0; i < profiles.Length(); ++i) {
      int width = profiles[i]->GetVideo().GetSize().width;
      int height = profiles[i]->GetVideo().GetSize().height;
      if (width < 0 || height < 0) {
        DOM_CAMERA_LOGW("Ignoring weird profile '%s' with width and/or height < 0\n",
          NS_ConvertUTF16toUTF8(profiles[i]->GetName()).get());
        continue;
      }
      for (nsTArray<Size>::size_type n = 0; n < sizes.Length(); ++n) {
        if (static_cast<uint32_t>(width) == sizes[n].width &&
            static_cast<uint32_t>(height) == sizes[n].height) {
          mRecorderProfiles.Put(profiles[i]->GetName(), profiles[i]);

          // "Best" or default profile is the one with the lowest priority
          // value and largest area.
          int area = width * height;
          uint32_t priority = profiles[i]->GetPriority();
          if (bestPriorityMatch > priority ||
              (bestPriorityMatch == priority && area > bestAreaMatch)) {
            bestIndexMatch = i;
            bestAreaMatch = area;
            bestPriorityMatch = priority;
          }
          break;
        }
      }
    }

    if (bestAreaMatch > 0) {
      nsAutoString name;
      name.AssignASCII("default");
      mRecorderProfiles.Put(name, profiles[bestIndexMatch]);
    }
  }

  return NS_OK;
}

nsresult
nsGonkCameraControl::GetRecorderProfiles(nsTArray<nsString>& aProfiles)
{
  nsresult rv = LoadRecorderProfiles();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  aProfiles.Clear();
  for (auto iter = mRecorderProfiles.Iter(); !iter.Done(); iter.Next()) {
    aProfiles.AppendElement(iter.Key());
  }
  return NS_OK;
}

ICameraControl::RecorderProfile*
nsGonkCameraControl::GetProfileInfo(const nsAString& aProfile)
{
  RecorderProfile* profile;
  if (!mRecorderProfiles.Get(aProfile, &profile)) {
    return nullptr;
  }
  return profile;
}

void
nsGonkCameraControl::OnRateLimitPreview(bool aLimit)
{
  CameraControlImpl::OnRateLimitPreview(aLimit);
}

void
nsGonkCameraControl::CreatePoster(Image* aImage, uint32_t aWidth, uint32_t aHeight, int32_t aRotation)
{
  class PosterRunnable : public nsRunnable {
  public:
    PosterRunnable(nsGonkCameraControl* aTarget, Image* aImage,
                   uint32_t aWidth, uint32_t aHeight, int32_t aRotation)
      : mTarget(aTarget)
      , mImage(aImage)
      , mWidth(aWidth)
      , mHeight(aHeight)
      , mRotation(aRotation)
      , mDst(nullptr)
      , mDstLength(0)
    { }

    virtual ~PosterRunnable()
    {
      mTarget->OnPoster(mDst, mDstLength);
    }

    NS_IMETHODIMP Run() override
    {
#ifdef MOZ_WIDGET_GONK
      // NV21 (yuv420sp) is 12 bits / pixel
      size_t srcLength = (mWidth * mHeight * 3 + 1) / 2;

      // ARGB is 32 bits / pixel
      size_t tmpLength = mWidth * mHeight * sizeof(uint32_t);
      nsAutoArrayPtr<uint8_t> tmp;
      tmp = new uint8_t[tmpLength];

      GrallocImage* nativeImage = static_cast<GrallocImage*>(mImage.get());
      android::sp<GraphicBuffer> graphicBuffer = nativeImage->GetGraphicBuffer();

      void* graphicSrc = nullptr;
      graphicBuffer->lock(GraphicBuffer::USAGE_SW_READ_MASK, &graphicSrc);

      uint32_t stride = mWidth * 4;
      int err = libyuv::ConvertToARGB(static_cast<uint8_t*>(graphicSrc),
                                      srcLength, tmp, stride, 0, 0,
                                      mWidth, mHeight, mWidth, mHeight,
                                      libyuv::kRotate0, libyuv::FOURCC_NV21);

      graphicBuffer->unlock();
      graphicSrc = nullptr;
      graphicBuffer.clear();
      nativeImage = nullptr;
      mImage = nullptr;

      if (NS_WARN_IF(err < 0)) {
        DOM_CAMERA_LOGE("CreatePoster: to ARGB failed (%d)\n", err);
        return NS_OK;
      }

      nsCOMPtr<imgIEncoder> encoder =
        do_CreateInstance("@mozilla.org/image/encoder;2?type=image/jpeg");
      if (NS_WARN_IF(!encoder)) {
        DOM_CAMERA_LOGE("CreatePoster: no JPEG encoder\n");
        return NS_OK;
      }

      nsString opt;
      nsresult rv = encoder->InitFromData(tmp, tmpLength, mWidth,
                                          mHeight, stride,
                                          imgIEncoder::INPUT_FORMAT_HOSTARGB,
                                          opt);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        DOM_CAMERA_LOGE("CreatePoster: encoder init failed (0x%x)\n",
                        rv);
        return NS_OK;
      }

      nsCOMPtr<nsIInputStream> stream = do_QueryInterface(encoder);
      if (NS_WARN_IF(!stream)) {
        DOM_CAMERA_LOGE("CreatePoster: to input stream failed\n");
        return NS_OK;
      }

      uint64_t length = 0;
      rv = stream->Available(&length);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        DOM_CAMERA_LOGE("CreatePoster: get length failed (0x%x)\n",
                        rv);
        return NS_OK;
      }

      rv = NS_ReadInputStreamToBuffer(stream, &mDst, length);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        DOM_CAMERA_LOGE("CreatePoster: read failed (0x%x)\n", rv);
        mDst = nullptr;
        return NS_OK;
      }

      mDstLength = length;
#endif
      return NS_OK;
    }

  private:
    RefPtr<nsGonkCameraControl> mTarget;
    RefPtr<Image> mImage;
    int32_t mWidth;
    int32_t mHeight;
    int32_t mRotation;
    void* mDst;
    size_t mDstLength;
  };

  nsCOMPtr<nsIRunnable> event = new PosterRunnable(this, aImage,
                                                   aWidth,
                                                   aHeight,
                                                   aRotation);

  nsCOMPtr<nsIEventTarget> target
    = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
  MOZ_ASSERT(target);

  target->Dispatch(event, NS_DISPATCH_NORMAL);
}

void
nsGonkCameraControl::OnPoster(void* aData, uint32_t aLength)
{
  RefPtr<BlobImpl> blobImpl;
  if (aData) {
    blobImpl = new BlobImplMemory(aData, aLength, NS_LITERAL_STRING("image/jpeg"));
  }
  CameraControlImpl::OnPoster(blobImpl);
}

void
nsGonkCameraControl::OnNewPreviewFrame(layers::TextureClient* aBuffer)
{
#ifdef MOZ_WIDGET_GONK
  RefPtr<GrallocImage> frame = new GrallocImage();

  IntSize picSize(mCurrentConfiguration.mPreviewSize.width,
                  mCurrentConfiguration.mPreviewSize.height);
  frame->SetData(aBuffer, picSize);

  if (mCapturePoster.exchange(false)) {
    CreatePoster(frame,
                 mCurrentConfiguration.mPreviewSize.width,
                 mCurrentConfiguration.mPreviewSize.height,
                 mVideoRotation);
    return;
  }

  OnNewPreviewFrame(frame, mCurrentConfiguration.mPreviewSize.width,
                    mCurrentConfiguration.mPreviewSize.height);
#endif
}

void
nsGonkCameraControl::OnSystemError(CameraControlListener::SystemContext aWhere,
                                   nsresult aError)
{
  if (aWhere == CameraControlListener::kSystemService) {
    StopInternal();
    OnHardwareStateChange(CameraControlListener::kHardwareClosed, NS_ERROR_FAILURE);
  }

  CameraControlImpl::OnSystemError(aWhere, aError);
}

// Gonk callback handlers.
namespace mozilla {

void
OnTakePictureComplete(nsGonkCameraControl* gc, uint8_t* aData, uint32_t aLength)
{
  gc->OnTakePictureComplete(aData, aLength);
}

void
OnTakePictureError(nsGonkCameraControl* gc)
{
  gc->OnTakePictureError();
}

void
OnAutoFocusComplete(nsGonkCameraControl* gc, bool aSuccess)
{
  gc->OnAutoFocusComplete(aSuccess, false);
}

void
OnAutoFocusMoving(nsGonkCameraControl* gc, bool aIsMoving)
{
  gc->OnAutoFocusMoving(aIsMoving);
}

void
OnFacesDetected(nsGonkCameraControl* gc, camera_frame_metadata_t* aMetaData)
{
  gc->OnFacesDetected(aMetaData);
}

void
OnRateLimitPreview(nsGonkCameraControl* gc, bool aLimit)
{
  gc->OnRateLimitPreview(aLimit);
}

void
OnNewPreviewFrame(nsGonkCameraControl* gc, layers::TextureClient* aBuffer)
{
  gc->OnNewPreviewFrame(aBuffer);
}

void
OnShutter(nsGonkCameraControl* gc)
{
  gc->OnShutter();
}

void
OnSystemError(nsGonkCameraControl* gc,
              CameraControlListener::SystemContext aWhere,
              int32_t aArg1, int32_t aArg2)
{
  DOM_CAMERA_LOGE("OnSystemError : aWhere=%d, aArg1=%d, aArg2=%d\n", aWhere, aArg1, aArg2);
  gc->OnSystemError(aWhere, NS_ERROR_FAILURE);
}

} // namespace mozilla
