PanoramaModule.java revision 224cf5df606a3017b2899e22b1b4ed82d97c525c
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.camera;
18
19import android.annotation.TargetApi;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.res.Configuration;
24import android.content.res.Resources;
25import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.graphics.Canvas;
28import android.graphics.ImageFormat;
29import android.graphics.PixelFormat;
30import android.graphics.Rect;
31import android.graphics.SurfaceTexture;
32import android.graphics.YuvImage;
33import android.graphics.drawable.BitmapDrawable;
34import android.graphics.drawable.Drawable;
35import android.hardware.Camera.Parameters;
36import android.hardware.Camera.Size;
37import android.media.ExifInterface;
38import android.net.Uri;
39import android.os.AsyncTask;
40import android.os.Handler;
41import android.os.Message;
42import android.os.PowerManager;
43import android.util.Log;
44import android.view.KeyEvent;
45import android.view.LayoutInflater;
46import android.view.MotionEvent;
47import android.view.OrientationEventListener;
48import android.view.View;
49import android.view.View.OnClickListener;
50import android.view.ViewGroup;
51import android.view.WindowManager;
52import android.widget.ImageView;
53import android.widget.LinearLayout;
54import android.widget.TextView;
55
56import com.android.camera.CameraManager.CameraProxy;
57import com.android.camera.ui.LayoutChangeNotifier;
58import com.android.camera.ui.LayoutNotifyView;
59import com.android.camera.ui.PopupManager;
60import com.android.camera.ui.Rotatable;
61import com.android.camera.ui.RotateLayout;
62import com.android.gallery3d.common.ApiHelper;
63import com.android.gallery3d.ui.GLRootView;
64
65import java.io.ByteArrayOutputStream;
66import java.io.File;
67import java.io.IOException;
68import java.text.DateFormat;
69import java.text.SimpleDateFormat;
70import java.util.List;
71import java.util.TimeZone;
72
73/**
74 * Activity to handle panorama capturing.
75 */
76@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture
77public class PanoramaModule implements CameraModule,
78        SurfaceTexture.OnFrameAvailableListener,
79        ShutterButton.OnShutterButtonListener,
80        LayoutChangeNotifier.Listener {
81
82    public static final int DEFAULT_SWEEP_ANGLE = 160;
83    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
84    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
85
86    private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1;
87    private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 2;
88    private static final int MSG_RESET_TO_PREVIEW = 3;
89    private static final int MSG_CLEAR_SCREEN_DELAY = 4;
90
91    private static final int SCREEN_DELAY = 2 * 60 * 1000;
92
93    private static final String TAG = "CAM PanoModule";
94    private static final int PREVIEW_STOPPED = 0;
95    private static final int PREVIEW_ACTIVE = 1;
96    private static final int CAPTURE_STATE_VIEWFINDER = 0;
97    private static final int CAPTURE_STATE_MOSAIC = 1;
98
99    private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd";
100    private static final String GPS_TIME_FORMAT_STR = "kk/1,mm/1,ss/1";
101    private static final String DATETIME_FORMAT_STR = "yyyy:MM:dd kk:mm:ss";
102
103    // The unit of speed is degrees per frame.
104    private static final float PANNING_SPEED_THRESHOLD = 2.5f;
105
106    private ContentResolver mContentResolver;
107
108    private GLRootView mGLRootView;
109    private ViewGroup mPanoLayout;
110    private LinearLayout mCaptureLayout;
111    private View mReviewLayout;
112    private ImageView mReview;
113    private RotateLayout mCaptureIndicator;
114    private PanoProgressBar mPanoProgressBar;
115    private PanoProgressBar mSavingProgressBar;
116    private LayoutNotifyView mPreviewArea;
117    private View mLeftIndicator;
118    private View mRightIndicator;
119    private MosaicPreviewRenderer mMosaicPreviewRenderer;
120    private TextView mTooFastPrompt;
121    private ShutterButton mShutterButton;
122    private Object mWaitObject = new Object();
123
124    private DateFormat mGPSDateStampFormat;
125    private DateFormat mGPSTimeStampFormat;
126    private DateFormat mDateTimeStampFormat;
127
128    private String mPreparePreviewString;
129    private String mDialogTitle;
130    private String mDialogOkString;
131    private String mDialogPanoramaFailedString;
132    private String mDialogWaitingPreviousString;
133
134    private int mIndicatorColor;
135    private int mIndicatorColorFast;
136
137    private boolean mUsingFrontCamera;
138    private int mPreviewWidth;
139    private int mPreviewHeight;
140    private int mCameraState;
141    private int mCaptureState;
142    private PowerManager.WakeLock mPartialWakeLock;
143    private MosaicFrameProcessor mMosaicFrameProcessor;
144    private boolean mMosaicFrameProcessorInitialized;
145    private AsyncTask <Void, Void, Void> mWaitProcessorTask;
146    private long mTimeTaken;
147    private Handler mMainHandler;
148    private SurfaceTexture mCameraTexture;
149    private boolean mThreadRunning;
150    private boolean mCancelComputation;
151    private float mHorizontalViewAngle;
152    private float mVerticalViewAngle;
153
154    // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of
155    // getting a better image quality by the former.
156    private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY;
157
158    private PanoOrientationEventListener mOrientationEventListener;
159    // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise
160    // respectively.
161    private int mDeviceOrientation;
162    private int mDeviceOrientationAtCapture;
163    private int mCameraOrientation;
164    private int mOrientationCompensation;
165
166    private RotateDialogController mRotateDialog;
167
168    private SoundClips.Player mSoundPlayer;
169
170    private Runnable mOnFrameAvailableRunnable;
171
172    private CameraActivity mActivity;
173    private View mRootView;
174    private CameraProxy mCameraDevice;
175    private boolean mPaused;
176
177    private class MosaicJpeg {
178        public MosaicJpeg(byte[] data, int width, int height) {
179            this.data = data;
180            this.width = width;
181            this.height = height;
182            this.isValid = true;
183        }
184
185        public MosaicJpeg() {
186            this.data = null;
187            this.width = 0;
188            this.height = 0;
189            this.isValid = false;
190        }
191
192        public final byte[] data;
193        public final int width;
194        public final int height;
195        public final boolean isValid;
196    }
197
198    private class PanoOrientationEventListener extends OrientationEventListener {
199        public PanoOrientationEventListener(Context context) {
200            super(context);
201        }
202
203        @Override
204        public void onOrientationChanged(int orientation) {
205            // We keep the last known orientation. So if the user first orient
206            // the camera then point the camera to floor or sky, we still have
207            // the correct orientation.
208            if (orientation == ORIENTATION_UNKNOWN) return;
209            mDeviceOrientation = Util.roundOrientation(orientation, mDeviceOrientation);
210            // When the screen is unlocked, display rotation may change. Always
211            // calculate the up-to-date orientationCompensation.
212            int orientationCompensation = mDeviceOrientation
213                    + Util.getDisplayRotation(mActivity) % 360;
214            if (mOrientationCompensation != orientationCompensation) {
215                mOrientationCompensation = orientationCompensation;
216                mActivity.getGLRoot().requestLayoutContentPane();
217            }
218        }
219    }
220
221    @Override
222    public void init(CameraActivity activity, View parent, boolean reuseScreenNail) {
223        mActivity = activity;
224        mRootView = (ViewGroup) parent;
225
226        createContentView();
227
228        mContentResolver = mActivity.getContentResolver();
229        if (reuseScreenNail) {
230            mActivity.reuseCameraScreenNail(true);
231        } else {
232            mActivity.createCameraScreenNail(true);
233        }
234
235        // This runs in UI thread.
236        mOnFrameAvailableRunnable = new Runnable() {
237            @Override
238            public void run() {
239                // Frames might still be available after the activity is paused.
240                // If we call onFrameAvailable after pausing, the GL thread will crash.
241                if (mPaused) return;
242
243                if (mGLRootView.getVisibility() != View.VISIBLE) {
244                    mMosaicPreviewRenderer.showPreviewFrameSync();
245                    mGLRootView.setVisibility(View.VISIBLE);
246                } else {
247                    if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
248                        mMosaicPreviewRenderer.showPreviewFrame();
249                    } else {
250                        mMosaicPreviewRenderer.alignFrameSync();
251                        mMosaicFrameProcessor.processFrame();
252                    }
253                }
254            }
255        };
256
257        mGPSDateStampFormat = new SimpleDateFormat(GPS_DATE_FORMAT_STR);
258        mGPSTimeStampFormat = new SimpleDateFormat(GPS_TIME_FORMAT_STR);
259        mDateTimeStampFormat = new SimpleDateFormat(DATETIME_FORMAT_STR);
260        TimeZone tzUTC = TimeZone.getTimeZone("UTC");
261        mGPSDateStampFormat.setTimeZone(tzUTC);
262        mGPSTimeStampFormat.setTimeZone(tzUTC);
263
264        PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE);
265        mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Panorama");
266
267        mOrientationEventListener = new PanoOrientationEventListener(mActivity);
268
269        mMosaicFrameProcessor = MosaicFrameProcessor.getInstance();
270
271        Resources appRes = mActivity.getResources();
272        mPreparePreviewString = appRes.getString(R.string.pano_dialog_prepare_preview);
273        mDialogTitle = appRes.getString(R.string.pano_dialog_title);
274        mDialogOkString = appRes.getString(R.string.dialog_ok);
275        mDialogPanoramaFailedString = appRes.getString(R.string.pano_dialog_panorama_failed);
276        mDialogWaitingPreviousString = appRes.getString(R.string.pano_dialog_waiting_previous);
277
278        mGLRootView = (GLRootView) mActivity.getGLRoot();
279
280        mMainHandler = new Handler() {
281            @Override
282            public void handleMessage(Message msg) {
283                switch (msg.what) {
284                    case MSG_LOW_RES_FINAL_MOSAIC_READY:
285                        onBackgroundThreadFinished();
286                        showFinalMosaic((Bitmap) msg.obj);
287                        saveHighResMosaic();
288                        break;
289                    case MSG_GENERATE_FINAL_MOSAIC_ERROR:
290                        onBackgroundThreadFinished();
291                        if (mPaused) {
292                            resetToPreview();
293                        } else {
294                            mRotateDialog.showAlertDialog(
295                                    mDialogTitle, mDialogPanoramaFailedString,
296                                    mDialogOkString, new Runnable() {
297                                        @Override
298                                        public void run() {
299                                            resetToPreview();
300                                        }},
301                                    null, null);
302                        }
303                        clearMosaicFrameProcessorIfNeeded();
304                        break;
305                    case MSG_RESET_TO_PREVIEW:
306                        onBackgroundThreadFinished();
307                        resetToPreview();
308                        clearMosaicFrameProcessorIfNeeded();
309                        break;
310                    case MSG_CLEAR_SCREEN_DELAY:
311                        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.
312                                FLAG_KEEP_SCREEN_ON);
313                        break;
314                }
315            }
316        };
317    }
318
319    @Override
320    public boolean dispatchTouchEvent(MotionEvent m) {
321        return mActivity.superDispatchTouchEvent(m);
322    }
323
324    private void setupCamera() throws CameraHardwareException, CameraDisabledException {
325        openCamera();
326        Parameters parameters = mCameraDevice.getParameters();
327        setupCaptureParams(parameters);
328        configureCamera(parameters);
329    }
330
331    private void releaseCamera() {
332        if (mCameraDevice != null) {
333            mCameraDevice.setPreviewCallbackWithBuffer(null);
334            CameraHolder.instance().release();
335            mCameraDevice = null;
336            mCameraState = PREVIEW_STOPPED;
337        }
338    }
339
340    private void openCamera() throws CameraHardwareException, CameraDisabledException {
341        int cameraId = CameraHolder.instance().getBackCameraId();
342        // If there is no back camera, use the first camera. Camera id starts
343        // from 0. Currently if a camera is not back facing, it is front facing.
344        // This is also forward compatible if we have a new facing other than
345        // back or front in the future.
346        if (cameraId == -1) cameraId = 0;
347        mCameraDevice = Util.openCamera(mActivity, cameraId);
348        mCameraOrientation = Util.getCameraOrientation(cameraId);
349        if (cameraId == CameraHolder.instance().getFrontCameraId()) mUsingFrontCamera = true;
350    }
351
352    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
353            boolean needSmaller) {
354        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
355        boolean hasFound = false;
356        for (Size size : supportedSizes) {
357            int h = size.height;
358            int w = size.width;
359            // we only want 4:3 format.
360            int d = DEFAULT_CAPTURE_PIXELS - h * w;
361            if (needSmaller && d < 0) { // no bigger preview than 960x720.
362                continue;
363            }
364            if (need4To3 && (h * 4 != w * 3)) {
365                continue;
366            }
367            d = Math.abs(d);
368            if (d < pixelsDiff) {
369                mPreviewWidth = w;
370                mPreviewHeight = h;
371                pixelsDiff = d;
372                hasFound = true;
373            }
374        }
375        return hasFound;
376    }
377
378    private void setupCaptureParams(Parameters parameters) {
379        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
380        if (!findBestPreviewSize(supportedSizes, true, true)) {
381            Log.w(TAG, "No 4:3 ratio preview size supported.");
382            if (!findBestPreviewSize(supportedSizes, false, true)) {
383                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
384                findBestPreviewSize(supportedSizes, false, false);
385            }
386        }
387        Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
388        parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
389
390        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
391        int last = frameRates.size() - 1;
392        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
393        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
394        parameters.setPreviewFpsRange(minFps, maxFps);
395        Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
396
397        List<String> supportedFocusModes = parameters.getSupportedFocusModes();
398        if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) {
399            parameters.setFocusMode(mTargetFocusMode);
400        } else {
401            // Use the default focus mode and log a message
402            Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode +
403                  " becuase the mode is not supported.");
404        }
405
406        parameters.set(Util.RECORDING_HINT, Util.FALSE);
407
408        mHorizontalViewAngle = parameters.getHorizontalViewAngle();
409        mVerticalViewAngle =  parameters.getVerticalViewAngle();
410    }
411
412    public int getPreviewBufSize() {
413        PixelFormat pixelInfo = new PixelFormat();
414        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
415        // TODO: remove this extra 32 byte after the driver bug is fixed.
416        return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
417    }
418
419    private void configureCamera(Parameters parameters) {
420        mCameraDevice.setParameters(parameters);
421    }
422
423    private void configMosaicPreview(int w, int h) {
424        stopCameraPreview();
425        CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
426        screenNail.setSize(w, h);
427        if (screenNail.getSurfaceTexture() == null) {
428            screenNail.acquireSurfaceTexture();
429        } else {
430            screenNail.releaseSurfaceTexture();
431            screenNail.acquireSurfaceTexture();
432            mActivity.notifyScreenNailChanged();
433        }
434        boolean isLandscape = (mActivity.getResources().getConfiguration().orientation
435                == Configuration.ORIENTATION_LANDSCAPE);
436        if (mMosaicPreviewRenderer != null) mMosaicPreviewRenderer.release();
437        mMosaicPreviewRenderer = new MosaicPreviewRenderer(
438                screenNail.getSurfaceTexture(), w, h, isLandscape);
439
440        mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture();
441        if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) {
442            resetToPreview();
443        }
444    }
445
446    // Receives the layout change event from the preview area. So we can set
447    // the camera preview screennail to the same size and initialize the mosaic
448    // preview renderer.
449    @Override
450    public void onLayoutChange(View v, int l, int t, int r, int b) {
451        Log.i(TAG, "layout change: "+(r - l) + "/" +(b - t));
452        mActivity.onLayoutChange(v, l, t, r, b);
453        configMosaicPreview(r - l, b - t);
454    }
455
456    @Override
457    public void onFrameAvailable(SurfaceTexture surface) {
458        /* This function may be called by some random thread,
459         * so let's be safe and jump back to ui thread.
460         * No OpenGL calls can be done here. */
461        mActivity.runOnUiThread(mOnFrameAvailableRunnable);
462    }
463
464    private void hideDirectionIndicators() {
465        mLeftIndicator.setVisibility(View.GONE);
466        mRightIndicator.setVisibility(View.GONE);
467    }
468
469    private void showDirectionIndicators(int direction) {
470        switch (direction) {
471            case PanoProgressBar.DIRECTION_NONE:
472                mLeftIndicator.setVisibility(View.VISIBLE);
473                mRightIndicator.setVisibility(View.VISIBLE);
474                break;
475            case PanoProgressBar.DIRECTION_LEFT:
476                mLeftIndicator.setVisibility(View.VISIBLE);
477                mRightIndicator.setVisibility(View.GONE);
478                break;
479            case PanoProgressBar.DIRECTION_RIGHT:
480                mLeftIndicator.setVisibility(View.GONE);
481                mRightIndicator.setVisibility(View.VISIBLE);
482                break;
483        }
484    }
485
486    public void startCapture() {
487        // Reset values so we can do this again.
488        mCancelComputation = false;
489        mTimeTaken = System.currentTimeMillis();
490        mActivity.setSwipingEnabled(false);
491        mActivity.hideSwitcher();
492        mShutterButton.setImageResource(R.drawable.btn_shutter_recording);
493        mCaptureState = CAPTURE_STATE_MOSAIC;
494        mCaptureIndicator.setVisibility(View.VISIBLE);
495        showDirectionIndicators(PanoProgressBar.DIRECTION_NONE);
496
497        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
498            @Override
499            public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
500                    float progressX, float progressY) {
501                float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle;
502                float accumulatedVerticalAngle = progressY * mVerticalViewAngle;
503                if (isFinished
504                        || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE)
505                        || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) {
506                    stopCapture(false);
507                } else {
508                    float panningRateXInDegree = panningRateX * mHorizontalViewAngle;
509                    float panningRateYInDegree = panningRateY * mVerticalViewAngle;
510                    updateProgress(panningRateXInDegree, panningRateYInDegree,
511                            accumulatedHorizontalAngle, accumulatedVerticalAngle);
512                }
513            }
514        });
515
516        mPanoProgressBar.reset();
517        // TODO: calculate the indicator width according to different devices to reflect the actual
518        // angle of view of the camera device.
519        mPanoProgressBar.setIndicatorWidth(20);
520        mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE);
521        mPanoProgressBar.setVisibility(View.VISIBLE);
522        mDeviceOrientationAtCapture = mDeviceOrientation;
523        keepScreenOn();
524        mActivity.getOrientationManager().lockOrientation();
525    }
526
527    private void stopCapture(boolean aborted) {
528        mCaptureState = CAPTURE_STATE_VIEWFINDER;
529        mCaptureIndicator.setVisibility(View.GONE);
530        hideTooFastIndication();
531        hideDirectionIndicators();
532
533        mMosaicFrameProcessor.setProgressListener(null);
534        stopCameraPreview();
535
536        mCameraTexture.setOnFrameAvailableListener(null);
537
538        if (!aborted && !mThreadRunning) {
539            mRotateDialog.showWaitingDialog(mPreparePreviewString);
540            // Hide shutter button, shutter icon, etc when waiting for
541            // panorama to stitch
542            mActivity.hideUI();
543            runBackgroundThread(new Thread() {
544                @Override
545                public void run() {
546                    MosaicJpeg jpeg = generateFinalMosaic(false);
547
548                    if (jpeg != null && jpeg.isValid) {
549                        Bitmap bitmap = null;
550                        bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
551                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
552                                MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
553                    } else {
554                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
555                                MSG_RESET_TO_PREVIEW));
556                    }
557                }
558            });
559        }
560        keepScreenOnAwhile();
561    }
562
563    private void showTooFastIndication() {
564        mTooFastPrompt.setVisibility(View.VISIBLE);
565        // The PreviewArea also contains the border for "too fast" indication.
566        mPreviewArea.setVisibility(View.VISIBLE);
567        mPanoProgressBar.setIndicatorColor(mIndicatorColorFast);
568        mLeftIndicator.setEnabled(true);
569        mRightIndicator.setEnabled(true);
570    }
571
572    private void hideTooFastIndication() {
573        mTooFastPrompt.setVisibility(View.GONE);
574        // We set "INVISIBLE" instead of "GONE" here because we need mPreviewArea to have layout
575        // information so we can know the size and position for mCameraScreenNail.
576        mPreviewArea.setVisibility(View.INVISIBLE);
577        mPanoProgressBar.setIndicatorColor(mIndicatorColor);
578        mLeftIndicator.setEnabled(false);
579        mRightIndicator.setEnabled(false);
580    }
581
582    private void updateProgress(float panningRateXInDegree, float panningRateYInDegree,
583            float progressHorizontalAngle, float progressVerticalAngle) {
584        mGLRootView.requestRender();
585
586        if ((Math.abs(panningRateXInDegree) > PANNING_SPEED_THRESHOLD)
587            || (Math.abs(panningRateYInDegree) > PANNING_SPEED_THRESHOLD)) {
588            showTooFastIndication();
589        } else {
590            hideTooFastIndication();
591        }
592        int angleInMajorDirection =
593                (Math.abs(progressHorizontalAngle) > Math.abs(progressVerticalAngle))
594                ? (int) progressHorizontalAngle
595                : (int) progressVerticalAngle;
596        mPanoProgressBar.setProgress((angleInMajorDirection));
597    }
598
599    private void setViews(Resources appRes) {
600        mCaptureState = CAPTURE_STATE_VIEWFINDER;
601        mPanoProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_pan_progress_bar);
602        mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
603        mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done));
604        mPanoProgressBar.setIndicatorColor(mIndicatorColor);
605        mPanoProgressBar.setOnDirectionChangeListener(
606                new PanoProgressBar.OnDirectionChangeListener () {
607                    @Override
608                    public void onDirectionChange(int direction) {
609                        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
610                            showDirectionIndicators(direction);
611                        }
612                    }
613                });
614
615        mLeftIndicator = mRootView.findViewById(R.id.pano_pan_left_indicator);
616        mRightIndicator = mRootView.findViewById(R.id.pano_pan_right_indicator);
617        mLeftIndicator.setEnabled(false);
618        mRightIndicator.setEnabled(false);
619        mTooFastPrompt = (TextView) mRootView.findViewById(R.id.pano_capture_too_fast_textview);
620        // This mPreviewArea also shows the border for visual "too fast" indication.
621        mPreviewArea = (LayoutNotifyView) mRootView.findViewById(R.id.pano_preview_area);
622        mPreviewArea.setOnLayoutChangeListener(this);
623
624        mSavingProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_saving_progress_bar);
625        mSavingProgressBar.setIndicatorWidth(0);
626        mSavingProgressBar.setMaxProgress(100);
627        mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
628        mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication));
629
630        mCaptureIndicator = (RotateLayout) mRootView.findViewById(R.id.pano_capture_indicator);
631
632        mReviewLayout = mRootView.findViewById(R.id.pano_review_layout);
633        mReview = (ImageView) mRootView.findViewById(R.id.pano_reviewarea);
634        View cancelButton = mRootView.findViewById(R.id.pano_review_cancel_button);
635        cancelButton.setOnClickListener(new OnClickListener() {
636            @Override
637            public void onClick(View arg0) {
638                if (mPaused || mCameraTexture == null) return;
639                cancelHighResComputation();
640            }
641        });
642
643        mShutterButton = mActivity.getShutterButton();
644        mShutterButton.setImageResource(R.drawable.btn_new_shutter);
645        mShutterButton.setOnShutterButtonListener(this);
646
647        if (mActivity.getResources().getConfiguration().orientation
648                == Configuration.ORIENTATION_PORTRAIT) {
649            final int[] ids = {
650                    R.id.pano_pan_progress_bar_layout,
651                    R.id.pano_saving_progress_bar_layout,
652                    R.id.pano_rotate_reviewarea};
653            for (int i = 0; i < ids.length; i++) {
654                Rotatable view = (Rotatable) mRootView.findViewById(ids[i]);
655                view.setOrientation(270, false);
656            }
657        }
658    }
659
660    private void createContentView() {
661        mActivity.getLayoutInflater().inflate(R.layout.panorama_module, (ViewGroup) mRootView);
662        Resources appRes = mActivity.getResources();
663        mCaptureLayout = (LinearLayout) mRootView.findViewById(R.id.camera_app_root);
664        mIndicatorColor = appRes.getColor(R.color.pano_progress_indication);
665        mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast);
666        mPanoLayout = (ViewGroup) mRootView.findViewById(R.id.pano_layout);
667        mRotateDialog = new RotateDialogController(mActivity, R.layout.rotate_dialog);
668        setViews(appRes);
669    }
670
671    @Override
672    public void onShutterButtonClick() {
673        // If mCameraTexture == null then GL setup is not finished yet.
674        // No buttons can be pressed.
675        if (mPaused || mThreadRunning || mCameraTexture == null) return;
676        // Since this button will stay on the screen when capturing, we need to check the state
677        // right now.
678        switch (mCaptureState) {
679            case CAPTURE_STATE_VIEWFINDER:
680                if(mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) return;
681                mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING);
682                startCapture();
683                break;
684            case CAPTURE_STATE_MOSAIC:
685                mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING);
686                stopCapture(false);
687        }
688    }
689
690    @Override
691    public void onShutterButtonFocus(boolean pressed) {
692    }
693
694    public void reportProgress() {
695        mSavingProgressBar.reset();
696        mSavingProgressBar.setRightIncreasing(true);
697        Thread t = new Thread() {
698            @Override
699            public void run() {
700                while (mThreadRunning) {
701                    final int progress = mMosaicFrameProcessor.reportProgress(
702                            true, mCancelComputation);
703
704                    try {
705                        synchronized (mWaitObject) {
706                            mWaitObject.wait(50);
707                        }
708                    } catch (InterruptedException e) {
709                        throw new RuntimeException("Panorama reportProgress failed", e);
710                    }
711                    // Update the progress bar
712                    mActivity.runOnUiThread(new Runnable() {
713                        @Override
714                        public void run() {
715                            mSavingProgressBar.setProgress(progress);
716                        }
717                    });
718                }
719            }
720        };
721        t.start();
722    }
723
724    private int getCaptureOrientation() {
725        // The panorama image returned from the library is oriented based on the
726        // natural orientation of a camera. We need to set an orientation for the image
727        // in its EXIF header, so the image can be displayed correctly.
728        // The orientation is calculated from compensating the
729        // device orientation at capture and the camera orientation respective to
730        // the natural orientation of the device.
731        int orientation;
732        if (mUsingFrontCamera) {
733            // mCameraOrientation is negative with respect to the front facing camera.
734            // See document of android.hardware.Camera.Parameters.setRotation.
735            orientation = (mDeviceOrientationAtCapture - mCameraOrientation + 360) % 360;
736        } else {
737            orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360;
738        }
739        return orientation;
740    }
741
742    public void saveHighResMosaic() {
743        runBackgroundThread(new Thread() {
744            @Override
745            public void run() {
746                mPartialWakeLock.acquire();
747                MosaicJpeg jpeg;
748                try {
749                    jpeg = generateFinalMosaic(true);
750                } finally {
751                    mPartialWakeLock.release();
752                }
753
754                if (jpeg == null) {  // Cancelled by user.
755                    mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
756                } else if (!jpeg.isValid) {  // Error when generating mosaic.
757                    mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
758                } else {
759                    int orientation = getCaptureOrientation();
760                    Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
761                    if (uri != null) {
762                        mActivity.addSecureAlbumItemIfNeeded(false, uri);
763                        Util.broadcastNewPicture(mActivity, uri);
764                    }
765                    mMainHandler.sendMessage(
766                            mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW));
767                }
768            }
769        });
770        reportProgress();
771    }
772
773    private void runBackgroundThread(Thread thread) {
774        mThreadRunning = true;
775        thread.start();
776    }
777
778    private void onBackgroundThreadFinished() {
779        mThreadRunning = false;
780        mRotateDialog.dismissDialog();
781    }
782
783    private void cancelHighResComputation() {
784        mCancelComputation = true;
785        synchronized (mWaitObject) {
786            mWaitObject.notify();
787        }
788    }
789
790    // This function will be called upon the first camera frame is available.
791    private void reset() {
792        mCaptureState = CAPTURE_STATE_VIEWFINDER;
793
794        mActivity.getOrientationManager().unlockOrientation();
795        // We should set mGLRootView visible too. However, since there might be no
796        // frame available yet, setting mGLRootView visible should be done right after
797        // the first camera frame is available and therefore it is done by
798        // mOnFirstFrameAvailableRunnable.
799        mActivity.setSwipingEnabled(true);
800        mShutterButton.setImageResource(R.drawable.btn_new_shutter);
801        mReviewLayout.setVisibility(View.GONE);
802        mPanoProgressBar.setVisibility(View.GONE);
803        // Orientation change will trigger onLayoutChange->configMosaicPreview->
804        // resetToPreview. Do not show the capture UI in film strip.
805        if (mActivity.mShowCameraAppView) {
806            mCaptureLayout.setVisibility(View.VISIBLE);
807            mActivity.showUI();
808        }
809        mMosaicFrameProcessor.reset();
810    }
811
812    private void resetToPreview() {
813        reset();
814        if (!mPaused) startCameraPreview();
815    }
816
817    private static class FlipBitmapDrawable extends BitmapDrawable {
818
819        public FlipBitmapDrawable(Resources res, Bitmap bitmap) {
820            super(res, bitmap);
821        }
822
823        @Override
824        public void draw(Canvas canvas) {
825            Rect bounds = getBounds();
826            int cx = bounds.centerX();
827            int cy = bounds.centerY();
828            canvas.save(Canvas.MATRIX_SAVE_FLAG);
829            canvas.rotate(180, cx, cy);
830            super.draw(canvas);
831            canvas.restore();
832        }
833    }
834
835    private void showFinalMosaic(Bitmap bitmap) {
836        if (bitmap != null) {
837            int orientation = getCaptureOrientation();
838            if (orientation >= 180) {
839                // We need to flip the drawable to compensate
840                mReview.setImageDrawable(new FlipBitmapDrawable(
841                        mActivity.getResources(), bitmap));
842            } else {
843                mReview.setImageBitmap(bitmap);
844            }
845        }
846
847        mGLRootView.setVisibility(View.GONE);
848        mCaptureLayout.setVisibility(View.GONE);
849        mReviewLayout.setVisibility(View.VISIBLE);
850    }
851
852    private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) {
853        if (jpegData != null) {
854            String filename = PanoUtil.createName(
855                    mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken);
856            String filepath = Storage.generateFilepath(filename);
857            Storage.writeFile(filepath, jpegData);
858
859            // Add Exif tags.
860            try {
861                ExifInterface exif = new ExifInterface(filepath);
862                exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP,
863                        mGPSDateStampFormat.format(mTimeTaken));
864                exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP,
865                        mGPSTimeStampFormat.format(mTimeTaken));
866                exif.setAttribute(ExifInterface.TAG_DATETIME,
867                        mDateTimeStampFormat.format(mTimeTaken));
868                exif.setAttribute(ExifInterface.TAG_ORIENTATION,
869                        getExifOrientation(orientation));
870                exif.saveAttributes();
871            } catch (IOException e) {
872                Log.e(TAG, "Cannot set EXIF for " + filepath, e);
873            }
874
875            int jpegLength = (int) (new File(filepath).length());
876            return Storage.addImage(mContentResolver, filename, mTimeTaken,
877                    null, orientation, jpegLength, filepath, width, height);
878        }
879        return null;
880    }
881
882    private static String getExifOrientation(int orientation) {
883        switch (orientation) {
884            case 0:
885                return String.valueOf(ExifInterface.ORIENTATION_NORMAL);
886            case 90:
887                return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90);
888            case 180:
889                return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180);
890            case 270:
891                return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270);
892            default:
893                throw new AssertionError("invalid: " + orientation);
894        }
895    }
896
897    private void clearMosaicFrameProcessorIfNeeded() {
898        if (!mPaused || mThreadRunning) return;
899        // Only clear the processor if it is initialized by this activity
900        // instance. Other activity instances may be using it.
901        if (mMosaicFrameProcessorInitialized) {
902            mMosaicFrameProcessor.clear();
903            mMosaicFrameProcessorInitialized = false;
904        }
905    }
906
907    private void initMosaicFrameProcessorIfNeeded() {
908        if (mPaused || mThreadRunning) return;
909        mMosaicFrameProcessor.initialize(
910                mPreviewWidth, mPreviewHeight, getPreviewBufSize());
911        mMosaicFrameProcessorInitialized = true;
912    }
913
914    @Override
915    public void onPauseBeforeSuper() {
916        mPaused = true;
917    }
918
919    @Override
920    public void onPauseAfterSuper() {
921        mOrientationEventListener.disable();
922        if (mCameraDevice == null) {
923            // Camera open failed. Nothing should be done here.
924            return;
925        }
926        // Stop the capturing first.
927        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
928            stopCapture(true);
929            reset();
930        }
931
932        releaseCamera();
933        mCameraTexture = null;
934
935        // The preview renderer might not have a chance to be initialized before
936        // onPause().
937        if (mMosaicPreviewRenderer != null) {
938            mMosaicPreviewRenderer.release();
939            mMosaicPreviewRenderer = null;
940        }
941
942        clearMosaicFrameProcessorIfNeeded();
943        if (mWaitProcessorTask != null) {
944            mWaitProcessorTask.cancel(true);
945            mWaitProcessorTask = null;
946        }
947        resetScreenOn();
948        if (mSoundPlayer != null) {
949            mSoundPlayer.release();
950            mSoundPlayer = null;
951        }
952        CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
953        if (screenNail.getSurfaceTexture() != null) {
954            screenNail.releaseSurfaceTexture();
955        }
956        System.gc();
957    }
958
959    @Override
960    public void onConfigurationChanged(Configuration newConfig) {
961
962        Drawable lowResReview = null;
963        if (mThreadRunning) lowResReview = mReview.getDrawable();
964
965        // Change layout in response to configuration change
966        mCaptureLayout.setOrientation(
967                newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
968                ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
969        mCaptureLayout.removeAllViews();
970        LayoutInflater inflater = mActivity.getLayoutInflater();
971        inflater.inflate(R.layout.preview_frame_pano, mCaptureLayout);
972
973        mPanoLayout.removeView(mReviewLayout);
974        inflater.inflate(R.layout.pano_review, mPanoLayout);
975
976        setViews(mActivity.getResources());
977        if (mThreadRunning) {
978            mReview.setImageDrawable(lowResReview);
979            mCaptureLayout.setVisibility(View.GONE);
980            mReviewLayout.setVisibility(View.VISIBLE);
981        }
982    }
983
984    @Override
985    public void onOrientationChanged(int orientation) {
986    }
987
988    @Override
989    public void onResumeBeforeSuper() {
990        mPaused = false;
991    }
992
993    @Override
994    public void onResumeAfterSuper() {
995        mOrientationEventListener.enable();
996
997        mCaptureState = CAPTURE_STATE_VIEWFINDER;
998
999        try {
1000            setupCamera();
1001        } catch (CameraHardwareException e) {
1002            Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
1003            return;
1004        } catch (CameraDisabledException e) {
1005            Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
1006            return;
1007        }
1008
1009        // Set up sound playback for shutter button
1010        mSoundPlayer = SoundClips.getPlayer(mActivity);
1011
1012        // Check if another panorama instance is using the mosaic frame processor.
1013        mRotateDialog.dismissDialog();
1014        if (!mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
1015            mGLRootView.setVisibility(View.GONE);
1016            mRotateDialog.showWaitingDialog(mDialogWaitingPreviousString);
1017            // If stitching is still going on, make sure switcher and shutter button
1018            // are not showing
1019            mActivity.hideUI();
1020            mWaitProcessorTask = new WaitProcessorTask().execute();
1021        } else {
1022            if (!mThreadRunning) mGLRootView.setVisibility(View.VISIBLE);
1023            // Camera must be initialized before MosaicFrameProcessor is
1024            // initialized. The preview size has to be decided by camera device.
1025            initMosaicFrameProcessorIfNeeded();
1026            int w = mPreviewArea.getWidth();
1027            int h = mPreviewArea.getHeight();
1028            if (w != 0 && h != 0) {  // The layout has been calculated.
1029                configMosaicPreview(w, h);
1030            }
1031        }
1032        keepScreenOnAwhile();
1033
1034        // Dismiss open menu if exists.
1035        PopupManager.getInstance(mActivity).notifyShowPopup(null);
1036        mRootView.requestLayout();
1037    }
1038
1039    /**
1040     * Generate the final mosaic image.
1041     *
1042     * @param highRes flag to indicate whether we want to get a high-res version.
1043     * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation
1044     *         process is cancelled; and a MosaicJpeg with its isValid flag set to false if there
1045     *         is an error in generating the final mosaic.
1046     */
1047    public MosaicJpeg generateFinalMosaic(boolean highRes) {
1048        int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes);
1049        if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) {
1050            return null;
1051        } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) {
1052            return new MosaicJpeg();
1053        }
1054
1055        byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
1056        if (imageData == null) {
1057            Log.e(TAG, "getFinalMosaicNV21() returned null.");
1058            return new MosaicJpeg();
1059        }
1060
1061        int len = imageData.length - 8;
1062        int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
1063                + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
1064        int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
1065                + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
1066        Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
1067
1068        if (width <= 0 || height <= 0) {
1069            // TODO: pop up an error message indicating that the final result is not generated.
1070            Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
1071                    height);
1072            return new MosaicJpeg();
1073        }
1074
1075        YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
1076        ByteArrayOutputStream out = new ByteArrayOutputStream();
1077        yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
1078        try {
1079            out.close();
1080        } catch (Exception e) {
1081            Log.e(TAG, "Exception in storing final mosaic", e);
1082            return new MosaicJpeg();
1083        }
1084        return new MosaicJpeg(out.toByteArray(), width, height);
1085    }
1086
1087    private void startCameraPreview() {
1088        if (mCameraDevice == null) {
1089            // Camera open failed. Return.
1090            return;
1091        }
1092
1093        // This works around a driver issue. startPreview may fail if
1094        // stopPreview/setPreviewTexture/startPreview are called several times
1095        // in a row. mCameraTexture can be null after pressing home during
1096        // mosaic generation and coming back. Preview will be started later in
1097        // onLayoutChange->configMosaicPreview. This also reduces the latency.
1098        if (mCameraTexture == null) return;
1099
1100        // If we're previewing already, stop the preview first (this will blank
1101        // the screen).
1102        if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
1103
1104        // Set the display orientation to 0, so that the underlying mosaic library
1105        // can always get undistorted mPreviewWidth x mPreviewHeight image data
1106        // from SurfaceTexture.
1107        mCameraDevice.setDisplayOrientation(0);
1108
1109        if (mCameraTexture != null) mCameraTexture.setOnFrameAvailableListener(this);
1110        mCameraDevice.setPreviewTextureAsync(mCameraTexture);
1111
1112        mCameraDevice.startPreviewAsync();
1113        mCameraState = PREVIEW_ACTIVE;
1114    }
1115
1116    private void stopCameraPreview() {
1117        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
1118            Log.v(TAG, "stopPreview");
1119            mCameraDevice.stopPreview();
1120        }
1121        mCameraState = PREVIEW_STOPPED;
1122    }
1123
1124    @Override
1125    public void onUserInteraction() {
1126        if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile();
1127    }
1128
1129    @Override
1130    public boolean onBackPressed() {
1131        // If panorama is generating low res or high res mosaic, ignore back
1132        // key. So the activity will not be destroyed.
1133        if (mThreadRunning) return true;
1134        return false;
1135    }
1136
1137    private void resetScreenOn() {
1138        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1139        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1140    }
1141
1142    private void keepScreenOnAwhile() {
1143        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1144        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1145        mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1146    }
1147
1148    private void keepScreenOn() {
1149        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1150        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1151    }
1152
1153    private class WaitProcessorTask extends AsyncTask<Void, Void, Void> {
1154        @Override
1155        protected Void doInBackground(Void... params) {
1156            synchronized (mMosaicFrameProcessor) {
1157                while (!isCancelled() && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
1158                    try {
1159                        mMosaicFrameProcessor.wait();
1160                    } catch (Exception e) {
1161                        // ignore
1162                    }
1163                }
1164            }
1165            return null;
1166        }
1167
1168        @Override
1169        protected void onPostExecute(Void result) {
1170            mWaitProcessorTask = null;
1171            mRotateDialog.dismissDialog();
1172            mGLRootView.setVisibility(View.VISIBLE);
1173            initMosaicFrameProcessorIfNeeded();
1174            int w = mPreviewArea.getWidth();
1175            int h = mPreviewArea.getHeight();
1176            if (w != 0 && h != 0) {  // The layout has been calculated.
1177                configMosaicPreview(w, h);
1178            }
1179            resetToPreview();
1180        }
1181    }
1182
1183    @Override
1184    public void onFullScreenChanged(boolean full) {
1185    }
1186
1187
1188    @Override
1189    public void onStop() {
1190    }
1191
1192    @Override
1193    public void installIntentFilter() {
1194    }
1195
1196    @Override
1197    public void onActivityResult(int requestCode, int resultCode, Intent data) {
1198    }
1199
1200
1201    @Override
1202    public boolean onKeyDown(int keyCode, KeyEvent event) {
1203        return false;
1204    }
1205
1206    @Override
1207    public boolean onKeyUp(int keyCode, KeyEvent event) {
1208        return false;
1209    }
1210
1211    @Override
1212    public void onSingleTapUp(View view, int x, int y) {
1213    }
1214
1215    @Override
1216    public void onPreviewTextureCopied() {
1217    }
1218
1219    @Override
1220    public void onCaptureTextureCopied() {
1221    }
1222
1223    @Override
1224    public boolean updateStorageHintOnResume() {
1225        return false;
1226    }
1227
1228    @Override
1229    public void updateCameraAppView() {
1230    }
1231
1232    @Override
1233    public boolean collapseCameraControls() {
1234        return false;
1235    }
1236
1237    @Override
1238    public boolean needsSwitcher() {
1239        return true;
1240    }
1241
1242    @Override
1243    public void onShowSwitcherPopup() {
1244    }
1245}
1246