| /* |
| * Copyright (C) 2016 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.dialer.callcomposer.camera; |
| |
| import android.content.Context; |
| import android.hardware.Camera; |
| import android.hardware.Camera.CameraInfo; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Looper; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.support.annotation.VisibleForTesting; |
| import android.text.TextUtils; |
| import android.view.MotionEvent; |
| import android.view.OrientationEventListener; |
| import android.view.Surface; |
| import android.view.View; |
| import android.view.WindowManager; |
| import com.android.dialer.callcomposer.camera.camerafocus.FocusOverlayManager; |
| import com.android.dialer.callcomposer.camera.camerafocus.RenderOverlay; |
| import com.android.dialer.common.Assert; |
| import com.android.dialer.common.LogUtil; |
| import com.android.dialer.common.concurrent.DialerExecutorComponent; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| |
| /** |
| * Class which manages interactions with the camera, but does not do any UI. This class is designed |
| * to be a singleton to ensure there is one component managing the camera and releasing the native |
| * resources. In order to acquire a camera, a caller must: |
| * |
| * <ul> |
| * <li>Call selectCamera to select front or back camera |
| * <li>Call setSurface to control where the preview is shown |
| * <li>Call openCamera to request the camera start preview |
| * </ul> |
| * |
| * Callers should call onPause and onResume to ensure that the camera is release while the activity |
| * is not active. This class is not thread safe. It should only be called from one thread (the UI |
| * thread or test thread) |
| */ |
| public class CameraManager implements FocusOverlayManager.Listener { |
| /** Callbacks for the camera manager listener */ |
| public interface CameraManagerListener { |
| void onCameraError(int errorCode, Exception e); |
| |
| void onCameraChanged(); |
| } |
| |
| /** Callback when taking image or video */ |
| public interface MediaCallback { |
| int MEDIA_CAMERA_CHANGED = 1; |
| int MEDIA_NO_DATA = 2; |
| |
| void onMediaReady(Uri uriToMedia, String contentType, int width, int height); |
| |
| void onMediaFailed(Exception exception); |
| |
| void onMediaInfo(int what); |
| } |
| |
| // Error codes |
| private static final int ERROR_OPENING_CAMERA = 1; |
| private static final int ERROR_SHOWING_PREVIEW = 2; |
| private static final int ERROR_HARDWARE_ACCELERATION_DISABLED = 3; |
| private static final int ERROR_TAKING_PICTURE = 4; |
| |
| private static final int NO_CAMERA_SELECTED = -1; |
| |
| private static final Camera.ShutterCallback DUMMY_SHUTTER_CALLBACK = |
| new Camera.ShutterCallback() { |
| @Override |
| public void onShutter() { |
| // Do nothing |
| } |
| }; |
| |
| private static CameraManager instance; |
| |
| /** The CameraInfo for the currently selected camera */ |
| private final CameraInfo cameraInfo; |
| |
| /** The index of the selected camera or NO_CAMERA_SELECTED if a camera hasn't been selected yet */ |
| private int cameraIndex; |
| |
| /** True if the device has front and back cameras */ |
| private final boolean hasFrontAndBackCamera; |
| |
| /** True if the camera should be open (may not yet be actually open) */ |
| private boolean openRequested; |
| |
| /** The preview view to show the preview on */ |
| private CameraPreview cameraPreview; |
| |
| /** The helper classs to handle orientation changes */ |
| private OrientationHandler orientationHandler; |
| |
| /** Tracks whether the preview has hardware acceleration */ |
| private boolean isHardwareAccelerationSupported; |
| |
| /** |
| * The task for opening the camera, so it doesn't block the UI thread Using AsyncTask rather than |
| * SafeAsyncTask because the tasks need to be serialized, but don't need to be on the UI thread |
| * TODO(blemmon): If we have other AyncTasks (not SafeAsyncTasks) this may contend and we may need |
| * to create a dedicated thread, or synchronize the threads in the thread pool |
| */ |
| private AsyncTask<Integer, Void, Camera> openCameraTask; |
| |
| /** |
| * The camera index that is queued to be opened, but not completed yet, or NO_CAMERA_SELECTED if |
| * no open task is pending |
| */ |
| private int pendingOpenCameraIndex = NO_CAMERA_SELECTED; |
| |
| /** The instance of the currently opened camera */ |
| private Camera camera; |
| |
| /** The rotation of the screen relative to the camera's natural orientation */ |
| private int rotation; |
| |
| /** The callback to notify when errors or other events occur */ |
| private CameraManagerListener listener; |
| |
| /** True if the camera is currently in the process of taking an image */ |
| private boolean takingPicture; |
| |
| /** Manages auto focus visual and behavior */ |
| private final FocusOverlayManager focusOverlayManager; |
| |
| private CameraManager() { |
| this.cameraInfo = new CameraInfo(); |
| cameraIndex = NO_CAMERA_SELECTED; |
| |
| // Check to see if a front and back camera exist |
| boolean hasFrontCamera = false; |
| boolean hasBackCamera = false; |
| final CameraInfo cameraInfo = new CameraInfo(); |
| final int cameraCount = Camera.getNumberOfCameras(); |
| try { |
| for (int i = 0; i < cameraCount; i++) { |
| Camera.getCameraInfo(i, cameraInfo); |
| if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) { |
| hasFrontCamera = true; |
| } else if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) { |
| hasBackCamera = true; |
| } |
| if (hasFrontCamera && hasBackCamera) { |
| break; |
| } |
| } |
| } catch (final RuntimeException e) { |
| LogUtil.e("CameraManager.CameraManager", "Unable to load camera info", e); |
| } |
| hasFrontAndBackCamera = hasFrontCamera && hasBackCamera; |
| focusOverlayManager = new FocusOverlayManager(this, Looper.getMainLooper()); |
| |
| // Assume the best until we are proven otherwise |
| isHardwareAccelerationSupported = true; |
| } |
| |
| /** Gets the singleton instance */ |
| public static CameraManager get() { |
| if (instance == null) { |
| instance = new CameraManager(); |
| } |
| return instance; |
| } |
| |
| /** |
| * Sets the surface to use to display the preview This must only be called AFTER the CameraPreview |
| * has a texture ready |
| * |
| * @param preview The preview surface view |
| */ |
| void setSurface(final CameraPreview preview) { |
| if (preview == cameraPreview) { |
| return; |
| } |
| |
| if (preview != null) { |
| Assert.checkArgument(preview.isValid()); |
| preview.setOnTouchListener( |
| new View.OnTouchListener() { |
| @Override |
| public boolean onTouch(final View view, final MotionEvent motionEvent) { |
| if ((motionEvent.getActionMasked() & MotionEvent.ACTION_UP) |
| == MotionEvent.ACTION_UP) { |
| focusOverlayManager.setPreviewSize(view.getWidth(), view.getHeight()); |
| focusOverlayManager.onSingleTapUp( |
| (int) motionEvent.getX() + view.getLeft(), |
| (int) motionEvent.getY() + view.getTop()); |
| } |
| view.performClick(); |
| return true; |
| } |
| }); |
| } |
| cameraPreview = preview; |
| tryShowPreview(); |
| } |
| |
| public void setRenderOverlay(final RenderOverlay renderOverlay) { |
| focusOverlayManager.setFocusRenderer( |
| renderOverlay != null ? renderOverlay.getPieRenderer() : null); |
| } |
| |
| /** Convenience function to swap between front and back facing cameras */ |
| public void swapCamera() { |
| Assert.checkState(cameraIndex >= 0); |
| selectCamera( |
| cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT |
| ? CameraInfo.CAMERA_FACING_BACK |
| : CameraInfo.CAMERA_FACING_FRONT); |
| } |
| |
| /** |
| * Selects the first camera facing the desired direction, or the first camera if there is no |
| * camera in the desired direction |
| * |
| * @param desiredFacing One of the CameraInfo.CAMERA_FACING_* constants |
| * @return True if a camera was selected, or false if selecting a camera failed |
| */ |
| public boolean selectCamera(final int desiredFacing) { |
| try { |
| // We already selected a camera facing that direction |
| if (cameraIndex >= 0 && this.cameraInfo.facing == desiredFacing) { |
| return true; |
| } |
| |
| final int cameraCount = Camera.getNumberOfCameras(); |
| Assert.checkState(cameraCount > 0); |
| |
| cameraIndex = NO_CAMERA_SELECTED; |
| setCamera(null); |
| final CameraInfo cameraInfo = new CameraInfo(); |
| for (int i = 0; i < cameraCount; i++) { |
| Camera.getCameraInfo(i, cameraInfo); |
| if (cameraInfo.facing == desiredFacing) { |
| cameraIndex = i; |
| Camera.getCameraInfo(i, this.cameraInfo); |
| break; |
| } |
| } |
| |
| // There's no camera in the desired facing direction, just select the first camera |
| // regardless of direction |
| if (cameraIndex < 0) { |
| cameraIndex = 0; |
| Camera.getCameraInfo(0, this.cameraInfo); |
| } |
| |
| if (openRequested) { |
| // The camera is open, so reopen with the newly selected camera |
| openCamera(); |
| } |
| return true; |
| } catch (final RuntimeException e) { |
| LogUtil.e("CameraManager.selectCamera", "RuntimeException in CameraManager.selectCamera", e); |
| if (listener != null) { |
| listener.onCameraError(ERROR_OPENING_CAMERA, e); |
| } |
| return false; |
| } |
| } |
| |
| public int getCameraIndex() { |
| return cameraIndex; |
| } |
| |
| public void selectCameraByIndex(final int cameraIndex) { |
| if (this.cameraIndex == cameraIndex) { |
| return; |
| } |
| |
| try { |
| this.cameraIndex = cameraIndex; |
| Camera.getCameraInfo(this.cameraIndex, cameraInfo); |
| if (openRequested) { |
| openCamera(); |
| } |
| } catch (final RuntimeException e) { |
| LogUtil.e( |
| "CameraManager.selectCameraByIndex", |
| "RuntimeException in CameraManager.selectCameraByIndex", |
| e); |
| if (listener != null) { |
| listener.onCameraError(ERROR_OPENING_CAMERA, e); |
| } |
| } |
| } |
| |
| @Nullable |
| @VisibleForTesting |
| public CameraInfo getCameraInfo() { |
| if (cameraIndex == NO_CAMERA_SELECTED) { |
| return null; |
| } |
| return cameraInfo; |
| } |
| |
| /** @return True if the device has both a front and back camera */ |
| public boolean hasFrontAndBackCamera() { |
| return hasFrontAndBackCamera; |
| } |
| |
| /** Opens the camera on a separate thread and initiates the preview if one is available */ |
| void openCamera() { |
| if (this.cameraIndex == NO_CAMERA_SELECTED) { |
| // Ensure a selected camera if none is currently selected. This may happen if the |
| // camera chooser is not the default media chooser. |
| selectCamera(CameraInfo.CAMERA_FACING_BACK); |
| } |
| openRequested = true; |
| // We're already opening the camera or already have the camera handle, nothing more to do |
| if (pendingOpenCameraIndex == this.cameraIndex || this.camera != null) { |
| return; |
| } |
| |
| // True if the task to open the camera has to be delayed until the current one completes |
| boolean delayTask = false; |
| |
| // Cancel any previous open camera tasks |
| if (openCameraTask != null) { |
| pendingOpenCameraIndex = NO_CAMERA_SELECTED; |
| delayTask = true; |
| } |
| |
| pendingOpenCameraIndex = this.cameraIndex; |
| openCameraTask = |
| new AsyncTask<Integer, Void, Camera>() { |
| private Exception exception; |
| |
| @Override |
| protected Camera doInBackground(final Integer... params) { |
| try { |
| final int cameraIndex = params[0]; |
| LogUtil.v( |
| "CameraManager.doInBackground", |
| "Opening camera " + CameraManager.this.cameraIndex); |
| return Camera.open(cameraIndex); |
| } catch (final Exception e) { |
| LogUtil.e("CameraManager.doInBackground", "Exception while opening camera", e); |
| exception = e; |
| return null; |
| } |
| } |
| |
| @Override |
| protected void onPostExecute(final Camera camera) { |
| // If we completed, but no longer want this camera, then release the camera |
| if (openCameraTask != this || !openRequested) { |
| releaseCamera(camera); |
| cleanup(); |
| return; |
| } |
| |
| cleanup(); |
| |
| LogUtil.v( |
| "CameraManager.onPostExecute", |
| "Opened camera " + CameraManager.this.cameraIndex + " " + (camera != null)); |
| setCamera(camera); |
| if (camera == null) { |
| if (listener != null) { |
| listener.onCameraError(ERROR_OPENING_CAMERA, exception); |
| } |
| LogUtil.e("CameraManager.onPostExecute", "Error opening camera"); |
| } |
| } |
| |
| @Override |
| protected void onCancelled() { |
| super.onCancelled(); |
| cleanup(); |
| } |
| |
| private void cleanup() { |
| pendingOpenCameraIndex = NO_CAMERA_SELECTED; |
| if (openCameraTask != null && openCameraTask.getStatus() == Status.PENDING) { |
| // If there's another task waiting on this one to complete, start it now |
| openCameraTask.execute(CameraManager.this.cameraIndex); |
| } else { |
| openCameraTask = null; |
| } |
| } |
| }; |
| LogUtil.v("CameraManager.openCamera", "Start opening camera " + this.cameraIndex); |
| if (!delayTask) { |
| openCameraTask.execute(this.cameraIndex); |
| } |
| } |
| |
| /** Closes the camera releasing the resources it uses */ |
| void closeCamera() { |
| openRequested = false; |
| setCamera(null); |
| } |
| |
| /** |
| * Sets the listener which will be notified of errors or other events in the camera |
| * |
| * @param listener The listener to notify |
| */ |
| public void setListener(final CameraManagerListener listener) { |
| Assert.isMainThread(); |
| this.listener = listener; |
| if (!isHardwareAccelerationSupported && this.listener != null) { |
| this.listener.onCameraError(ERROR_HARDWARE_ACCELERATION_DISABLED, null); |
| } |
| } |
| |
| public void takePicture(final float heightPercent, @NonNull final MediaCallback callback) { |
| Assert.checkState(!takingPicture); |
| Assert.isNotNull(callback); |
| cameraPreview.setFocusable(false); |
| focusOverlayManager.cancelAutoFocus(); |
| if (this.camera == null) { |
| // The caller should have checked isCameraAvailable first, but just in case, protect |
| // against a null camera by notifying the callback that taking the picture didn't work |
| callback.onMediaFailed(null); |
| return; |
| } |
| final Camera.PictureCallback jpegCallback = |
| new Camera.PictureCallback() { |
| @Override |
| public void onPictureTaken(final byte[] bytes, final Camera camera) { |
| takingPicture = false; |
| if (CameraManager.this.camera != camera) { |
| // This may happen if the camera was changed between front/back while the |
| // picture is being taken. |
| callback.onMediaInfo(MediaCallback.MEDIA_CAMERA_CHANGED); |
| return; |
| } |
| |
| if (bytes == null) { |
| callback.onMediaInfo(MediaCallback.MEDIA_NO_DATA); |
| return; |
| } |
| |
| final Camera.Size size = camera.getParameters().getPictureSize(); |
| int width; |
| int height; |
| if (rotation == 90 || rotation == 270) { |
| // Is rotated, so swapping dimensions is desired |
| // noinspection SuspiciousNameCombination |
| width = size.height; |
| // noinspection SuspiciousNameCombination |
| height = size.width; |
| } else { |
| width = size.width; |
| height = size.height; |
| } |
| LogUtil.i( |
| "CameraManager.onPictureTaken", "taken picture size: " + bytes.length + " bytes"); |
| DialerExecutorComponent.get(cameraPreview.getContext()) |
| .dialerExecutorFactory() |
| .createNonUiTaskBuilder( |
| new ImagePersistWorker( |
| width, height, heightPercent, bytes, cameraPreview.getContext())) |
| .onSuccess( |
| (result) -> { |
| callback.onMediaReady( |
| result.getUri(), "image/jpeg", result.getWidth(), result.getHeight()); |
| }) |
| .onFailure( |
| (throwable) -> { |
| callback.onMediaFailed(new Exception("Persisting image failed", throwable)); |
| }) |
| .build() |
| .executeSerial(null); |
| } |
| }; |
| |
| takingPicture = true; |
| try { |
| this.camera.takePicture( |
| // A shutter callback is required to enable shutter sound |
| DUMMY_SHUTTER_CALLBACK, null /* raw */, null /* postView */, jpegCallback); |
| } catch (final RuntimeException e) { |
| LogUtil.e("CameraManager.takePicture", "RuntimeException in CameraManager.takePicture", e); |
| takingPicture = false; |
| if (listener != null) { |
| listener.onCameraError(ERROR_TAKING_PICTURE, e); |
| } |
| } |
| } |
| |
| /** |
| * Asynchronously releases a camera |
| * |
| * @param camera The camera to release |
| */ |
| private void releaseCamera(final Camera camera) { |
| if (camera == null) { |
| return; |
| } |
| |
| focusOverlayManager.onCameraReleased(); |
| |
| new AsyncTask<Void, Void, Void>() { |
| @Override |
| protected Void doInBackground(final Void... params) { |
| LogUtil.v("CameraManager.doInBackground", "Releasing camera " + cameraIndex); |
| camera.release(); |
| return null; |
| } |
| }.execute(); |
| } |
| |
| /** |
| * Updates the orientation of the {@link Camera} w.r.t. the orientation of the device and the |
| * orientation that the physical camera is mounted on the device. |
| * |
| * @param camera that needs to be reorientated |
| * @param screenRotation rotation of the physical device |
| * @param cameraOrientation {@link CameraInfo#orientation} |
| * @param cameraIsFrontFacing {@link CameraInfo#CAMERA_FACING_FRONT} |
| * @return rotation that images returned from {@link |
| * android.hardware.Camera.PictureCallback#onPictureTaken(byte[], Camera)} will be rotated. |
| */ |
| @VisibleForTesting |
| static int updateCameraRotation( |
| @NonNull Camera camera, |
| int screenRotation, |
| int cameraOrientation, |
| boolean cameraIsFrontFacing) { |
| Assert.isNotNull(camera); |
| Assert.checkArgument(cameraOrientation % 90 == 0); |
| |
| int rotation = screenRotationToDegress(screenRotation); |
| boolean portrait = rotation == 0 || rotation == 180; |
| |
| if (!portrait && !cameraIsFrontFacing) { |
| rotation += 180; |
| } |
| rotation += cameraOrientation; |
| rotation %= 360; |
| |
| // Rotate the camera |
| if (portrait && cameraIsFrontFacing) { |
| camera.setDisplayOrientation((rotation + 180) % 360); |
| } else { |
| camera.setDisplayOrientation(rotation); |
| } |
| |
| // Rotate the images returned when a picture is taken |
| Camera.Parameters params = camera.getParameters(); |
| params.setRotation(rotation); |
| camera.setParameters(params); |
| return rotation; |
| } |
| |
| private static int screenRotationToDegress(int screenRotation) { |
| switch (screenRotation) { |
| case Surface.ROTATION_0: |
| return 0; |
| case Surface.ROTATION_90: |
| return 90; |
| case Surface.ROTATION_180: |
| return 180; |
| case Surface.ROTATION_270: |
| return 270; |
| default: |
| throw Assert.createIllegalStateFailException("Invalid surface rotation."); |
| } |
| } |
| |
| /** Sets the current camera, releasing any previously opened camera */ |
| private void setCamera(final Camera camera) { |
| if (this.camera == camera) { |
| return; |
| } |
| |
| releaseCamera(this.camera); |
| this.camera = camera; |
| tryShowPreview(); |
| if (listener != null) { |
| listener.onCameraChanged(); |
| } |
| } |
| |
| /** Shows the preview if the camera is open and the preview is loaded */ |
| private void tryShowPreview() { |
| if (cameraPreview == null || this.camera == null) { |
| if (orientationHandler != null) { |
| orientationHandler.disable(); |
| orientationHandler = null; |
| } |
| focusOverlayManager.onPreviewStopped(); |
| return; |
| } |
| try { |
| this.camera.stopPreview(); |
| if (!takingPicture) { |
| rotation = |
| updateCameraRotation( |
| this.camera, |
| getScreenRotation(), |
| cameraInfo.orientation, |
| cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT); |
| } |
| |
| final Camera.Parameters params = this.camera.getParameters(); |
| final Camera.Size pictureSize = chooseBestPictureSize(); |
| final Camera.Size previewSize = chooseBestPreviewSize(pictureSize); |
| params.setPreviewSize(previewSize.width, previewSize.height); |
| params.setPictureSize(pictureSize.width, pictureSize.height); |
| logCameraSize("Setting preview size: ", previewSize); |
| logCameraSize("Setting picture size: ", pictureSize); |
| cameraPreview.setSize(previewSize, cameraInfo.orientation); |
| for (final String focusMode : params.getSupportedFocusModes()) { |
| if (TextUtils.equals(focusMode, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { |
| // Use continuous focus if available |
| params.setFocusMode(focusMode); |
| break; |
| } |
| } |
| |
| this.camera.setParameters(params); |
| cameraPreview.startPreview(this.camera); |
| this.camera.startPreview(); |
| this.camera.setAutoFocusMoveCallback( |
| new Camera.AutoFocusMoveCallback() { |
| @Override |
| public void onAutoFocusMoving(final boolean start, final Camera camera) { |
| focusOverlayManager.onAutoFocusMoving(start); |
| } |
| }); |
| focusOverlayManager.setParameters(this.camera.getParameters()); |
| focusOverlayManager.setMirror(cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK); |
| focusOverlayManager.onPreviewStarted(); |
| if (orientationHandler == null) { |
| orientationHandler = new OrientationHandler(cameraPreview.getContext()); |
| orientationHandler.enable(); |
| } |
| } catch (final IOException e) { |
| LogUtil.e("CameraManager.tryShowPreview", "IOException in CameraManager.tryShowPreview", e); |
| if (listener != null) { |
| listener.onCameraError(ERROR_SHOWING_PREVIEW, e); |
| } |
| } catch (final RuntimeException e) { |
| LogUtil.e( |
| "CameraManager.tryShowPreview", "RuntimeException in CameraManager.tryShowPreview", e); |
| if (listener != null) { |
| listener.onCameraError(ERROR_SHOWING_PREVIEW, e); |
| } |
| } |
| } |
| |
| private int getScreenRotation() { |
| return cameraPreview |
| .getContext() |
| .getSystemService(WindowManager.class) |
| .getDefaultDisplay() |
| .getRotation(); |
| } |
| |
| public boolean isCameraAvailable() { |
| return camera != null && !takingPicture && isHardwareAccelerationSupported; |
| } |
| |
| /** |
| * Choose the best picture size by trying to find a size close to the MmsConfig's max size, which |
| * is closest to the screen aspect ratio. In case of RCS conversation returns default size. |
| */ |
| private Camera.Size chooseBestPictureSize() { |
| return camera.getParameters().getPictureSize(); |
| } |
| |
| /** |
| * Chose the best preview size based on the picture size. Try to find a size with the same aspect |
| * ratio and size as the picture if possible |
| */ |
| private Camera.Size chooseBestPreviewSize(final Camera.Size pictureSize) { |
| final List<Camera.Size> sizes = |
| new ArrayList<Camera.Size>(camera.getParameters().getSupportedPreviewSizes()); |
| final float aspectRatio = pictureSize.width / (float) pictureSize.height; |
| final int capturePixels = pictureSize.width * pictureSize.height; |
| |
| // Sort the sizes so the best size is first |
| Collections.sort( |
| sizes, |
| new SizeComparator(Integer.MAX_VALUE, Integer.MAX_VALUE, aspectRatio, capturePixels)); |
| |
| return sizes.get(0); |
| } |
| |
| private class OrientationHandler extends OrientationEventListener { |
| OrientationHandler(final Context context) { |
| super(context); |
| } |
| |
| @Override |
| public void onOrientationChanged(final int orientation) { |
| if (!takingPicture) { |
| rotation = |
| updateCameraRotation( |
| camera, |
| getScreenRotation(), |
| cameraInfo.orientation, |
| cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT); |
| } |
| } |
| } |
| |
| private static class SizeComparator implements Comparator<Camera.Size> { |
| private static final int PREFER_LEFT = -1; |
| private static final int PREFER_RIGHT = 1; |
| |
| // The max width/height for the preferred size. Integer.MAX_VALUE if no size limit |
| private final int maxWidth; |
| private final int maxHeight; |
| |
| // The desired aspect ratio |
| private final float targetAspectRatio; |
| |
| // The desired size (width x height) to try to match |
| private final int targetPixels; |
| |
| public SizeComparator( |
| final int maxWidth, |
| final int maxHeight, |
| final float targetAspectRatio, |
| final int targetPixels) { |
| this.maxWidth = maxWidth; |
| this.maxHeight = maxHeight; |
| this.targetAspectRatio = targetAspectRatio; |
| this.targetPixels = targetPixels; |
| } |
| |
| /** |
| * Returns a negative value if left is a better choice than right, or a positive value if right |
| * is a better choice is better than left. 0 if they are equal |
| */ |
| @Override |
| public int compare(final Camera.Size left, final Camera.Size right) { |
| // If one size is less than the max size prefer it over the other |
| if ((left.width <= maxWidth && left.height <= maxHeight) |
| != (right.width <= maxWidth && right.height <= maxHeight)) { |
| return left.width <= maxWidth ? PREFER_LEFT : PREFER_RIGHT; |
| } |
| |
| // If one is closer to the target aspect ratio, prefer it. |
| final float leftAspectRatio = left.width / (float) left.height; |
| final float rightAspectRatio = right.width / (float) right.height; |
| final float leftAspectRatioDiff = Math.abs(leftAspectRatio - targetAspectRatio); |
| final float rightAspectRatioDiff = Math.abs(rightAspectRatio - targetAspectRatio); |
| if (leftAspectRatioDiff != rightAspectRatioDiff) { |
| return (leftAspectRatioDiff - rightAspectRatioDiff) < 0 ? PREFER_LEFT : PREFER_RIGHT; |
| } |
| |
| // At this point they have the same aspect ratio diff and are either both bigger |
| // than the max size or both smaller than the max size, so prefer the one closest |
| // to target size |
| final int leftDiff = Math.abs((left.width * left.height) - targetPixels); |
| final int rightDiff = Math.abs((right.width * right.height) - targetPixels); |
| return leftDiff - rightDiff; |
| } |
| } |
| |
| @Override // From FocusOverlayManager.Listener |
| public void autoFocus() { |
| if (this.camera == null) { |
| return; |
| } |
| |
| try { |
| this.camera.autoFocus( |
| new Camera.AutoFocusCallback() { |
| @Override |
| public void onAutoFocus(final boolean success, final Camera camera) { |
| focusOverlayManager.onAutoFocus(success, false /* shutterDown */); |
| } |
| }); |
| } catch (final RuntimeException e) { |
| LogUtil.e("CameraManager.autoFocus", "RuntimeException in CameraManager.autoFocus", e); |
| // If autofocus fails, the camera should have called the callback with success=false, |
| // but some throw an exception here |
| focusOverlayManager.onAutoFocus(false /*success*/, false /*shutterDown*/); |
| } |
| } |
| |
| @Override // From FocusOverlayManager.Listener |
| public void cancelAutoFocus() { |
| if (camera == null) { |
| return; |
| } |
| try { |
| camera.cancelAutoFocus(); |
| } catch (final RuntimeException e) { |
| // Ignore |
| LogUtil.e( |
| "CameraManager.cancelAutoFocus", "RuntimeException in CameraManager.cancelAutoFocus", e); |
| } |
| } |
| |
| @Override // From FocusOverlayManager.Listener |
| public boolean capture() { |
| return false; |
| } |
| |
| @Override // From FocusOverlayManager.Listener |
| public void setFocusParameters() { |
| if (camera == null) { |
| return; |
| } |
| try { |
| final Camera.Parameters parameters = camera.getParameters(); |
| parameters.setFocusMode(focusOverlayManager.getFocusMode()); |
| if (parameters.getMaxNumFocusAreas() > 0) { |
| // Don't set focus areas (even to null) if focus areas aren't supported, camera may |
| // crash |
| parameters.setFocusAreas(focusOverlayManager.getFocusAreas()); |
| } |
| parameters.setMeteringAreas(focusOverlayManager.getMeteringAreas()); |
| camera.setParameters(parameters); |
| } catch (final RuntimeException e) { |
| // This occurs when the device is out of space or when the camera is locked |
| LogUtil.e( |
| "CameraManager.setFocusParameters", |
| "RuntimeException in CameraManager setFocusParameters"); |
| } |
| } |
| |
| public void resetPreview() { |
| camera.startPreview(); |
| if (cameraPreview != null) { |
| cameraPreview.setFocusable(true); |
| } |
| } |
| |
| private void logCameraSize(final String prefix, final Camera.Size size) { |
| // Log the camera size and aspect ratio for help when examining bug reports for camera |
| // failures |
| LogUtil.i( |
| "CameraManager.logCameraSize", |
| prefix + size.width + "x" + size.height + " (" + (size.width / (float) size.height) + ")"); |
| } |
| |
| @VisibleForTesting |
| public void resetCameraManager() { |
| instance = null; |
| } |
| } |