| /* |
| * Copyright (C) 2014 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.camera; |
| |
| import android.content.Context; |
| import android.graphics.Matrix; |
| import android.graphics.Point; |
| import android.graphics.RectF; |
| import android.graphics.SurfaceTexture; |
| import android.location.Location; |
| import android.media.MediaActionSound; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.SystemClock; |
| import android.view.GestureDetector; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.Surface; |
| import android.view.View; |
| |
| import com.android.camera.app.AppController; |
| import com.android.camera.app.CameraAppUI; |
| import com.android.camera.app.CameraAppUI.BottomBarUISpec; |
| import com.android.camera.app.LocationManager; |
| import com.android.camera.app.OrientationManager.DeviceOrientation; |
| import com.android.camera.async.MainThread; |
| import com.android.camera.burst.BurstFacade; |
| import com.android.camera.burst.BurstFacadeFactory; |
| import com.android.camera.burst.BurstReadyStateChangeListener; |
| import com.android.camera.burst.OrientationLockController; |
| import com.android.camera.captureintent.PreviewTransformCalculator; |
| import com.android.camera.debug.DebugPropertyHelper; |
| import com.android.camera.debug.Log; |
| import com.android.camera.debug.Log.Tag; |
| import com.android.camera.device.CameraId; |
| import com.android.camera.hardware.HardwareSpec; |
| import com.android.camera.hardware.HeadingSensor; |
| import com.android.camera.module.ModuleController; |
| import com.android.camera.one.OneCamera; |
| import com.android.camera.one.OneCamera.AutoFocusState; |
| import com.android.camera.one.OneCamera.CaptureReadyCallback; |
| import com.android.camera.one.OneCamera.Facing; |
| import com.android.camera.one.OneCamera.OpenCallback; |
| import com.android.camera.one.OneCamera.PhotoCaptureParameters; |
| import com.android.camera.one.OneCameraAccessException; |
| import com.android.camera.one.OneCameraCaptureSetting; |
| import com.android.camera.one.OneCameraCharacteristics; |
| import com.android.camera.one.OneCameraException; |
| import com.android.camera.one.OneCameraManager; |
| import com.android.camera.one.OneCameraModule; |
| import com.android.camera.one.OneCameraOpener; |
| import com.android.camera.one.config.OneCameraFeatureConfig; |
| import com.android.camera.one.v2.photo.ImageRotationCalculator; |
| import com.android.camera.one.v2.photo.ImageRotationCalculatorImpl; |
| import com.android.camera.remote.RemoteCameraModule; |
| import com.android.camera.session.CaptureSession; |
| import com.android.camera.settings.Keys; |
| import com.android.camera.settings.SettingsManager; |
| import com.android.camera.stats.CaptureStats; |
| import com.android.camera.stats.UsageStatistics; |
| import com.android.camera.stats.profiler.Profile; |
| import com.android.camera.stats.profiler.Profiler; |
| import com.android.camera.stats.profiler.Profilers; |
| import com.android.camera.ui.CountDownView; |
| import com.android.camera.ui.PreviewStatusListener; |
| import com.android.camera.ui.TouchCoordinate; |
| import com.android.camera.ui.focus.FocusController; |
| import com.android.camera.ui.focus.FocusSound; |
| import com.android.camera.util.AndroidServices; |
| import com.android.camera.util.ApiHelper; |
| import com.android.camera.util.CameraUtil; |
| import com.android.camera.util.GcamHelper; |
| import com.android.camera.util.Size; |
| import com.android.camera2.R; |
| import com.android.ex.camera2.portability.CameraAgent.CameraProxy; |
| import com.google.common.logging.eventprotos; |
| |
| import java.util.concurrent.Semaphore; |
| import java.util.concurrent.TimeUnit; |
| |
| import javax.annotation.Nonnull; |
| |
| /** |
| * New Capture module that is made to support photo and video capture on top of |
| * the OneCamera API, to transparently support GCam. |
| * <p> |
| * This has been a re-write with pieces taken and improved from GCamModule and |
| * PhotoModule, which are to be retired eventually. |
| * <p> |
| */ |
| public class CaptureModule extends CameraModule implements |
| ModuleController, |
| CountDownView.OnCountDownStatusListener, |
| OneCamera.PictureCallback, |
| OneCamera.FocusStateListener, |
| OneCamera.ReadyStateChangedListener, |
| RemoteCameraModule { |
| |
| private static final Tag TAG = new Tag("CaptureModule"); |
| /** Enable additional debug output. */ |
| private static final boolean DEBUG = true; |
| /** Workaround Flag for b/19271661 to use autotransformation in Capture Layout in Nexus4 **/ |
| private static final boolean USE_AUTOTRANSFORM_UI_LAYOUT = ApiHelper.IS_NEXUS_4; |
| |
| /** Timeout for camera open/close operations. */ |
| private static final int CAMERA_OPEN_CLOSE_TIMEOUT_MILLIS = 2500; |
| |
| /** System Properties switch to enable debugging focus UI. */ |
| private static final boolean CAPTURE_DEBUG_UI = DebugPropertyHelper.showCaptureDebugUI(); |
| |
| private final Object mDimensionLock = new Object(); |
| |
| /** |
| * Sticky Gcam mode is when this module's sole purpose it to be the Gcam |
| * mode. If true, the device uses {@link PhotoModule} for normal picture |
| * taking. |
| */ |
| private final boolean mStickyGcamCamera; |
| |
| /** Controller giving us access to other services. */ |
| private final AppController mAppController; |
| /** The applications settings manager. */ |
| private final SettingsManager mSettingsManager; |
| /** Application context. */ |
| private final Context mContext; |
| /** Module UI. */ |
| private CaptureModuleUI mUI; |
| /** The camera manager used to open cameras. */ |
| private OneCameraOpener mOneCameraOpener; |
| /** The manager to query for camera device information */ |
| private OneCameraManager mOneCameraManager; |
| /** The currently opened camera device, or null if the camera is closed. */ |
| private OneCamera mCamera; |
| /** The selected picture size. */ |
| private Size mPictureSize; |
| /** Fair semaphore held when opening or closing the camera. */ |
| private final Semaphore mCameraOpenCloseLock = new Semaphore(1, true); |
| /** The direction the currently opened camera is facing to. */ |
| private Facing mCameraFacing; |
| /** Whether HDR Scene mode is currently enabled. */ |
| private boolean mHdrSceneEnabled = false; |
| private boolean mHdrPlusEnabled = false; |
| private final Object mSurfaceTextureLock = new Object(); |
| /** |
| * Flag that is used when Fatal Error Handler is running and the app should |
| * not continue execution |
| */ |
| private boolean mShowErrorAndFinish; |
| private TouchCoordinate mLastShutterTouchCoordinate = null; |
| |
| private FocusController mFocusController; |
| private OneCameraCharacteristics mCameraCharacteristics; |
| final private PreviewTransformCalculator mPreviewTransformCalculator; |
| |
| /** The listener to listen events from the CaptureModuleUI. */ |
| private final CaptureModuleUI.CaptureModuleUIListener mUIListener = |
| new CaptureModuleUI.CaptureModuleUIListener() { |
| @Override |
| public void onZoomRatioChanged(float zoomRatio) { |
| mZoomValue = zoomRatio; |
| if (mCamera != null) { |
| mCamera.setZoom(zoomRatio); |
| } |
| } |
| }; |
| |
| /** The listener to respond preview area changes. */ |
| private final PreviewStatusListener.PreviewAreaChangedListener mPreviewAreaChangedListener = |
| new PreviewStatusListener.PreviewAreaChangedListener() { |
| @Override |
| public void onPreviewAreaChanged(RectF previewArea) { |
| mPreviewArea = previewArea; |
| mFocusController.configurePreviewDimensions(previewArea); |
| } |
| }; |
| |
| /** The listener to listen events from the preview. */ |
| private final PreviewStatusListener mPreviewStatusListener = new PreviewStatusListener() { |
| @Override |
| public void onPreviewLayoutChanged(View v, int left, int top, int right, |
| int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { |
| int width = right - left; |
| int height = bottom - top; |
| updatePreviewTransform(width, height, false); |
| } |
| |
| @Override |
| public boolean shouldAutoAdjustTransformMatrixOnLayout() { |
| return USE_AUTOTRANSFORM_UI_LAYOUT; |
| } |
| |
| @Override |
| public void onPreviewFlipped() { |
| // Do nothing because when preview is flipped, TextureView will lay |
| // itself out again, which will then trigger a transform matrix |
| // update. |
| } |
| |
| @Override |
| public GestureDetector.OnGestureListener getGestureListener() { |
| return new GestureDetector.SimpleOnGestureListener() { |
| @Override |
| public boolean onSingleTapUp(MotionEvent ev) { |
| Point tapPoint = new Point((int) ev.getX(), (int) ev.getY()); |
| Log.v(TAG, "onSingleTapUpPreview location=" + tapPoint); |
| if (!mCameraCharacteristics.isAutoExposureSupported() && |
| !mCameraCharacteristics.isAutoFocusSupported()) { |
| return false; |
| } |
| startActiveFocusAt(tapPoint.x, tapPoint.y); |
| return true; |
| } |
| }; |
| } |
| |
| @Override |
| public View.OnTouchListener getTouchListener() { |
| return null; |
| } |
| |
| @Override |
| public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { |
| Log.d(TAG, "onSurfaceTextureAvailable"); |
| // Force to re-apply transform matrix here as a workaround for |
| // b/11168275 |
| updatePreviewTransform(width, height, true); |
| synchronized (mSurfaceTextureLock) { |
| mPreviewSurfaceTexture = surface; |
| } |
| reopenCamera(); |
| } |
| |
| @Override |
| public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { |
| Log.d(TAG, "onSurfaceTextureDestroyed"); |
| synchronized (mSurfaceTextureLock) { |
| mPreviewSurfaceTexture = null; |
| } |
| closeCamera(); |
| return true; |
| } |
| |
| @Override |
| public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { |
| Log.d(TAG, "onSurfaceTextureSizeChanged"); |
| updatePreviewBufferSize(); |
| } |
| |
| @Override |
| public void onSurfaceTextureUpdated(SurfaceTexture surface) { |
| if (mState == ModuleState.UPDATE_TRANSFORM_ON_NEXT_SURFACE_TEXTURE_UPDATE) { |
| Log.d(TAG, "onSurfaceTextureUpdated --> updatePreviewTransform"); |
| mState = ModuleState.IDLE; |
| CameraAppUI appUI = mAppController.getCameraAppUI(); |
| updatePreviewTransform(appUI.getSurfaceWidth(), appUI.getSurfaceHeight(), true); |
| } |
| } |
| }; |
| |
| private final OneCamera.PictureSaverCallback mPictureSaverCallback = |
| new OneCamera.PictureSaverCallback() { |
| @Override |
| public void onRemoteThumbnailAvailable(final byte[] jpegImage) { |
| mMainThread.execute(new Runnable() { |
| @Override |
| public void run() { |
| mAppController.getServices().getRemoteShutterListener() |
| .onPictureTaken(jpegImage); |
| } |
| }); |
| } |
| }; |
| |
| /** State by the module state machine. */ |
| private static enum ModuleState { |
| IDLE, |
| WATCH_FOR_NEXT_FRAME_AFTER_PREVIEW_STARTED, |
| UPDATE_TRANSFORM_ON_NEXT_SURFACE_TEXTURE_UPDATE, |
| } |
| |
| /** The current state of the module. */ |
| private ModuleState mState = ModuleState.IDLE; |
| /** Current zoom value. */ |
| private float mZoomValue = 1f; |
| |
| /** Records beginning frame of each AF scan. */ |
| private long mAutoFocusScanStartFrame = -1; |
| /** Records beginning time of each AF scan in uptimeMillis. */ |
| private long mAutoFocusScanStartTime; |
| |
| /** Heading sensor. */ |
| private HeadingSensor mHeadingSensor; |
| |
| /** Used to fetch and embed the location into captured images. */ |
| private final LocationManager mLocationManager; |
| /** Plays sounds for countdown timer. */ |
| private SoundPlayer mSoundPlayer; |
| private final MediaActionSound mMediaActionSound; |
| |
| /** Whether the module is paused right now. */ |
| private boolean mPaused; |
| |
| /** Main thread. */ |
| private final MainThread mMainThread; |
| /** Handler thread for camera-related operations. */ |
| private Handler mCameraHandler; |
| |
| /** Current display rotation in degrees. */ |
| private int mDisplayRotation; |
| /** Current screen width in pixels. */ |
| private int mScreenWidth; |
| /** Current screen height in pixels. */ |
| private int mScreenHeight; |
| /** Current width of preview frames from camera. */ |
| private int mPreviewBufferWidth; |
| /** Current height of preview frames from camera.. */ |
| private int mPreviewBufferHeight; |
| /** Area used by preview. */ |
| RectF mPreviewArea; |
| |
| /** The surface texture for the preview. */ |
| private SurfaceTexture mPreviewSurfaceTexture; |
| |
| /** The burst manager for controlling the burst. */ |
| private final BurstFacade mBurstController; |
| private static final String BURST_SESSIONS_DIR = "burst_sessions"; |
| |
| private final Profiler mProfiler = Profilers.instance().guard(); |
| |
| public CaptureModule(AppController appController) { |
| this(appController, false); |
| } |
| |
| /** Constructs a new capture module. */ |
| public CaptureModule(AppController appController, boolean stickyHdr) { |
| super(appController); |
| Profile guard = mProfiler.create("new CaptureModule").start(); |
| mPaused = true; |
| mMainThread = MainThread.create(); |
| mAppController = appController; |
| mContext = mAppController.getAndroidContext(); |
| mSettingsManager = mAppController.getSettingsManager(); |
| mStickyGcamCamera = stickyHdr; |
| mLocationManager = mAppController.getLocationManager(); |
| mPreviewTransformCalculator = new PreviewTransformCalculator( |
| mAppController.getOrientationManager()); |
| |
| mBurstController = BurstFacadeFactory.create(mContext, |
| new OrientationLockController() { |
| @Override |
| public void unlockOrientation() { |
| mAppController.getOrientationManager().unlockOrientation(); |
| } |
| |
| @Override |
| public void lockOrientation() { |
| mAppController.getOrientationManager().lockOrientation(); |
| } |
| }, |
| new BurstReadyStateChangeListener() { |
| @Override |
| public void onBurstReadyStateChanged(boolean ready) { |
| // TODO: This needs to take into account the state of |
| // the whole system, not just burst. |
| onReadyStateChanged(false); |
| } |
| }); |
| mMediaActionSound = new MediaActionSound(); |
| guard.stop(); |
| } |
| |
| private boolean updateCameraCharacteristics() { |
| try { |
| CameraId cameraId = mOneCameraManager.findFirstCameraFacing(mCameraFacing); |
| if (cameraId != null && cameraId.getValue() != null) { |
| mCameraCharacteristics = mOneCameraManager.getOneCameraCharacteristics(cameraId); |
| return mCameraCharacteristics != null; |
| } |
| } catch (OneCameraAccessException ignored) { } |
| mAppController.getFatalErrorHandler().onGenericCameraAccessFailure(); |
| return false; |
| } |
| |
| @Override |
| public void init(CameraActivity activity, boolean isSecureCamera, boolean isCaptureIntent) { |
| Profile guard = mProfiler.create("CaptureModule.init").start(); |
| Log.d(TAG, "init UseAutotransformUiLayout = " + USE_AUTOTRANSFORM_UI_LAYOUT); |
| HandlerThread thread = new HandlerThread("CaptureModule.mCameraHandler"); |
| thread.start(); |
| mCameraHandler = new Handler(thread.getLooper()); |
| mOneCameraOpener = mAppController.getCameraOpener(); |
| |
| try { |
| mOneCameraManager = OneCameraModule.provideOneCameraManager(); |
| } catch (OneCameraException e) { |
| Log.e(TAG, "Unable to provide a OneCameraManager. ", e); |
| } |
| mDisplayRotation = CameraUtil.getDisplayRotation(activity); |
| mCameraFacing = getFacingFromCameraId( |
| mSettingsManager.getInteger(mAppController.getModuleScope(), Keys.KEY_CAMERA_ID)); |
| mShowErrorAndFinish = !updateCameraCharacteristics(); |
| if (mShowErrorAndFinish) { |
| return; |
| } |
| mUI = new CaptureModuleUI(activity, mAppController.getModuleLayoutRoot(), mUIListener); |
| mAppController.setPreviewStatusListener(mPreviewStatusListener); |
| synchronized (mSurfaceTextureLock) { |
| mPreviewSurfaceTexture = mAppController.getCameraAppUI().getSurfaceTexture(); |
| } |
| mSoundPlayer = new SoundPlayer(mContext); |
| |
| FocusSound focusSound = new FocusSound(mSoundPlayer, R.raw.material_camera_focus); |
| mFocusController = new FocusController(mUI.getFocusRing(), focusSound, mMainThread); |
| |
| mHeadingSensor = new HeadingSensor(AndroidServices.instance().provideSensorManager()); |
| |
| View cancelButton = activity.findViewById(R.id.shutter_cancel_button); |
| cancelButton.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| cancelCountDown(); |
| } |
| }); |
| |
| mMediaActionSound.load(MediaActionSound.SHUTTER_CLICK); |
| guard.stop(); |
| } |
| |
| @Override |
| public void onShutterButtonLongPressed() { |
| try { |
| OneCameraCharacteristics cameraCharacteristics; |
| CameraId cameraId = mOneCameraManager.findFirstCameraFacing(mCameraFacing); |
| cameraCharacteristics = mOneCameraManager.getOneCameraCharacteristics(cameraId); |
| DeviceOrientation deviceOrientation = mAppController.getOrientationManager() |
| .getDeviceOrientation(); |
| ImageRotationCalculator imageRotationCalculator = ImageRotationCalculatorImpl |
| .from(mAppController.getOrientationManager(), cameraCharacteristics); |
| |
| mBurstController.startBurst( |
| new CaptureSession.CaptureSessionCreator() { |
| @Override |
| public CaptureSession createAndStartEmpty() { |
| return createAndStartUntrackedCaptureSession(); |
| } |
| }, |
| deviceOrientation, |
| mCamera.getDirection(), |
| imageRotationCalculator.toImageRotation().getDegrees()); |
| |
| } catch (OneCameraAccessException e) { |
| Log.e(TAG, "Cannot start burst", e); |
| return; |
| } |
| } |
| |
| @Override |
| public void onShutterButtonFocus(boolean pressed) { |
| if (!pressed) { |
| // the shutter button was released, stop any bursts. |
| mBurstController.stopBurst(); |
| } |
| } |
| |
| @Override |
| public void onShutterCoordinate(TouchCoordinate coord) { |
| mLastShutterTouchCoordinate = coord; |
| } |
| |
| @Override |
| public void onShutterButtonClick() { |
| if (mCamera == null) { |
| return; |
| } |
| |
| int countDownDuration = mSettingsManager |
| .getInteger(SettingsManager.SCOPE_GLOBAL, Keys.KEY_COUNTDOWN_DURATION); |
| if (countDownDuration > 0) { |
| // Start count down. |
| mAppController.getCameraAppUI().transitionToCancel(); |
| mAppController.getCameraAppUI().hideModeOptions(); |
| mUI.setCountdownFinishedListener(this); |
| mUI.startCountdown(countDownDuration); |
| // Will take picture later via listener callback. |
| } else { |
| takePictureNow(); |
| } |
| } |
| |
| |
| private void decorateSessionAtCaptureTime(CaptureSession session) { |
| String flashSetting = |
| mSettingsManager.getString(mAppController.getCameraScope(), |
| Keys.KEY_FLASH_MODE); |
| boolean gridLinesOn = Keys.areGridLinesOn(mSettingsManager); |
| float timerDuration = mSettingsManager |
| .getInteger(SettingsManager.SCOPE_GLOBAL, Keys.KEY_COUNTDOWN_DURATION); |
| |
| session.getCollector().decorateAtTimeCaptureRequest( |
| eventprotos.NavigationChange.Mode.PHOTO_CAPTURE, |
| session.getTitle() + ".jpg", |
| (mCameraFacing == Facing.FRONT), |
| mHdrSceneEnabled, |
| mZoomValue, |
| flashSetting, |
| gridLinesOn, |
| timerDuration, |
| mLastShutterTouchCoordinate, |
| null /* TODO: Implement Volume Button Shutter Click Instrumentation */, |
| mCameraCharacteristics.getSensorInfoActiveArraySize() |
| ); |
| } |
| |
| private void takePictureNow() { |
| if (mCamera == null) { |
| Log.i(TAG, "Not taking picture since Camera is closed."); |
| return; |
| } |
| |
| CaptureSession session = createAndStartCaptureSession(); |
| int orientation = mAppController.getOrientationManager().getDeviceOrientation() |
| .getDegrees(); |
| |
| // TODO: This should really not use getExternalCacheDir and instead use |
| // the SessionStorage API. Need to sync with gcam if that's OK. |
| PhotoCaptureParameters params = new PhotoCaptureParameters( |
| session.getTitle(), orientation, session.getLocation(), |
| mContext.getExternalCacheDir(), this, mPictureSaverCallback, |
| mHeadingSensor.getCurrentHeading(), mZoomValue, 0); |
| decorateSessionAtCaptureTime(session); |
| mCamera.takePicture(params, session); |
| } |
| |
| /** |
| * Creates, starts and returns a new capture session. The returned session |
| * will have been started with an empty placeholder image. |
| */ |
| private CaptureSession createAndStartCaptureSession() { |
| long sessionTime = getSessionTime(); |
| Location location = mLocationManager.getCurrentLocation(); |
| String title = CameraUtil.instance().createJpegName(sessionTime); |
| CaptureSession session = getServices().getCaptureSessionManager() |
| .createNewSession(title, sessionTime, location); |
| |
| session.startEmpty(new CaptureStats(mHdrPlusEnabled), |
| new Size((int) mPreviewArea.width(), (int) mPreviewArea.height())); |
| return session; |
| } |
| |
| private CaptureSession createAndStartUntrackedCaptureSession() { |
| long sessionTime = getSessionTime(); |
| Location location = mLocationManager.getCurrentLocation(); |
| String title = CameraUtil.instance().createJpegName(sessionTime); |
| CaptureSession session = getServices().getCaptureSessionManager() |
| .createNewSession(title, sessionTime, location); |
| |
| session.startEmpty(null, |
| new Size((int) mPreviewArea.width(), (int) mPreviewArea.height())); |
| return session; |
| } |
| |
| private long getSessionTime() { |
| // TODO: Replace with a mockable TimeProvider interface. |
| return System.currentTimeMillis(); |
| } |
| |
| @Override |
| public void onCountDownFinished() { |
| mAppController.getCameraAppUI().transitionToCapture(); |
| mAppController.getCameraAppUI().showModeOptions(); |
| if (mPaused) { |
| return; |
| } |
| takePictureNow(); |
| } |
| |
| @Override |
| public void onRemainingSecondsChanged(int remainingSeconds) { |
| if (remainingSeconds == 1) { |
| mSoundPlayer.play(R.raw.timer_final_second, 0.6f); |
| } else if (remainingSeconds == 2 || remainingSeconds == 3) { |
| mSoundPlayer.play(R.raw.timer_increment, 0.6f); |
| } |
| } |
| |
| private void cancelCountDown() { |
| if (mUI.isCountingDown()) { |
| // Cancel on-going countdown. |
| mUI.cancelCountDown(); |
| } |
| |
| if (!mPaused) { |
| mAppController.getCameraAppUI().showModeOptions(); |
| mAppController.getCameraAppUI().transitionToCapture(); |
| } |
| } |
| |
| @Override |
| public void onQuickExpose() { |
| mMainThread.execute(new Runnable() { |
| @Override |
| public void run() { |
| // Starts the short version of the capture animation UI. |
| mAppController.startFlashAnimation(true); |
| mMediaActionSound.play(MediaActionSound.SHUTTER_CLICK); |
| } |
| }); |
| } |
| |
| @Override |
| public void onRemoteShutterPress() { |
| Log.d(TAG, "onRemoteShutterPress"); |
| // TODO: Check whether shutter is enabled. |
| takePictureNow(); |
| } |
| |
| private void initSurfaceTextureConsumer() { |
| synchronized (mSurfaceTextureLock) { |
| if (mPreviewSurfaceTexture != null) { |
| mPreviewSurfaceTexture.setDefaultBufferSize( |
| mAppController.getCameraAppUI().getSurfaceWidth(), |
| mAppController.getCameraAppUI().getSurfaceHeight()); |
| } |
| } |
| reopenCamera(); |
| } |
| |
| private void reopenCamera() { |
| if (mPaused) { |
| return; |
| } |
| AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() { |
| @Override |
| public void run() { |
| closeCamera(); |
| if(!mAppController.isPaused()) { |
| openCameraAndStartPreview(); |
| } |
| } |
| }); |
| } |
| |
| private SurfaceTexture getPreviewSurfaceTexture() { |
| synchronized (mSurfaceTextureLock) { |
| return mPreviewSurfaceTexture; |
| } |
| } |
| |
| private void updatePreviewBufferSize() { |
| synchronized (mSurfaceTextureLock) { |
| if (mPreviewSurfaceTexture != null) { |
| mPreviewSurfaceTexture.setDefaultBufferSize(mPreviewBufferWidth, |
| mPreviewBufferHeight); |
| } |
| } |
| } |
| |
| @Override |
| public void resume() { |
| if (mShowErrorAndFinish) { |
| return; |
| } |
| Profile guard = mProfiler.create("CaptureModule.resume").start(); |
| |
| // We'll transition into 'ready' once the preview is started. |
| onReadyStateChanged(false); |
| mPaused = false; |
| mAppController.addPreviewAreaSizeChangedListener(mPreviewAreaChangedListener); |
| mAppController.addPreviewAreaSizeChangedListener(mUI); |
| |
| guard.mark(); |
| getServices().getRemoteShutterListener().onModuleReady(this); |
| guard.mark("getRemoteShutterListener.onModuleReady"); |
| mBurstController.initialize(new SurfaceTexture(0)); |
| |
| // TODO: Check if we can really take a photo right now (memory, camera |
| // state, ... ). |
| mAppController.getCameraAppUI().enableModeOptions(); |
| mAppController.setShutterEnabled(true); |
| mAppController.getCameraAppUI().showAccessibilityZoomUI( |
| mCameraCharacteristics.getAvailableMaxDigitalZoom()); |
| |
| mHdrPlusEnabled = mStickyGcamCamera || mAppController.getSettingsManager().getInteger( |
| SettingsManager.SCOPE_GLOBAL, Keys.KEY_CAMERA_HDR_PLUS) == 1; |
| |
| mHdrSceneEnabled = !mStickyGcamCamera && mAppController.getSettingsManager().getBoolean( |
| SettingsManager.SCOPE_GLOBAL, Keys.KEY_CAMERA_HDR); |
| |
| // This means we are resuming with an existing preview texture. This |
| // means we will never get the onSurfaceTextureAvailable call. So we |
| // have to open the camera and start the preview here. |
| SurfaceTexture texture = getPreviewSurfaceTexture(); |
| |
| guard.mark(); |
| if (texture != null) { |
| initSurfaceTextureConsumer(); |
| guard.mark("initSurfaceTextureConsumer"); |
| } |
| |
| mSoundPlayer.loadSound(R.raw.timer_final_second); |
| mSoundPlayer.loadSound(R.raw.timer_increment); |
| |
| guard.mark(); |
| mHeadingSensor.activate(); |
| guard.stop("mHeadingSensor.activate()"); |
| } |
| |
| @Override |
| public void pause() { |
| if (mShowErrorAndFinish) { |
| return; |
| } |
| cancelCountDown(); |
| mPaused = true; |
| mHeadingSensor.deactivate(); |
| |
| mAppController.removePreviewAreaSizeChangedListener(mUI); |
| mAppController.removePreviewAreaSizeChangedListener(mPreviewAreaChangedListener); |
| getServices().getRemoteShutterListener().onModuleExit(); |
| mBurstController.release(); |
| closeCamera(); |
| resetTextureBufferSize(); |
| mSoundPlayer.unloadSound(R.raw.timer_final_second); |
| mSoundPlayer.unloadSound(R.raw.timer_increment); |
| } |
| |
| @Override |
| public void destroy() { |
| mSoundPlayer.release(); |
| mMediaActionSound.release(); |
| mCameraHandler.getLooper().quitSafely(); |
| } |
| |
| @Override |
| public void onLayoutOrientationChanged(boolean isLandscape) { |
| Log.d(TAG, "onLayoutOrientationChanged"); |
| } |
| |
| @Override |
| public void onCameraAvailable(CameraProxy cameraProxy) { |
| // Ignore since we manage the camera ourselves until we remove this. |
| } |
| |
| @Override |
| public void hardResetSettings(SettingsManager settingsManager) { |
| if (mStickyGcamCamera) { |
| // Sticky HDR+ mode should hard reset HDR+ to on, and camera back |
| // facing. |
| settingsManager.set(SettingsManager.SCOPE_GLOBAL, Keys.KEY_CAMERA_HDR_PLUS, true); |
| settingsManager.set(mAppController.getModuleScope(), Keys.KEY_CAMERA_ID, |
| mOneCameraManager.findFirstCameraFacing(Facing.BACK).getValue()); |
| } |
| } |
| |
| @Override |
| public HardwareSpec getHardwareSpec() { |
| return new HardwareSpec() { |
| @Override |
| public boolean isFrontCameraSupported() { |
| return mOneCameraManager.hasCameraFacing(Facing.FRONT); |
| } |
| |
| @Override |
| public boolean isHdrSupported() { |
| if (ApiHelper.IS_NEXUS_4 && is16by9AspectRatio(mPictureSize)) { |
| Log.v(TAG, "16:9 N4, no HDR support"); |
| return false; |
| } else { |
| return mCameraCharacteristics.isHdrSceneSupported(); |
| } |
| } |
| |
| @Override |
| public boolean isHdrPlusSupported() { |
| OneCameraFeatureConfig featureConfig = mAppController.getCameraFeatureConfig(); |
| return featureConfig.getHdrPlusSupportLevel(mCameraFacing) != |
| OneCameraFeatureConfig.HdrPlusSupportLevel.NONE; |
| } |
| |
| @Override |
| public boolean isFlashSupported() { |
| return mCameraCharacteristics.isFlashSupported(); |
| } |
| }; |
| } |
| |
| @Override |
| public BottomBarUISpec getBottomBarSpec() { |
| HardwareSpec hardwareSpec = getHardwareSpec(); |
| BottomBarUISpec bottomBarSpec = new BottomBarUISpec(); |
| bottomBarSpec.enableGridLines = true; |
| bottomBarSpec.enableCamera = true; |
| bottomBarSpec.cameraCallback = getCameraCallback(); |
| bottomBarSpec.enableHdr = |
| hardwareSpec.isHdrSupported() || hardwareSpec.isHdrPlusSupported(); |
| bottomBarSpec.hdrCallback = getHdrButtonCallback(); |
| bottomBarSpec.enableSelfTimer = true; |
| bottomBarSpec.showSelfTimer = true; |
| bottomBarSpec.isExposureCompensationSupported = mCameraCharacteristics |
| .isExposureCompensationSupported(); |
| bottomBarSpec.enableExposureCompensation = bottomBarSpec.isExposureCompensationSupported; |
| |
| // We must read the key from the settings because the button callback |
| // is not executed until after this method is called. |
| if ((hardwareSpec.isHdrPlusSupported() && |
| mAppController.getSettingsManager().getBoolean( |
| SettingsManager.SCOPE_GLOBAL, Keys.KEY_CAMERA_HDR_PLUS)) || |
| ( hardwareSpec.isHdrSupported() && |
| mAppController.getSettingsManager().getBoolean( |
| SettingsManager.SCOPE_GLOBAL, Keys.KEY_CAMERA_HDR))) { |
| // Disable flash if this is a sticky gcam camera, or if |
| // HDR is enabled. |
| bottomBarSpec.enableFlash = false; |
| // Disable manual exposure if HDR is enabled. |
| bottomBarSpec.enableExposureCompensation = false; |
| } else { |
| // If we are not in HDR / GCAM mode, fallback on the |
| // flash supported property and manual exposure supported property |
| // for this camera. |
| bottomBarSpec.enableFlash = mCameraCharacteristics.isFlashSupported(); |
| } |
| |
| bottomBarSpec.minExposureCompensation = |
| mCameraCharacteristics.getMinExposureCompensation(); |
| bottomBarSpec.maxExposureCompensation = |
| mCameraCharacteristics.getMaxExposureCompensation(); |
| bottomBarSpec.exposureCompensationStep = |
| mCameraCharacteristics.getExposureCompensationStep(); |
| bottomBarSpec.exposureCompensationSetCallback = |
| new BottomBarUISpec.ExposureCompensationSetCallback() { |
| @Override |
| public void setExposure(int value) { |
| mSettingsManager.set( |
| mAppController.getCameraScope(), Keys.KEY_EXPOSURE, value); |
| } |
| }; |
| |
| return bottomBarSpec; |
| } |
| |
| @Override |
| public boolean isUsingBottomBar() { |
| return true; |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_CAMERA: |
| case KeyEvent.KEYCODE_DPAD_CENTER: |
| if (mUI.isCountingDown()) { |
| cancelCountDown(); |
| } else if (event.getRepeatCount() == 0) { |
| onShutterButtonClick(); |
| } |
| return true; |
| case KeyEvent.KEYCODE_VOLUME_UP: |
| case KeyEvent.KEYCODE_VOLUME_DOWN: |
| // Prevent default. |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_VOLUME_UP: |
| case KeyEvent.KEYCODE_VOLUME_DOWN: |
| onShutterButtonClick(); |
| return true; |
| } |
| return false; |
| } |
| |
| // TODO: Consider refactoring FocusOverlayManager. |
| // Currently AF state transitions are controlled in OneCameraImpl. |
| // PhotoModule uses FocusOverlayManager which uses API1/portability |
| // logic and coordinates. |
| private void startActiveFocusAt(int viewX, int viewY) { |
| if (mCamera == null) { |
| // If we receive this after the camera is closed, do nothing. |
| return; |
| } |
| |
| // TODO: make mFocusController final and remove null check. |
| if (mFocusController == null) { |
| Log.v(TAG, "CaptureModule mFocusController is null!"); |
| return; |
| } |
| mFocusController.showActiveFocusAt(viewX, viewY); |
| |
| // Normalize coordinates to [0,1] per CameraOne API. |
| float points[] = new float[2]; |
| points[0] = (viewX - mPreviewArea.left) / mPreviewArea.width(); |
| points[1] = (viewY - mPreviewArea.top) / mPreviewArea.height(); |
| |
| // Rotate coordinates to portrait orientation per CameraOne API. |
| Matrix rotationMatrix = new Matrix(); |
| rotationMatrix.setRotate(mDisplayRotation, 0.5f, 0.5f); |
| rotationMatrix.mapPoints(points); |
| |
| // Invert X coordinate on front camera since the display is mirrored. |
| if (mCameraCharacteristics.getCameraDirection() == Facing.FRONT) { |
| points[0] = 1 - points[0]; |
| } |
| |
| mCamera.triggerFocusAndMeterAtPoint(points[0], points[1]); |
| |
| // Log touch (screen coordinates). |
| if (mZoomValue == 1f) { |
| TouchCoordinate touchCoordinate = new TouchCoordinate( |
| viewX - mPreviewArea.left, |
| viewY - mPreviewArea.top, |
| mPreviewArea.width(), |
| mPreviewArea.height()); |
| // TODO: Add to logging: duration, rotation. |
| UsageStatistics.instance().tapToFocus(touchCoordinate, null); |
| } |
| } |
| |
| /** |
| * Show AF target in center of preview. |
| */ |
| private void startPassiveFocus() { |
| // TODO: make mFocusController final and remove null check. |
| if (mFocusController == null) { |
| return; |
| } |
| |
| // TODO: Some passive focus scans may trigger on a location |
| // instead of the center of the screen. |
| mFocusController.showPassiveFocusAtCenter(); |
| } |
| |
| /** |
| * Update UI based on AF state changes. |
| */ |
| @Override |
| public void onFocusStatusUpdate(final AutoFocusState state, long frameNumber) { |
| Log.v(TAG, "AF status is state:" + state); |
| |
| switch (state) { |
| case PASSIVE_SCAN: |
| startPassiveFocus(); |
| break; |
| case ACTIVE_SCAN: |
| // Unused, manual scans are triggered via the UI |
| break; |
| case PASSIVE_FOCUSED: |
| case PASSIVE_UNFOCUSED: |
| // Unused |
| break; |
| case ACTIVE_FOCUSED: |
| case ACTIVE_UNFOCUSED: |
| // Unused |
| break; |
| } |
| |
| if (CAPTURE_DEBUG_UI) { |
| measureAutoFocusScans(state, frameNumber); |
| } |
| } |
| |
| private void measureAutoFocusScans(final AutoFocusState state, long frameNumber) { |
| // Log AF scan lengths. |
| boolean passive = false; |
| switch (state) { |
| case PASSIVE_SCAN: |
| case ACTIVE_SCAN: |
| if (mAutoFocusScanStartFrame == -1) { |
| mAutoFocusScanStartFrame = frameNumber; |
| mAutoFocusScanStartTime = SystemClock.uptimeMillis(); |
| } |
| break; |
| case PASSIVE_FOCUSED: |
| case PASSIVE_UNFOCUSED: |
| passive = true; |
| case ACTIVE_FOCUSED: |
| case ACTIVE_UNFOCUSED: |
| if (mAutoFocusScanStartFrame != -1) { |
| long frames = frameNumber - mAutoFocusScanStartFrame; |
| long dt = SystemClock.uptimeMillis() - mAutoFocusScanStartTime; |
| int fps = Math.round(frames * 1000f / dt); |
| String report = String.format("%s scan: fps=%d frames=%d", |
| passive ? "CAF" : "AF", fps, frames); |
| Log.v(TAG, report); |
| mUI.showDebugMessage(String.format("%d / %d", frames, fps)); |
| mAutoFocusScanStartFrame = -1; |
| } |
| break; |
| } |
| } |
| |
| @Override |
| public void onReadyStateChanged(boolean readyForCapture) { |
| if (readyForCapture) { |
| mAppController.getCameraAppUI().enableModeOptions(); |
| } |
| mAppController.setShutterEnabled(readyForCapture); |
| } |
| |
| @Override |
| public String getPeekAccessibilityString() { |
| return mAppController.getAndroidContext() |
| .getResources().getString(R.string.photo_accessibility_peek); |
| } |
| |
| @Override |
| public void onThumbnailResult(byte[] jpegData) { |
| getServices().getRemoteShutterListener().onPictureTaken(jpegData); |
| } |
| |
| @Override |
| public void onPictureTaken(CaptureSession session) { |
| mAppController.getCameraAppUI().enableModeOptions(); |
| } |
| |
| @Override |
| public void onPictureSaved(Uri uri) { |
| mAppController.notifyNewMedia(uri); |
| } |
| |
| @Override |
| public void onTakePictureProgress(float progress) { |
| mUI.setPictureTakingProgress((int) (progress * 100)); |
| } |
| |
| @Override |
| public void onPictureTakingFailed() { |
| mAppController.getFatalErrorHandler().onMediaStorageFailure(); |
| } |
| |
| /** |
| * Updates the preview transform matrix to adapt to the current preview |
| * width, height, and orientation. |
| */ |
| public void updatePreviewTransform() { |
| int width; |
| int height; |
| synchronized (mDimensionLock) { |
| width = mScreenWidth; |
| height = mScreenHeight; |
| } |
| updatePreviewTransform(width, height); |
| } |
| |
| /** |
| * @return Depending on whether we're in sticky-HDR mode or not, return the |
| * proper callback to be used for when the HDR/HDR+ button is |
| * pressed. |
| */ |
| private ButtonManager.ButtonCallback getHdrButtonCallback() { |
| if (mStickyGcamCamera) { |
| return new ButtonManager.ButtonCallback() { |
| @Override |
| public void onStateChanged(int state) { |
| if (mPaused) { |
| return; |
| } |
| if (state == ButtonManager.ON) { |
| throw new IllegalStateException( |
| "Can't leave hdr plus mode if switching to hdr plus mode."); |
| } |
| SettingsManager settingsManager = mAppController.getSettingsManager(); |
| settingsManager.set(mAppController.getModuleScope(), |
| Keys.KEY_REQUEST_RETURN_HDR_PLUS, false); |
| switchToRegularCapture(); |
| } |
| }; |
| } else { |
| return new ButtonManager.ButtonCallback() { |
| @Override |
| public void onStateChanged(int hdrEnabled) { |
| if (mPaused) { |
| return; |
| } |
| |
| // Only reload the camera if we are toggling HDR+. |
| if (GcamHelper.hasGcamCapture(mAppController.getCameraFeatureConfig())) { |
| mHdrPlusEnabled = hdrEnabled == 1; |
| switchCamera(); |
| } else { |
| mHdrSceneEnabled = hdrEnabled == 1; |
| } |
| } |
| }; |
| } |
| } |
| |
| /** |
| * @return Depending on whether we're in sticky-HDR mode or not, this |
| * returns the proper callback to be used for when the camera |
| * (front/back switch) button is pressed. |
| */ |
| private ButtonManager.ButtonCallback getCameraCallback() { |
| if (mStickyGcamCamera) { |
| return new ButtonManager.ButtonCallback() { |
| @Override |
| public void onStateChanged(int state) { |
| if (mPaused) { |
| return; |
| } |
| |
| // At the time this callback is fired, the camera id setting |
| // has changed to the desired camera. |
| SettingsManager settingsManager = mAppController.getSettingsManager(); |
| if (Keys.isCameraBackFacing(settingsManager, |
| mAppController.getModuleScope())) { |
| throw new IllegalStateException( |
| "Hdr plus should never be switching from front facing camera."); |
| } |
| |
| // Switch to photo mode, but request a return to hdr plus on |
| // switching to back camera again. |
| settingsManager.set(mAppController.getModuleScope(), |
| Keys.KEY_REQUEST_RETURN_HDR_PLUS, true); |
| switchToRegularCapture(); |
| } |
| }; |
| } else { |
| return new ButtonManager.ButtonCallback() { |
| @Override |
| public void onStateChanged(int cameraId) { |
| if (mPaused) { |
| return; |
| } |
| |
| ButtonManager buttonManager = mAppController.getButtonManager(); |
| buttonManager.disableCameraButtonAndBlock(); |
| |
| // At the time this callback is fired, the camera id |
| // has be set to the desired camera. |
| mSettingsManager.set(mAppController.getModuleScope(), Keys.KEY_CAMERA_ID, |
| cameraId); |
| |
| Log.d(TAG, "Start to switch camera. cameraId=" + cameraId); |
| mCameraFacing = getFacingFromCameraId(cameraId); |
| mShowErrorAndFinish = !updateCameraCharacteristics(); |
| switchCamera(); |
| } |
| }; |
| } |
| } |
| |
| /** |
| * Switches to PhotoModule to do regular photo captures. |
| * <p> |
| * TODO: Remove this once we use CaptureModule for photo taking. |
| */ |
| private void switchToRegularCapture() { |
| // Turn off HDR+ before switching back to normal photo mode. |
| SettingsManager settingsManager = mAppController.getSettingsManager(); |
| settingsManager.set(SettingsManager.SCOPE_GLOBAL, Keys.KEY_CAMERA_HDR_PLUS, false); |
| |
| // Disable this button to prevent callbacks from this module from firing |
| // while we are transitioning modules. |
| ButtonManager buttonManager = mAppController.getButtonManager(); |
| buttonManager.disableButtonClick(ButtonManager.BUTTON_HDR_PLUS); |
| mAppController.getCameraAppUI().freezeScreenUntilPreviewReady(); |
| mAppController.onModeSelected(mContext.getResources().getInteger( |
| R.integer.camera_mode_photo)); |
| buttonManager.enableButtonClick(ButtonManager.BUTTON_HDR_PLUS); |
| } |
| |
| /** |
| * Called when the preview started. Informs the app controller and queues a |
| * transform update when the next preview frame arrives. |
| */ |
| private void onPreviewStarted() { |
| if (mState == ModuleState.WATCH_FOR_NEXT_FRAME_AFTER_PREVIEW_STARTED) { |
| mState = ModuleState.UPDATE_TRANSFORM_ON_NEXT_SURFACE_TEXTURE_UPDATE; |
| } |
| mAppController.onPreviewStarted(); |
| } |
| |
| /** |
| * Update the preview transform based on the new dimensions. Will not force |
| * an update, if it's not necessary. |
| */ |
| private void updatePreviewTransform(int incomingWidth, int incomingHeight) { |
| updatePreviewTransform(incomingWidth, incomingHeight, false); |
| } |
| |
| /** |
| * Returns whether it is necessary to apply device-specific fix for b/19271661 |
| * on the AutoTransform Path, i.e. USE_AUTOTRANSFORM_UI_LAYOUT == true |
| * |
| * @return whether to apply workaround fix for b/19271661 |
| */ |
| private boolean requiresNexus4SpecificFixFor16By9Previews() { |
| return USE_AUTOTRANSFORM_UI_LAYOUT && ApiHelper.IS_NEXUS_4 |
| && is16by9AspectRatio(mPictureSize); |
| } |
| |
| /*** |
| * Update the preview transform based on the new dimensions. TODO: Make work |
| * with all: aspect ratios/resolutions x screens/cameras. |
| */ |
| private void updatePreviewTransform(int incomingWidth, int incomingHeight, |
| boolean forceUpdate) { |
| Log.d(TAG, "updatePreviewTransform: " + incomingWidth + " x " + incomingHeight); |
| |
| synchronized (mDimensionLock) { |
| int incomingRotation = CameraUtil.getDisplayRotation(mUI.getActivity()); |
| // Check for an actual change: |
| if (mScreenHeight == incomingHeight && mScreenWidth == incomingWidth && |
| incomingRotation == mDisplayRotation && !forceUpdate) { |
| return; |
| } |
| // Update display rotation and dimensions |
| mDisplayRotation = incomingRotation; |
| mScreenWidth = incomingWidth; |
| mScreenHeight = incomingHeight; |
| updatePreviewBufferDimension(); |
| |
| // Assumptions: |
| // - Aspect ratio for the sensor buffers is in landscape |
| // orientation, |
| // - Dimensions of buffers received are rotated to the natural |
| // device orientation. |
| // - The contents of each buffer are rotated by the inverse of |
| // the display rotation. |
| // - Surface scales the buffer to fit the current view bounds. |
| |
| // Get natural orientation and buffer dimensions |
| |
| if(USE_AUTOTRANSFORM_UI_LAYOUT) { |
| // Use PhotoUI-based AutoTransformation Interface |
| if (mPreviewBufferWidth != 0 && mPreviewBufferHeight != 0) { |
| if (requiresNexus4SpecificFixFor16By9Previews()) { |
| // Force preview size to be 16:9, even though surface is 4:3 |
| // Surface content is assumed to be 16:9. |
| mAppController.updatePreviewAspectRatio(16.f / 9.f); |
| } else { |
| mAppController.updatePreviewAspectRatio( |
| mPreviewBufferWidth / (float) mPreviewBufferHeight); |
| } |
| } |
| } else { |
| Matrix transformMatrix = mPreviewTransformCalculator.toTransformMatrix( |
| new Size(mScreenWidth, mScreenHeight), |
| new Size(mPreviewBufferWidth, mPreviewBufferHeight)); |
| mAppController.updatePreviewTransform(transformMatrix); |
| } |
| } |
| } |
| |
| |
| /** |
| * Calculates whether a picture size is 16:9 ratio, regardless of its |
| * orientation. |
| * |
| * @param size the size of the picture to be considered |
| * @return true, if the picture is 16:9; false if it's invalid or size is null |
| */ |
| private boolean is16by9AspectRatio(Size size) { |
| if (size == null || size.getWidth() == 0 || size.getHeight() == 0) { |
| return false; |
| } |
| |
| // Normalize aspect ratio to be greater than 1. |
| final float aspectRatio = (size.getHeight() > size.getWidth()) |
| ? (size.getHeight() / (float) size.getWidth()) |
| : (size.getWidth() / (float) size.getHeight()); |
| |
| return Math.abs(aspectRatio - (16.f / 9.f)) < 0.001f; |
| } |
| |
| /** |
| * Based on the current picture size, selects the best preview dimension and |
| * stores it in {@link #mPreviewBufferWidth} and |
| * {@link #mPreviewBufferHeight}. |
| */ |
| private void updatePreviewBufferDimension() { |
| if (mCamera == null) { |
| return; |
| } |
| |
| Size previewBufferSize = mCamera.pickPreviewSize(mPictureSize, mUI.getActivity()); |
| mPreviewBufferWidth = previewBufferSize.getWidth(); |
| mPreviewBufferHeight = previewBufferSize.getHeight(); |
| |
| // Workaround for N4 TextureView/HAL issues b/19271661 for 16:9 preview |
| // streams. |
| if (requiresNexus4SpecificFixFor16By9Previews()) { |
| // Override the preview selection logic to the largest N4 4:3 |
| // preview size but pass in 16:9 aspect ratio in |
| // UpdatePreviewAspectRatio later. |
| mPreviewBufferWidth = 1280; |
| mPreviewBufferHeight = 960; |
| } |
| updatePreviewBufferSize(); |
| } |
| |
| /** |
| * Open camera and start the preview. |
| */ |
| private void openCameraAndStartPreview() { |
| Profile guard = mProfiler.create("CaptureModule.openCameraAndStartPreview()").start(); |
| try { |
| // TODO Given the current design, we cannot guarantee that one of |
| // CaptureReadyCallback.onSetupFailed or onReadyForCapture will |
| // be called (see below), so it's possible that |
| // mCameraOpenCloseLock.release() is never called under extremely |
| // rare cases. If we leak the lock, this timeout ensures that we at |
| // least crash so we don't deadlock the app. |
| if (!mCameraOpenCloseLock.tryAcquire(CAMERA_OPEN_CLOSE_TIMEOUT_MILLIS, |
| TimeUnit.MILLISECONDS)) { |
| throw new RuntimeException("Time out waiting to acquire camera-open lock."); |
| } |
| } catch (InterruptedException e) { |
| throw new RuntimeException("Interrupted while waiting to acquire camera-open lock.", e); |
| } |
| |
| guard.mark("Acquired mCameraOpenCloseLock"); |
| |
| if (mOneCameraOpener == null) { |
| Log.e(TAG, "no available OneCameraManager, showing error dialog"); |
| mCameraOpenCloseLock.release(); |
| mAppController.getFatalErrorHandler().onGenericCameraAccessFailure(); |
| guard.stop("No OneCameraManager"); |
| return; |
| } |
| if (mCamera != null) { |
| // If the camera is already open, do nothing. |
| Log.d(TAG, "Camera already open, not re-opening."); |
| mCameraOpenCloseLock.release(); |
| guard.stop("Camera is already open"); |
| return; |
| } |
| |
| // Derive objects necessary for camera creation. |
| MainThread mainThread = MainThread.create(); |
| ImageRotationCalculator imageRotationCalculator = ImageRotationCalculatorImpl |
| .from(mAppController.getOrientationManager(), mCameraCharacteristics); |
| |
| // Only enable GCam on the back camera |
| boolean useHdr = mHdrPlusEnabled && mCameraFacing == Facing.BACK; |
| |
| CameraId cameraId = mOneCameraManager.findFirstCameraFacing(mCameraFacing); |
| final String settingScope = SettingsManager.getCameraSettingScope(cameraId.getValue()); |
| |
| OneCameraCaptureSetting captureSetting; |
| // Read the preferred picture size from the setting. |
| try { |
| mPictureSize = mAppController.getResolutionSetting().getPictureSize( |
| cameraId, mCameraFacing); |
| captureSetting = OneCameraCaptureSetting.create(mPictureSize, mSettingsManager, |
| getHardwareSpec(), settingScope, useHdr); |
| } catch (OneCameraAccessException ex) { |
| mAppController.getFatalErrorHandler().onGenericCameraAccessFailure(); |
| return; |
| } |
| |
| mOneCameraOpener.open(cameraId, captureSetting, mCameraHandler, mainThread, |
| imageRotationCalculator, mBurstController, mSoundPlayer, |
| new OpenCallback() { |
| @Override |
| public void onFailure() { |
| Log.e(TAG, "Could not open camera."); |
| // Sometimes the failure happens due to the controller |
| // being in paused state but mCamera is already |
| // initialized. In these cases we just need to close the |
| // camera device without showing the error dialog. |
| // Application will properly reopen the camera on the next |
| // resume operation (b/21025113). |
| boolean isControllerPaused = mAppController.isPaused(); |
| if (mCamera != null) { |
| mCamera.close(); |
| } |
| mCamera = null; |
| mCameraOpenCloseLock.release(); |
| if (!isControllerPaused) { |
| mAppController.getFatalErrorHandler().onCameraOpenFailure(); |
| } |
| } |
| |
| @Override |
| public void onCameraClosed() { |
| mCamera = null; |
| mCameraOpenCloseLock.release(); |
| } |
| |
| @Override |
| public void onCameraOpened(@Nonnull final OneCamera camera) { |
| Log.d(TAG, "onCameraOpened: " + camera); |
| mCamera = camera; |
| |
| // A race condition exists where the camera may be in the process |
| // of opening (blocked), but the activity gets destroyed. If the |
| // preview is initialized or callbacks are invoked on a destroyed |
| // activity, bad things can happen. |
| if (mAppController.isPaused()) { |
| onFailure(); |
| return; |
| } |
| |
| // When camera is opened, the zoom is implicitly reset to 1.0f |
| mZoomValue = 1.0f; |
| |
| updatePreviewBufferDimension(); |
| |
| // If the surface texture is not destroyed, it may have |
| // the last frame lingering. We need to hold off setting |
| // transform until preview is started. |
| updatePreviewBufferSize(); |
| mState = ModuleState.WATCH_FOR_NEXT_FRAME_AFTER_PREVIEW_STARTED; |
| Log.d(TAG, "starting preview ..."); |
| |
| // TODO: make mFocusController final and remove null |
| // check. |
| if (mFocusController != null) { |
| camera.setFocusDistanceListener(mFocusController); |
| } |
| |
| mMainThread.execute(new Runnable() { |
| @Override |
| public void run() { |
| mAppController.getCameraAppUI().onChangeCamera(); |
| mAppController.getButtonManager().enableCameraButton(); |
| } |
| }); |
| |
| // TODO: Consider rolling these two calls into one. |
| camera.startPreview(new Surface(getPreviewSurfaceTexture()), |
| new CaptureReadyCallback() { |
| @Override |
| public void onSetupFailed() { |
| // We must release this lock here, |
| // before posting to the main handler |
| // since we may be blocked in pause(), |
| // getting ready to close the camera. |
| mCameraOpenCloseLock.release(); |
| Log.e(TAG, "Could not set up preview."); |
| mMainThread.execute(new Runnable() { |
| @Override |
| public void run() { |
| if (mCamera == null) { |
| Log.d(TAG, "Camera closed, aborting."); |
| return; |
| } |
| mCamera.close(); |
| mCamera = null; |
| // TODO: Show an error message |
| // and exit. |
| } |
| }); |
| } |
| |
| @Override |
| public void onReadyForCapture() { |
| // We must release this lock here, |
| // before posting to the main handler |
| // since we may be blocked in pause(), |
| // getting ready to close the camera. |
| mCameraOpenCloseLock.release(); |
| mMainThread.execute(new Runnable() { |
| @Override |
| public void run() { |
| Log.d(TAG, "Ready for capture."); |
| if (mCamera == null) { |
| Log.d(TAG, "Camera closed, aborting."); |
| return; |
| } |
| onPreviewStarted(); |
| // May be overridden by |
| // subsequent call to |
| // onReadyStateChanged(). |
| onReadyStateChanged(true); |
| mCamera.setReadyStateChangedListener( |
| CaptureModule.this); |
| // Enable zooming after preview |
| // has started. |
| mUI.initializeZoom(mCamera.getMaxZoom()); |
| mCamera.setFocusStateListener(CaptureModule.this); |
| } |
| }); |
| } |
| }); |
| } |
| }, mAppController.getFatalErrorHandler()); |
| guard.stop("mOneCameraOpener.open()"); |
| } |
| |
| private void closeCamera() { |
| Profile profile = mProfiler.create("CaptureModule.closeCamera()").start(); |
| try { |
| mCameraOpenCloseLock.acquire(); |
| } catch (InterruptedException e) { |
| throw new RuntimeException("Interrupted while waiting to acquire camera-open lock.", e); |
| } |
| profile.mark("mCameraOpenCloseLock.acquire()"); |
| try { |
| if (mCamera != null) { |
| mCamera.close(); |
| profile.mark("mCamera.close()"); |
| mCamera.setFocusStateListener(null); |
| mCamera = null; |
| } |
| } finally { |
| mCameraOpenCloseLock.release(); |
| } |
| profile.stop(); |
| } |
| |
| /** |
| * Re-initialize the camera if e.g. the HDR mode or facing property changed. |
| */ |
| private void switchCamera() { |
| if (mShowErrorAndFinish) { |
| return; |
| } |
| if (mPaused) { |
| return; |
| } |
| cancelCountDown(); |
| mAppController.freezeScreenUntilPreviewReady(); |
| initSurfaceTextureConsumer(); |
| } |
| |
| /** |
| * Returns which way around the camera is facing, based on it's ID. |
| */ |
| private Facing getFacingFromCameraId(int cameraId) { |
| return mAppController.getCameraProvider().getCharacteristics(cameraId) |
| .isFacingFront() ? Facing.FRONT : Facing.BACK; |
| } |
| |
| private void resetTextureBufferSize() { |
| // According to the documentation for |
| // SurfaceTexture.setDefaultBufferSize, |
| // photo and video based image producers (presumably only Camera 1 api), |
| // override this buffer size. Any module that uses egl to render to a |
| // SurfaceTexture must have these buffer sizes reset manually. Otherwise |
| // the SurfaceTexture cannot be transformed by matrix set on the |
| // TextureView. |
| updatePreviewBufferSize(); |
| } |
| } |