diff options
Diffstat (limited to 'src/com/android/messaging/ui/mediapicker/CameraManager.java')
-rw-r--r-- | src/com/android/messaging/ui/mediapicker/CameraManager.java | 1200 |
1 files changed, 1200 insertions, 0 deletions
diff --git a/src/com/android/messaging/ui/mediapicker/CameraManager.java b/src/com/android/messaging/ui/mediapicker/CameraManager.java new file mode 100644 index 0000000..166ebd7 --- /dev/null +++ b/src/com/android/messaging/ui/mediapicker/CameraManager.java @@ -0,0 +1,1200 @@ +/* + * Copyright (C) 2015 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.messaging.ui.mediapicker; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.media.MediaRecorder; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.OrientationEventListener; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; + +import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider; +import com.android.messaging.Factory; +import com.android.messaging.datamodel.data.ParticipantData; +import com.android.messaging.datamodel.media.ImageRequest; +import com.android.messaging.sms.MmsConfig; +import com.android.messaging.ui.mediapicker.camerafocus.FocusOverlayManager; +import com.android.messaging.ui.mediapicker.camerafocus.RenderOverlay; +import com.android.messaging.util.Assert; +import com.android.messaging.util.BugleGservices; +import com.android.messaging.util.BugleGservicesKeys; +import com.android.messaging.util.LogUtil; +import com.android.messaging.util.OsUtil; +import com.android.messaging.util.UiUtils; +import com.google.common.annotations.VisibleForTesting; + +import java.io.FileNotFoundException; +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) + */ +class CameraManager implements FocusOverlayManager.Listener { + /** + * Wrapper around the framework camera API to allow mocking different hardware scenarios while + * unit testing + */ + interface CameraWrapper { + int getNumberOfCameras(); + void getCameraInfo(int index, CameraInfo cameraInfo); + Camera open(int cameraId); + /** Add a wrapper for release because a final method cannot be mocked */ + void release(Camera camera); + } + + /** + * Callbacks for the camera manager listener + */ + interface CameraManagerListener { + void onCameraError(int errorCode, Exception e); + void onCameraChanged(); + } + + /** + * Callback when taking image or video + */ + interface MediaCallback { + static final int MEDIA_CAMERA_CHANGED = 1; + static final 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 + static final int ERROR_OPENING_CAMERA = 1; + static final int ERROR_SHOWING_PREVIEW = 2; + static final int ERROR_INITIALIZING_VIDEO = 3; + static final int ERROR_STORAGE_FAILURE = 4; + static final int ERROR_RECORDING_VIDEO = 5; + static final int ERROR_HARDWARE_ACCELERATION_DISABLED = 6; + static final int ERROR_TAKING_PICTURE = 7; + + private static final String TAG = LogUtil.BUGLE_TAG; + private static final int NO_CAMERA_SELECTED = -1; + + private static CameraManager sInstance; + + /** Default camera wrapper which directs calls to the framework APIs */ + private static CameraWrapper sCameraWrapper = new CameraWrapper() { + @Override + public int getNumberOfCameras() { + return Camera.getNumberOfCameras(); + } + + @Override + public void getCameraInfo(final int index, final CameraInfo cameraInfo) { + Camera.getCameraInfo(index, cameraInfo); + } + + @Override + public Camera open(final int cameraId) { + return Camera.open(cameraId); + } + + @Override + public void release(final Camera camera) { + camera.release(); + } + }; + + /** The CameraInfo for the currently selected camera */ + private final CameraInfo mCameraInfo; + + /** + * The index of the selected camera or NO_CAMERA_SELECTED if a camera hasn't been selected yet + */ + private int mCameraIndex; + + /** True if the device has front and back cameras */ + private final boolean mHasFrontAndBackCamera; + + /** True if the camera should be open (may not yet be actually open) */ + private boolean mOpenRequested; + + /** True if the camera is requested to be in video mode */ + private boolean mVideoModeRequested; + + /** The media recorder for video mode */ + private MmsVideoRecorder mMediaRecorder; + + /** Callback to call with video recording updates */ + private MediaCallback mVideoCallback; + + /** The preview view to show the preview on */ + private CameraPreview mCameraPreview; + + /** The helper classs to handle orientation changes */ + private OrientationHandler mOrientationHandler; + + /** Tracks whether the preview has hardware acceleration */ + private boolean mIsHardwareAccelerationSupported; + + /** + * 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: 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> mOpenCameraTask; + + /** + * 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 mPendingOpenCameraIndex = NO_CAMERA_SELECTED; + + /** The instance of the currently opened camera */ + private Camera mCamera; + + /** The rotation of the screen relative to the camera's natural orientation */ + private int mRotation; + + /** The callback to notify when errors or other events occur */ + private CameraManagerListener mListener; + + /** True if the camera is currently in the process of taking an image */ + private boolean mTakingPicture; + + /** Provides subscription-related data to access per-subscription configurations. */ + private DraftMessageSubscriptionDataProvider mSubscriptionDataProvider; + + /** Manages auto focus visual and behavior */ + private final FocusOverlayManager mFocusOverlayManager; + + private CameraManager() { + mCameraInfo = new CameraInfo(); + mCameraIndex = 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 = sCameraWrapper.getNumberOfCameras(); + try { + for (int i = 0; i < cameraCount; i++) { + sCameraWrapper.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(TAG, "Unable to load camera info", e); + } + mHasFrontAndBackCamera = hasFrontCamera && hasBackCamera; + mFocusOverlayManager = new FocusOverlayManager(this, Looper.getMainLooper()); + + // Assume the best until we are proven otherwise + mIsHardwareAccelerationSupported = true; + } + + /** Gets the singleton instance */ + static CameraManager get() { + if (sInstance == null) { + sInstance = new CameraManager(); + } + return sInstance; + } + + /** Allows tests to inject a custom camera wrapper */ + @VisibleForTesting + static void setCameraWrapper(final CameraWrapper cameraWrapper) { + sCameraWrapper = cameraWrapper; + sInstance = null; + } + + /** + * 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 == mCameraPreview) { + return; + } + + if (preview != null) { + Assert.isTrue(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) { + mFocusOverlayManager.setPreviewSize(view.getWidth(), view.getHeight()); + mFocusOverlayManager.onSingleTapUp( + (int) motionEvent.getX() + view.getLeft(), + (int) motionEvent.getY() + view.getTop()); + } + return true; + } + }); + } + mCameraPreview = preview; + tryShowPreview(); + } + + void setRenderOverlay(final RenderOverlay renderOverlay) { + mFocusOverlayManager.setFocusRenderer(renderOverlay != null ? + renderOverlay.getPieRenderer() : null); + } + + /** Convenience function to swap between front and back facing cameras */ + void swapCamera() { + Assert.isTrue(mCameraIndex >= 0); + selectCamera(mCameraInfo.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 + */ + boolean selectCamera(final int desiredFacing) { + try { + // We already selected a camera facing that direction + if (mCameraIndex >= 0 && mCameraInfo.facing == desiredFacing) { + return true; + } + + final int cameraCount = sCameraWrapper.getNumberOfCameras(); + Assert.isTrue(cameraCount > 0); + + mCameraIndex = NO_CAMERA_SELECTED; + setCamera(null); + final CameraInfo cameraInfo = new CameraInfo(); + for (int i = 0; i < cameraCount; i++) { + sCameraWrapper.getCameraInfo(i, cameraInfo); + if (cameraInfo.facing == desiredFacing) { + mCameraIndex = i; + sCameraWrapper.getCameraInfo(i, mCameraInfo); + break; + } + } + + // There's no camera in the desired facing direction, just select the first camera + // regardless of direction + if (mCameraIndex < 0) { + mCameraIndex = 0; + sCameraWrapper.getCameraInfo(0, mCameraInfo); + } + + if (mOpenRequested) { + // The camera is open, so reopen with the newly selected camera + openCamera(); + } + return true; + } catch (final RuntimeException e) { + LogUtil.e(TAG, "RuntimeException in CameraManager.selectCamera", e); + if (mListener != null) { + mListener.onCameraError(ERROR_OPENING_CAMERA, e); + } + return false; + } + } + + int getCameraIndex() { + return mCameraIndex; + } + + void selectCameraByIndex(final int cameraIndex) { + if (mCameraIndex == cameraIndex) { + return; + } + + try { + mCameraIndex = cameraIndex; + sCameraWrapper.getCameraInfo(mCameraIndex, mCameraInfo); + if (mOpenRequested) { + openCamera(); + } + } catch (final RuntimeException e) { + LogUtil.e(TAG, "RuntimeException in CameraManager.selectCameraByIndex", e); + if (mListener != null) { + mListener.onCameraError(ERROR_OPENING_CAMERA, e); + } + } + } + + @VisibleForTesting + CameraInfo getCameraInfo() { + if (mCameraIndex == NO_CAMERA_SELECTED) { + return null; + } + return mCameraInfo; + } + + /** @return True if this device has camera capabilities */ + boolean hasAnyCamera() { + return sCameraWrapper.getNumberOfCameras() > 0; + } + + /** @return True if the device has both a front and back camera */ + boolean hasFrontAndBackCamera() { + return mHasFrontAndBackCamera; + } + + /** + * Opens the camera on a separate thread and initiates the preview if one is available + */ + void openCamera() { + if (mCameraIndex == 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); + } + mOpenRequested = true; + // We're already opening the camera or already have the camera handle, nothing more to do + if (mPendingOpenCameraIndex == mCameraIndex || mCamera != 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 (mOpenCameraTask != null) { + mPendingOpenCameraIndex = NO_CAMERA_SELECTED; + delayTask = true; + } + + mPendingOpenCameraIndex = mCameraIndex; + mOpenCameraTask = new AsyncTask<Integer, Void, Camera>() { + private Exception mException; + + @Override + protected Camera doInBackground(final Integer... params) { + try { + final int cameraIndex = params[0]; + if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { + LogUtil.v(TAG, "Opening camera " + mCameraIndex); + } + return sCameraWrapper.open(cameraIndex); + } catch (final Exception e) { + LogUtil.e(TAG, "Exception while opening camera", e); + mException = e; + return null; + } + } + + @Override + protected void onPostExecute(final Camera camera) { + // If we completed, but no longer want this camera, then release the camera + if (mOpenCameraTask != this || !mOpenRequested) { + releaseCamera(camera); + cleanup(); + return; + } + + cleanup(); + + if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { + LogUtil.v(TAG, "Opened camera " + mCameraIndex + " " + (camera != null)); + } + + setCamera(camera); + if (camera == null) { + if (mListener != null) { + mListener.onCameraError(ERROR_OPENING_CAMERA, mException); + } + LogUtil.e(TAG, "Error opening camera"); + } + } + + @Override + protected void onCancelled() { + super.onCancelled(); + cleanup(); + } + + private void cleanup() { + mPendingOpenCameraIndex = NO_CAMERA_SELECTED; + if (mOpenCameraTask != null && mOpenCameraTask.getStatus() == Status.PENDING) { + // If there's another task waiting on this one to complete, start it now + mOpenCameraTask.execute(mCameraIndex); + } else { + mOpenCameraTask = null; + } + + } + }; + if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { + LogUtil.v(TAG, "Start opening camera " + mCameraIndex); + } + + if (!delayTask) { + mOpenCameraTask.execute(mCameraIndex); + } + } + + boolean isVideoMode() { + return mVideoModeRequested; + } + + boolean isRecording() { + return mVideoModeRequested && mVideoCallback != null; + } + + void setVideoMode(final boolean videoMode) { + if (mVideoModeRequested == videoMode) { + return; + } + mVideoModeRequested = videoMode; + tryInitOrCleanupVideoMode(); + } + + /** Closes the camera releasing the resources it uses */ + void closeCamera() { + mOpenRequested = false; + setCamera(null); + } + + /** Temporarily closes the camera if it is open */ + void onPause() { + setCamera(null); + } + + /** Reopens the camera if it was opened when onPause was called */ + void onResume() { + if (mOpenRequested) { + openCamera(); + } + } + + /** + * Sets the listener which will be notified of errors or other events in the camera + * @param listener The listener to notify + */ + void setListener(final CameraManagerListener listener) { + Assert.isMainThread(); + mListener = listener; + if (!mIsHardwareAccelerationSupported && mListener != null) { + mListener.onCameraError(ERROR_HARDWARE_ACCELERATION_DISABLED, null); + } + } + + void setSubscriptionDataProvider(final DraftMessageSubscriptionDataProvider provider) { + mSubscriptionDataProvider = provider; + } + + void takePicture(final float heightPercent, @NonNull final MediaCallback callback) { + Assert.isTrue(!mVideoModeRequested); + Assert.isTrue(!mTakingPicture); + Assert.notNull(callback); + if (mCamera == 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) { + mTakingPicture = false; + if (mCamera != 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 (mRotation == 90 || mRotation == 270) { + width = size.height; + height = size.width; + } else { + width = size.width; + height = size.height; + } + new ImagePersistTask( + width, height, heightPercent, bytes, mCameraPreview.getContext(), callback) + .executeOnThreadPool(); + } + }; + + mTakingPicture = true; + try { + mCamera.takePicture( + null /* shutter */, + null /* raw */, + null /* postView */, + jpegCallback); + } catch (final RuntimeException e) { + LogUtil.e(TAG, "RuntimeException in CameraManager.takePicture", e); + mTakingPicture = false; + if (mListener != null) { + mListener.onCameraError(ERROR_TAKING_PICTURE, e); + } + } + } + + void startVideo(final MediaCallback callback) { + Assert.notNull(callback); + Assert.isTrue(!isRecording()); + mVideoCallback = callback; + tryStartVideoCapture(); + } + + /** + * Asynchronously releases a camera + * @param camera The camera to release + */ + private void releaseCamera(final Camera camera) { + if (camera == null) { + return; + } + + mFocusOverlayManager.onCameraReleased(); + + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(final Void... params) { + if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { + LogUtil.v(TAG, "Releasing camera " + mCameraIndex); + } + sCameraWrapper.release(camera); + return null; + } + }.execute(); + } + + private void releaseMediaRecorder(final boolean cleanupFile) { + if (mMediaRecorder == null) { + return; + } + mVideoModeRequested = false; + + if (cleanupFile) { + mMediaRecorder.cleanupTempFile(); + if (mVideoCallback != null) { + final MediaCallback callback = mVideoCallback; + mVideoCallback = null; + // Notify the callback that we've stopped recording + callback.onMediaReady(null /*uri*/, null /*contentType*/, 0 /*width*/, + 0 /*height*/); + } + } + + mMediaRecorder.release(); + mMediaRecorder = null; + + if (mCamera != null) { + try { + mCamera.reconnect(); + } catch (final IOException e) { + LogUtil.e(TAG, "IOException in CameraManager.releaseMediaRecorder", e); + if (mListener != null) { + mListener.onCameraError(ERROR_OPENING_CAMERA, e); + } + } catch (final RuntimeException e) { + LogUtil.e(TAG, "RuntimeException in CameraManager.releaseMediaRecorder", e); + if (mListener != null) { + mListener.onCameraError(ERROR_OPENING_CAMERA, e); + } + } + } + restoreRequestedOrientation(); + } + + /** Updates the orientation of the camera to match the orientation of the device */ + private void updateCameraOrientation() { + if (mCamera == null || mCameraPreview == null || mTakingPicture) { + return; + } + + final WindowManager windowManager = + (WindowManager) mCameraPreview.getContext().getSystemService( + Context.WINDOW_SERVICE); + + int degrees = 0; + switch (windowManager.getDefaultDisplay().getRotation()) { + case Surface.ROTATION_0: degrees = 0; break; + case Surface.ROTATION_90: degrees = 90; break; + case Surface.ROTATION_180: degrees = 180; break; + case Surface.ROTATION_270: degrees = 270; break; + } + + // The display orientation of the camera (this controls the preview image). + int orientation; + + // The clockwise rotation angle relative to the orientation of the camera. This affects + // pictures returned by the camera in Camera.PictureCallback. + int rotation; + if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + orientation = (mCameraInfo.orientation + degrees) % 360; + rotation = orientation; + // compensate the mirror but only for orientation + orientation = (360 - orientation) % 360; + } else { // back-facing + orientation = (mCameraInfo.orientation - degrees + 360) % 360; + rotation = orientation; + } + mRotation = rotation; + if (mMediaRecorder == null) { + try { + mCamera.setDisplayOrientation(orientation); + final Camera.Parameters params = mCamera.getParameters(); + params.setRotation(rotation); + mCamera.setParameters(params); + } catch (final RuntimeException e) { + LogUtil.e(TAG, "RuntimeException in CameraManager.updateCameraOrientation", e); + if (mListener != null) { + mListener.onCameraError(ERROR_OPENING_CAMERA, e); + } + } + } + } + + /** Sets the current camera, releasing any previously opened camera */ + private void setCamera(final Camera camera) { + if (mCamera == camera) { + return; + } + + releaseMediaRecorder(true /* cleanupFile */); + releaseCamera(mCamera); + mCamera = camera; + tryShowPreview(); + if (mListener != null) { + mListener.onCameraChanged(); + } + } + + /** Shows the preview if the camera is open and the preview is loaded */ + private void tryShowPreview() { + if (mCameraPreview == null || mCamera == null) { + if (mOrientationHandler != null) { + mOrientationHandler.disable(); + mOrientationHandler = null; + } + releaseMediaRecorder(true /* cleanupFile */); + mFocusOverlayManager.onPreviewStopped(); + return; + } + try { + mCamera.stopPreview(); + updateCameraOrientation(); + + final Camera.Parameters params = mCamera.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); + mCameraPreview.setSize(previewSize, mCameraInfo.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; + } + } + + mCamera.setParameters(params); + mCameraPreview.startPreview(mCamera); + mCamera.startPreview(); + mCamera.setAutoFocusMoveCallback(new Camera.AutoFocusMoveCallback() { + @Override + public void onAutoFocusMoving(final boolean start, final Camera camera) { + mFocusOverlayManager.onAutoFocusMoving(start); + } + }); + mFocusOverlayManager.setParameters(mCamera.getParameters()); + mFocusOverlayManager.setMirror(mCameraInfo.facing == CameraInfo.CAMERA_FACING_BACK); + mFocusOverlayManager.onPreviewStarted(); + tryInitOrCleanupVideoMode(); + if (mOrientationHandler == null) { + mOrientationHandler = new OrientationHandler(mCameraPreview.getContext()); + mOrientationHandler.enable(); + } + } catch (final IOException e) { + LogUtil.e(TAG, "IOException in CameraManager.tryShowPreview", e); + if (mListener != null) { + mListener.onCameraError(ERROR_SHOWING_PREVIEW, e); + } + } catch (final RuntimeException e) { + LogUtil.e(TAG, "RuntimeException in CameraManager.tryShowPreview", e); + if (mListener != null) { + mListener.onCameraError(ERROR_SHOWING_PREVIEW, e); + } + } + } + + private void tryInitOrCleanupVideoMode() { + if (!mVideoModeRequested || mCamera == null || mCameraPreview == null) { + releaseMediaRecorder(true /* cleanupFile */); + return; + } + + if (mMediaRecorder != null) { + return; + } + + try { + mCamera.unlock(); + final int maxMessageSize = getMmsConfig().getMaxMessageSize(); + mMediaRecorder = new MmsVideoRecorder(mCamera, mCameraIndex, mRotation, maxMessageSize); + mMediaRecorder.prepare(); + } catch (final FileNotFoundException e) { + LogUtil.e(TAG, "FileNotFoundException in CameraManager.tryInitOrCleanupVideoMode", e); + if (mListener != null) { + mListener.onCameraError(ERROR_STORAGE_FAILURE, e); + } + setVideoMode(false); + return; + } catch (final IOException e) { + LogUtil.e(TAG, "IOException in CameraManager.tryInitOrCleanupVideoMode", e); + if (mListener != null) { + mListener.onCameraError(ERROR_INITIALIZING_VIDEO, e); + } + setVideoMode(false); + return; + } catch (final RuntimeException e) { + LogUtil.e(TAG, "RuntimeException in CameraManager.tryInitOrCleanupVideoMode", e); + if (mListener != null) { + mListener.onCameraError(ERROR_INITIALIZING_VIDEO, e); + } + setVideoMode(false); + return; + } + + tryStartVideoCapture(); + } + + private void tryStartVideoCapture() { + if (mMediaRecorder == null || mVideoCallback == null) { + return; + } + + mMediaRecorder.setOnErrorListener(new MediaRecorder.OnErrorListener() { + @Override + public void onError(final MediaRecorder mediaRecorder, final int what, + final int extra) { + if (mListener != null) { + mListener.onCameraError(ERROR_RECORDING_VIDEO, null); + } + restoreRequestedOrientation(); + } + }); + + mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { + @Override + public void onInfo(final MediaRecorder mediaRecorder, final int what, final int extra) { + if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED || + what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { + stopVideo(); + } + } + }); + + try { + mMediaRecorder.start(); + final Activity activity = UiUtils.getActivity(mCameraPreview.getContext()); + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + lockOrientation(); + } catch (final IllegalStateException e) { + LogUtil.e(TAG, "IllegalStateException in CameraManager.tryStartVideoCapture", e); + if (mListener != null) { + mListener.onCameraError(ERROR_RECORDING_VIDEO, e); + } + setVideoMode(false); + restoreRequestedOrientation(); + } catch (final RuntimeException e) { + LogUtil.e(TAG, "RuntimeException in CameraManager.tryStartVideoCapture", e); + if (mListener != null) { + mListener.onCameraError(ERROR_RECORDING_VIDEO, e); + } + setVideoMode(false); + restoreRequestedOrientation(); + } + } + + void stopVideo() { + int width = ImageRequest.UNSPECIFIED_SIZE; + int height = ImageRequest.UNSPECIFIED_SIZE; + Uri uri = null; + String contentType = null; + try { + final Activity activity = UiUtils.getActivity(mCameraPreview.getContext()); + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + mMediaRecorder.stop(); + width = mMediaRecorder.getVideoWidth(); + height = mMediaRecorder.getVideoHeight(); + uri = mMediaRecorder.getVideoUri(); + contentType = mMediaRecorder.getContentType(); + } catch (final RuntimeException e) { + // MediaRecorder.stop will throw a RuntimeException if the video was too short, let the + // finally clause call the callback with null uri and handle cleanup + LogUtil.e(TAG, "RuntimeException in CameraManager.stopVideo", e); + } finally { + final MediaCallback videoCallback = mVideoCallback; + mVideoCallback = null; + releaseMediaRecorder(false /* cleanupFile */); + if (uri == null) { + tryInitOrCleanupVideoMode(); + } + videoCallback.onMediaReady(uri, contentType, width, height); + } + } + + boolean isCameraAvailable() { + return mCamera != null && !mTakingPicture && mIsHardwareAccelerationSupported; + } + + /** + * External components call into this to report if hardware acceleration is supported. When + * hardware acceleration isn't supported, we need to report an error through the listener + * interface + * @param isHardwareAccelerationSupported True if the preview is rendering in a hardware + * accelerated view. + */ + void reportHardwareAccelerationSupported(final boolean isHardwareAccelerationSupported) { + Assert.isMainThread(); + if (mIsHardwareAccelerationSupported == isHardwareAccelerationSupported) { + // If the value hasn't changed nothing more to do + return; + } + + mIsHardwareAccelerationSupported = isHardwareAccelerationSupported; + if (!isHardwareAccelerationSupported) { + LogUtil.e(TAG, "Software rendering - cannot open camera"); + if (mListener != null) { + mListener.onCameraError(ERROR_HARDWARE_ACCELERATION_DISABLED, null); + } + } + } + + /** Returns the scale factor to scale the width/height to max allowed in MmsConfig */ + private float getScaleFactorForMaxAllowedSize(final int width, final int height, + final int maxWidth, final int maxHeight) { + if (maxWidth <= 0 || maxHeight <= 0) { + // MmsConfig initialization runs asynchronously on application startup, so there's a + // chance (albeit a very slight one) that we don't have it yet. + LogUtil.w(LogUtil.BUGLE_TAG, "Max image size not loaded in MmsConfig"); + return 1.0f; + } + + if (width <= maxWidth && height <= maxHeight) { + // Already meeting requirements. + return 1.0f; + } + + return Math.min(maxWidth * 1.0f / width, maxHeight * 1.0f / height); + } + + private MmsConfig getMmsConfig() { + final int subId = mSubscriptionDataProvider != null ? + mSubscriptionDataProvider.getConversationSelfSubId() : + ParticipantData.DEFAULT_SELF_SUB_ID; + return MmsConfig.get(subId); + } + + /** + * 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 + */ + private Camera.Size chooseBestPictureSize() { + final Context context = mCameraPreview.getContext(); + final Resources resources = context.getResources(); + final DisplayMetrics displayMetrics = resources.getDisplayMetrics(); + final int displayOrientation = resources.getConfiguration().orientation; + int cameraOrientation = mCameraInfo.orientation; + + int screenWidth; + int screenHeight; + if (displayOrientation == Configuration.ORIENTATION_LANDSCAPE) { + // Rotate the camera orientation 90 degrees to compensate for the rotated display + // metrics. Direction doesn't matter because we're just using it for width/height + cameraOrientation += 90; + } + + // Check the camera orientation relative to the display. + // For 0, 180, 360, the screen width/height are the display width/height + // For 90, 270, the screen width/height are inverted from the display + if (cameraOrientation % 180 == 0) { + screenWidth = displayMetrics.widthPixels; + screenHeight = displayMetrics.heightPixels; + } else { + screenWidth = displayMetrics.heightPixels; + screenHeight = displayMetrics.widthPixels; + } + + final MmsConfig mmsConfig = getMmsConfig(); + final int maxWidth = mmsConfig.getMaxImageWidth(); + final int maxHeight = mmsConfig.getMaxImageHeight(); + + // Constrain the size within the max width/height defined by MmsConfig. + final float scaleFactor = getScaleFactorForMaxAllowedSize(screenWidth, screenHeight, + maxWidth, maxHeight); + screenWidth *= scaleFactor; + screenHeight *= scaleFactor; + + final float aspectRatio = BugleGservices.get().getFloat( + BugleGservicesKeys.CAMERA_ASPECT_RATIO, + screenWidth / (float) screenHeight); + final List<Camera.Size> sizes = new ArrayList<Camera.Size>( + mCamera.getParameters().getSupportedPictureSizes()); + final int maxPixels = maxWidth * maxHeight; + + // Sort the sizes so the best size is first + Collections.sort(sizes, new SizeComparator(maxWidth, maxHeight, aspectRatio, maxPixels)); + + return sizes.get(0); + } + + /** + * 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>( + mCamera.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) { + updateCameraOrientation(); + } + } + + 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 mMaxWidth; + private final int mMaxHeight; + + // The desired aspect ratio + private final float mTargetAspectRatio; + + // The desired size (width x height) to try to match + private final int mTargetPixels; + + public SizeComparator(final int maxWidth, final int maxHeight, + final float targetAspectRatio, final int targetPixels) { + mMaxWidth = maxWidth; + mMaxHeight = maxHeight; + mTargetAspectRatio = targetAspectRatio; + mTargetPixels = 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 <= mMaxWidth && left.height <= mMaxHeight) != + (right.width <= mMaxWidth && right.height <= mMaxHeight)) { + return left.width <= mMaxWidth ? 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 - mTargetAspectRatio); + final float rightAspectRatioDiff = Math.abs(rightAspectRatio - mTargetAspectRatio); + 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) - mTargetPixels); + final int rightDiff = Math.abs((right.width * right.height) - mTargetPixels); + return leftDiff - rightDiff; + } + } + + @Override // From FocusOverlayManager.Listener + public void autoFocus() { + if (mCamera == null) { + return; + } + + try { + mCamera.autoFocus(new Camera.AutoFocusCallback() { + @Override + public void onAutoFocus(final boolean success, final Camera camera) { + mFocusOverlayManager.onAutoFocus(success, false /* shutterDown */); + } + }); + } catch (final RuntimeException e) { + LogUtil.e(TAG, "RuntimeException in CameraManager.autoFocus", e); + // If autofocus fails, the camera should have called the callback with success=false, + // but some throw an exception here + mFocusOverlayManager.onAutoFocus(false /*success*/, false /*shutterDown*/); + } + } + + @Override // From FocusOverlayManager.Listener + public void cancelAutoFocus() { + if (mCamera == null) { + return; + } + try { + mCamera.cancelAutoFocus(); + } catch (final RuntimeException e) { + // Ignore + LogUtil.e(TAG, "RuntimeException in CameraManager.cancelAutoFocus", e); + } + } + + @Override // From FocusOverlayManager.Listener + public boolean capture() { + return false; + } + + @Override // From FocusOverlayManager.Listener + public void setFocusParameters() { + if (mCamera == null) { + return; + } + try { + final Camera.Parameters parameters = mCamera.getParameters(); + parameters.setFocusMode(mFocusOverlayManager.getFocusMode()); + if (parameters.getMaxNumFocusAreas() > 0) { + // Don't set focus areas (even to null) if focus areas aren't supported, camera may + // crash + parameters.setFocusAreas(mFocusOverlayManager.getFocusAreas()); + } + parameters.setMeteringAreas(mFocusOverlayManager.getMeteringAreas()); + mCamera.setParameters(parameters); + } catch (final RuntimeException e) { + // This occurs when the device is out of space or when the camera is locked + LogUtil.e(TAG, "RuntimeException in CameraManager setFocusParameters"); + } + } + + 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(TAG, prefix + size.width + "x" + size.height + + " (" + (size.width / (float) size.height) + ")"); + } + + + private Integer mSavedOrientation = null; + + private void lockOrientation() { + // when we start recording, lock our orientation + final Activity a = UiUtils.getActivity(mCameraPreview.getContext()); + final WindowManager windowManager = + (WindowManager) a.getSystemService(Context.WINDOW_SERVICE); + final int rotation = windowManager.getDefaultDisplay().getRotation(); + + mSavedOrientation = a.getRequestedOrientation(); + switch (rotation) { + case Surface.ROTATION_0: + a.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + break; + case Surface.ROTATION_90: + a.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + break; + case Surface.ROTATION_180: + a.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT); + break; + case Surface.ROTATION_270: + a.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); + break; + } + + } + + private void restoreRequestedOrientation() { + if (mSavedOrientation != null) { + final Activity a = UiUtils.getActivity(mCameraPreview.getContext()); + if (a != null) { + a.setRequestedOrientation(mSavedOrientation); + } + mSavedOrientation = null; + } + } + + static boolean hasCameraPermission() { + return OsUtil.hasPermission(Manifest.permission.CAMERA); + } +} |