content: /*
   * Copyright 2017 The Android Things Samples Authors.
   *
   * 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.example.androidthings.imageclassifier;
  
  import android.annotation.SuppressLint;
  import android.content.Context;
  import android.graphics.ImageFormat;
  import android.hardware.camera2.CameraAccessException;
  import android.hardware.camera2.CameraCaptureSession;
  import android.hardware.camera2.CameraCharacteristics;
  import android.hardware.camera2.CameraDevice;
  import android.hardware.camera2.CameraManager;
  import android.hardware.camera2.CaptureRequest;
  import android.hardware.camera2.CaptureResult;
  import android.hardware.camera2.TotalCaptureResult;
  import android.hardware.camera2.params.StreamConfigurationMap;
  import android.media.ImageReader;
  import android.os.Handler;
  import android.support.annotation.NonNull;
  import android.util.Log;
  import android.util.Pair;
  import android.util.Size;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Comparator;
  
  public class CameraHandler {
      private static final String TAG = CameraHandler.class.getSimpleName();
  
      private static final int MAX_IMAGES = 1;
      private CameraDevice mCameraDevice;
      private CameraCaptureSession mCaptureSession;
      private boolean initialized;
  
      private Size mImageDimensions;
  
      /**
       * An {@link ImageReader} that handles still image capture.
       */
      private ImageReader mImageReader;
  
      // Lazy-loaded singleton, so only one instance of the camera is created.
      private CameraHandler() {
      }
  
      private static class InstanceHolder {
          private static CameraHandler mCamera = new CameraHandler();
      }
  
      public static CameraHandler getInstance() {
          return InstanceHolder.mCamera;
      }
  
      /**
       * Initialize the camera device
       */
      @SuppressLint("MissingPermission")
      public void initializeCamera(Context context, Handler backgroundHandler, Size minSize,
                                   ImageReader.OnImageAvailableListener imageAvailableListener)
          throws CameraAccessException {
          if (initialized) {
              throw new IllegalStateException(
                      "CameraHandler is already initialized or is initializing");
          }
          initialized = true;
          // Discover the camera instance
          CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
          String camId = getCameraId(context);
  
          // Initialize the image processor with the largest available size.
          CameraCharacteristics characteristics = manager.getCameraCharacteristics(camId);
          StreamConfigurationMap map = characteristics.get(
              CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
  
          Size bestSize = getBestCameraSize(map.getOutputSizes(ImageFormat.JPEG), minSize);
          if (bestSize == null) {
              throw new RuntimeException("We could not find a camera resolution that is larger than "
                      + minSize.getWidth() + "x" + minSize.getHeight());
          }
  
          mImageReader = ImageReader.newInstance(bestSize.getWidth(), bestSize.getHeight(),
              ImageFormat.JPEG, MAX_IMAGES);
          mImageDimensions = bestSize;
          Log.d(TAG, "Will capture photos that are " + mImageDimensions.getWidth() + " x " +
              mImageDimensions.getHeight());
          mImageReader.setOnImageAvailableListener(imageAvailableListener, backgroundHandler);
  
          // Open the camera resource
          try {
              manager.openCamera(camId, mStateCallback, backgroundHandler);
          } catch (CameraAccessException cae) {
              Log.e(TAG, "Camera access exception", cae);
          }
      }
  
      public Size getImageDimensions() {
          return mImageDimensions;
      }
  
      /**
       * Begin a still image capture
       */
      public void takePicture() {
          if (mCameraDevice == null) {
              Log.w(TAG, "Cannot capture image. Camera not initialized.");
              return;
          }
          // Create a CameraCaptureSession for capturing still images.
          try {
              mCameraDevice.createCaptureSession(
                      Collections.singletonList(mImageReader.getSurface()),
                      mSessionCallback,
                      null);
          } catch (CameraAccessException cae) {
              Log.e(TAG, "Cannot create camera capture session", cae);
          }
      }
  
      /**
       * Execute a new capture request within the active session
       */
      private void triggerImageCapture() {
          try {
              final CaptureRequest.Builder captureBuilder =
                      mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
              captureBuilder.addTarget(mImageReader.getSurface());
              captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
              Log.d(TAG, "Capture request created.");
              mCaptureSession.capture(captureBuilder.build(), mCaptureCallback, null);
          } catch (CameraAccessException cae) {
              Log.e(TAG, "Cannot trigger a capture request");
          }
      }
  
      private void closeCaptureSession() {
          if (mCaptureSession != null) {
              try {
                  mCaptureSession.close();
              } catch (Exception ex) {
                  Log.w(TAG, "Could not close capture session", ex);
              }
              mCaptureSession = null;
          }
      }
  
      /**
       * Close the camera resources
       */
      public void shutDown() {
          try {
              closeCaptureSession();
              if (mCameraDevice != null) {
                  mCameraDevice.close();
              }
              mImageReader.close();
          } finally {
              initialized = false;
          }
      }
  
      public static String getCameraId(Context context) {
          CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
          String[] camIds = null;
          try {
              camIds = manager.getCameraIdList();
          } catch (CameraAccessException e) {
              Log.w(TAG, "Cannot get the list of available cameras", e);
          }
          if (camIds == null || camIds.length < 1) {
              Log.d(TAG, "No cameras found");
              return null;
          }
          return camIds[0];
      }
  
      /**
       * Helpful debugging method:  Dump all supported camera formats to log.  You don't need to run
       * this for normal operation, but it's very helpful when porting this code to different
       * hardware.
       */
      public static void dumpFormatInfo(Context context) {
          // Discover the camera instance
          CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
          String camId = getCameraId(context);
          if (camId == null) {
              return;
          }
          Log.d(TAG, "Using camera id " + camId);
          try {
              CameraCharacteristics characteristics = manager.getCameraCharacteristics(camId);
              StreamConfigurationMap configs = characteristics.get(
                      CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
              for (int format : configs.getOutputFormats()) {
                  Log.d(TAG, "Getting sizes for format: " + format);
                  for (Size s : configs.getOutputSizes(format)) {
                      Log.d(TAG, "\t" + s.toString());
                  }
              }
              int[] effects = characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_EFFECTS);
              for (int effect : effects) {
                  Log.d(TAG, "Effect available: " + effect);
              }
          } catch (CameraAccessException e) {
              Log.e(TAG, "Camera access exception getting characteristics.");
          }
      }
  
  
      /**
       * Callback handling device state changes
       */
      private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
          @Override
          public void onOpened(@NonNull CameraDevice cameraDevice) {
              Log.d(TAG, "Opened camera.");
              mCameraDevice = cameraDevice;
          }
          @Override
          public void onDisconnected(@NonNull CameraDevice cameraDevice) {
              Log.d(TAG, "Camera disconnected, closing.");
              closeCaptureSession();
              cameraDevice.close();
          }
          @Override
          public void onError(@NonNull CameraDevice cameraDevice, int i) {
              Log.d(TAG, "Camera device error, closing.");
              closeCaptureSession();
              cameraDevice.close();
          }
          @Override
          public void onClosed(@NonNull CameraDevice cameraDevice) {
              Log.d(TAG, "Closed camera, releasing");
              mCameraDevice = null;
          }
      };
  
      /**
       * Callback handling session state changes
       */
      private CameraCaptureSession.StateCallback mSessionCallback =
              new CameraCaptureSession.StateCallback() {
                  @Override
                  public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                      // The camera is already closed
                      if (mCameraDevice == null) {
                          return;
                      }
                      // When the session is ready, we start capture.
                      mCaptureSession = cameraCaptureSession;
                      triggerImageCapture();
                  }
                  @Override
                  public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
                      Log.w(TAG, "Failed to configure camera");
                  }
              };
  
      /**
       * Callback handling capture session events
       */
      private final CameraCaptureSession.CaptureCallback mCaptureCallback =
              new CameraCaptureSession.CaptureCallback() {
                  @Override
                  public void onCaptureProgressed(@NonNull CameraCaptureSession session,
                                                  @NonNull CaptureRequest request,
                                                  @NonNull CaptureResult partialResult) {
                      Log.d(TAG, "Partial result");
                  }
                  @Override
                  public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                                 @NonNull CaptureRequest request,
                                                 @NonNull TotalCaptureResult result) {
                      session.close();
                      mCaptureSession = null;
                      Log.d(TAG, "CaptureSession closed");
                  }
              };
  
      static Size getBestCameraSize(Size[] availableCameraResolutions, Size minSize) {
          // This should select the closest size that is not too small
          Arrays.sort(availableCameraResolutions, new CompareSizesByArea()); // Sort by smallest first
          for (Size resolution : availableCameraResolutions) {
              if (resolution.getWidth() >= minSize.getWidth() &&
                      resolution.getHeight() >= minSize.getHeight()) {
                  return resolution;
              }
          }
          return null;
      }
  
  
      /**
       * Compares two {@code Size}s based on their areas ascending.
       */
      static class CompareSizesByArea implements Comparator<Size> {
          @Override
          public int compare(Size lhs, Size rhs) {
              // We cast here to ensure the multiplications won't overflow
              return Long.signum((long) lhs.getWidth() * lhs.getHeight() -
                      (long) rhs.getWidth() * rhs.getHeight());
          }
      }
  }