/* * 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: * * * * 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 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() { 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() { @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 sizes = new ArrayList(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 { 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; } }