PanoramaActivity.java revision 5fde6d5259fccaed4bc517697a4930d3b69ae860
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.panorama;
18
19import com.android.camera.CameraDisabledException;
20import com.android.camera.CameraHardwareException;
21import com.android.camera.CameraHolder;
22import com.android.camera.MenuHelper;
23import com.android.camera.ModePicker;
24import com.android.camera.R;
25import com.android.camera.Storage;
26import com.android.camera.Util;
27
28import android.app.Activity;
29import android.app.ProgressDialog;
30import android.content.Context;
31import android.graphics.Bitmap;
32import android.graphics.BitmapFactory;
33import android.graphics.ImageFormat;
34import android.graphics.PixelFormat;
35import android.graphics.Rect;
36import android.graphics.SurfaceTexture;
37import android.graphics.YuvImage;
38import android.hardware.Camera;
39import android.hardware.Camera.Parameters;
40import android.hardware.Camera.Size;
41import android.hardware.Sensor;
42import android.hardware.SensorEvent;
43import android.hardware.SensorEventListener;
44import android.hardware.SensorManager;
45import android.os.Bundle;
46import android.os.Handler;
47import android.os.Message;
48import android.util.Log;
49import android.view.View;
50import android.view.WindowManager;
51import android.widget.Button;
52import android.widget.ImageView;
53import android.widget.TextView;
54
55import java.io.ByteArrayOutputStream;
56import java.util.List;
57
58/**
59 * Activity to handle panorama capturing.
60 */
61public class PanoramaActivity extends Activity implements
62        ModePicker.OnModeChangeListener,
63        SurfaceTexture.OnFrameAvailableListener,
64        MosaicRendererSurfaceViewRenderer.MosaicSurfaceCreateListener {
65    public static final int DEFAULT_SWEEP_ANGLE = 160;
66    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
67    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
68
69    private static final int MSG_FINAL_MOSAIC_READY = 1;
70    private static final int MSG_RESET_TO_PREVIEW = 2;
71
72    private static final String TAG = "PanoramaActivity";
73    private static final int PREVIEW_STOPPED = 0;
74    private static final int PREVIEW_ACTIVE = 1;
75    private static final int CAPTURE_VIEWFINDER = 0;
76    private static final int CAPTURE_MOSAIC = 1;
77
78    // Speed is in unit of deg/sec
79    private static final float PANNING_SPEED_THRESHOLD = 30f;
80
81    // Ratio of nanosecond to second
82    private static final float NS2S = 1.0f / 1000000000.0f;
83
84    private boolean mPausing;
85
86    private View mPanoControlLayout;
87    private View mCaptureLayout;
88    private Button mStopCaptureButton;
89    private View mReviewLayout;
90    private ImageView mReview;
91    private CaptureView mCaptureView;
92    private MosaicRendererSurfaceView mMosaicView;
93    private TextView mTooFastPrompt;
94
95    private ProgressDialog mProgressDialog;
96    private String mPreparePreviewString;
97    private String mGeneratePanoramaString;
98
99    private int mPreviewWidth;
100    private int mPreviewHeight;
101    private Camera mCameraDevice;
102    private int mCameraState;
103    private int mCaptureState;
104    private SensorManager mSensorManager;
105    private Sensor mSensor;
106    private ModePicker mModePicker;
107    private MosaicFrameProcessor mMosaicFrameProcessor;
108    private String mCurrentImagePath = null;
109    private long mTimeTaken;
110    private Handler mMainHandler;
111    private SurfaceTexture mSurfaceTexture;
112    private boolean mThreadRunning;
113    private float[] mTransformMatrix;
114    private float mHorizontalViewAngle;
115    private float mVerticalViewAngle;
116
117    @Override
118    public void onCreate(Bundle icicle) {
119        super.onCreate(icicle);
120
121        getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
122                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
123
124        createContentView();
125
126        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
127        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
128        if (mSensor == null) {
129            mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
130        }
131
132        mTransformMatrix = new float[16];
133
134        mPreparePreviewString =
135                getResources().getString(R.string.pano_dialog_prepare_preview);
136        mGeneratePanoramaString =
137                getResources().getString(R.string.pano_dialog_generate_panorama);
138
139        mMainHandler = new Handler() {
140            @Override
141            public void handleMessage(Message msg) {
142                switch (msg.what) {
143                    case MSG_FINAL_MOSAIC_READY:
144                        onBackgroundThreadFinished();
145                        showFinalMosaic((Bitmap) msg.obj);
146                        break;
147                    case MSG_RESET_TO_PREVIEW:
148                        onBackgroundThreadFinished();
149                        resetToPreview();
150                        break;
151                }
152                clearMosaicFrameProcessorIfNeeded();
153            }
154        };
155    }
156
157    private void setupCamera() {
158        openCamera();
159        Parameters parameters = mCameraDevice.getParameters();
160        setupCaptureParams(parameters);
161        configureCamera(parameters);
162    }
163
164    private void releaseCamera() {
165        if (mCameraDevice != null) {
166            mCameraDevice.setPreviewCallbackWithBuffer(null);
167            CameraHolder.instance().release();
168            mCameraDevice = null;
169            mCameraState = PREVIEW_STOPPED;
170        }
171    }
172
173    private void openCamera() {
174        try {
175            mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId());
176        } catch (CameraHardwareException e) {
177            Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
178            return;
179        } catch (CameraDisabledException e) {
180            Util.showErrorAndFinish(this, R.string.camera_disabled);
181            return;
182        }
183    }
184
185    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
186            boolean needSmaller) {
187        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
188        boolean hasFound = false;
189        for (Size size : supportedSizes) {
190            int h = size.height;
191            int w = size.width;
192            // we only want 4:3 format.
193            int d = DEFAULT_CAPTURE_PIXELS - h * w;
194            if (needSmaller && d < 0) { // no bigger preview than 960x720.
195                continue;
196            }
197            if (need4To3 && (h * 4 != w * 3)) {
198                continue;
199            }
200            d = Math.abs(d);
201            if (d < pixelsDiff) {
202                mPreviewWidth = w;
203                mPreviewHeight = h;
204                pixelsDiff = d;
205                hasFound = true;
206            }
207        }
208        return hasFound;
209    }
210
211    private void setupCaptureParams(Parameters parameters) {
212        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
213        if (!findBestPreviewSize(supportedSizes, true, true)) {
214            Log.w(TAG, "No 4:3 ratio preview size supported.");
215            if (!findBestPreviewSize(supportedSizes, false, true)) {
216                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
217                findBestPreviewSize(supportedSizes, false, false);
218            }
219        }
220        Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
221        parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
222
223        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
224        int last = frameRates.size() - 1;
225        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
226        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
227        parameters.setPreviewFpsRange(minFps, maxFps);
228        Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
229
230        parameters.setRecordingHint(false);
231
232        mHorizontalViewAngle = parameters.getHorizontalViewAngle();
233        mVerticalViewAngle = parameters.getVerticalViewAngle();
234    }
235
236    public int getPreviewBufSize() {
237        PixelFormat pixelInfo = new PixelFormat();
238        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
239        // TODO: remove this extra 32 byte after the driver bug is fixed.
240        return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
241    }
242
243    private void configureCamera(Parameters parameters) {
244        mCameraDevice.setParameters(parameters);
245
246        int orientation = Util.getDisplayOrientation(Util.getDisplayRotation(this),
247                CameraHolder.instance().getBackCameraId());
248        mCameraDevice.setDisplayOrientation(orientation);
249    }
250
251    private boolean switchToOtherMode(int mode) {
252        if (isFinishing()) {
253            return false;
254        }
255        MenuHelper.gotoMode(mode, this);
256        finish();
257        return true;
258    }
259
260    public boolean onModeChanged(int mode) {
261        if (mode != ModePicker.MODE_PANORAMA) {
262            return switchToOtherMode(mode);
263        } else {
264            return true;
265        }
266    }
267
268    @Override
269    public void onMosaicSurfaceCreated(final int textureID) {
270        runOnUiThread(new Runnable() {
271            @Override
272            public void run() {
273                if (mSurfaceTexture != null) {
274                    mSurfaceTexture.release();
275                }
276                mSurfaceTexture = new SurfaceTexture(textureID);
277                if (!mPausing) {
278                    mSurfaceTexture.setOnFrameAvailableListener(PanoramaActivity.this);
279                    startCameraPreview();
280                }
281            }
282        });
283    }
284
285    public void runViewFinder() {
286        mMosaicView.setWarping(false);
287        // Call preprocess to render it to low-res and high-res RGB textures.
288        mMosaicView.preprocess(mTransformMatrix);
289        mMosaicView.setReady();
290        mMosaicView.requestRender();
291    }
292
293    public void runMosaicCapture() {
294        mMosaicView.setWarping(true);
295        // Call preprocess to render it to low-res and high-res RGB textures.
296        mMosaicView.preprocess(mTransformMatrix);
297        // Lock the conditional variable to ensure the order of transferGPUtoCPU and
298        // mMosaicFrame.processFrame().
299        mMosaicView.lockPreviewReadyFlag();
300        // Now, transfer the textures from GPU to CPU memory for processing
301        mMosaicView.transferGPUtoCPU();
302        // Wait on the condition variable (will be opened when GPU->CPU transfer is done).
303        mMosaicView.waitUntilPreviewReady();
304        mMosaicFrameProcessor.processFrame();
305    }
306
307    public synchronized void onFrameAvailable(SurfaceTexture surface) {
308        /* This function may be called by some random thread,
309         * so let's be safe and use synchronize. No OpenGL calls can be done here.
310         */
311        // Updating the texture should be done in the GL thread which mMosaicView is attached.
312        mMosaicView.queueEvent(new Runnable() {
313            @Override
314            public void run() {
315                mSurfaceTexture.updateTexImage();
316                mSurfaceTexture.getTransformMatrix(mTransformMatrix);
317            }
318        });
319        // Update the transformation matrix for mosaic pre-process.
320        if (mCaptureState == CAPTURE_VIEWFINDER) {
321            runViewFinder();
322        } else {
323            runMosaicCapture();
324        }
325    }
326
327    public void startCapture() {
328        // Reset values so we can do this again.
329        mTimeTaken = System.currentTimeMillis();
330        mCaptureState = CAPTURE_MOSAIC;
331        mTooFastPrompt.setVisibility(View.GONE);
332
333        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
334            @Override
335            public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
336                    int traversedAngleX, int traversedAngleY) {
337                if (isFinished) {
338                    stopCapture();
339                } else {
340                    updateProgress(panningRateX, panningRateY, traversedAngleX, traversedAngleY);
341                }
342            }
343        });
344
345        mStopCaptureButton.setVisibility(View.VISIBLE);
346        mCaptureView.setVisibility(View.VISIBLE);
347        mMosaicView.setVisibility(View.VISIBLE);
348        mPanoControlLayout.setVisibility(View.GONE);
349
350    }
351
352    private void stopCapture() {
353        mCaptureState = CAPTURE_VIEWFINDER;
354
355        mMosaicFrameProcessor.setProgressListener(null);
356        stopCameraPreview();
357
358        mSurfaceTexture.setOnFrameAvailableListener(null);
359
360        // TODO: show some dialog for long computation.
361        if (!mThreadRunning) {
362            runBackgroundThread(mPreparePreviewString, new Thread() {
363                @Override
364                public void run() {
365                    byte[] jpegData = generateFinalMosaic(false);
366                    Bitmap bitmap = null;
367                    if (jpegData != null) {
368                        bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
369                    }
370                    mMainHandler.sendMessage(mMainHandler.obtainMessage(
371                            MSG_FINAL_MOSAIC_READY, bitmap));
372                }
373            });
374        }
375    }
376
377    private void updateProgress(float panningRateX, float panningRateY,
378            int traversedAngleX, int traversedAngleY) {
379
380        mMosaicView.setReady();
381        mMosaicView.requestRender();
382
383        // TODO: Now we just display warning message by the panning speed.
384        // Since we only support horizontal panning, we should display a warning message
385        // in UI when there're significant vertical movements.
386        if ((panningRateX * mHorizontalViewAngle > PANNING_SPEED_THRESHOLD)
387                || (panningRateY * mVerticalViewAngle > PANNING_SPEED_THRESHOLD)) {
388            // TODO: draw speed indication according to the UI spec.
389            mTooFastPrompt.setVisibility(View.VISIBLE);
390            mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1);
391            mCaptureView.invalidate();
392        } else {
393            mTooFastPrompt.setVisibility(View.GONE);
394            mCaptureView.setSweepAngle(Math.max(traversedAngleX, traversedAngleY) + 1);
395            mCaptureView.invalidate();
396        }
397    }
398
399    private void createContentView() {
400        setContentView(R.layout.panorama);
401
402        mCaptureState = CAPTURE_VIEWFINDER;
403
404        mCaptureLayout = (View) findViewById(R.id.pano_capture_layout);
405        mCaptureView = (CaptureView) findViewById(R.id.pano_capture_view);
406        mCaptureView.setStartAngle(-DEFAULT_SWEEP_ANGLE / 2);
407        mStopCaptureButton = (Button) findViewById(R.id.pano_capture_stop_button);
408        mTooFastPrompt = (TextView) findViewById(R.id.pano_capture_too_fast_textview);
409
410        mReviewLayout = (View) findViewById(R.id.pano_review_layout);
411        mReview = (ImageView) findViewById(R.id.pano_reviewarea);
412        mMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer);
413        mMosaicView.getRenderer().setMosaicSurfaceCreateListener(this);
414        mMosaicView.setVisibility(View.VISIBLE);
415
416        mPanoControlLayout = (View) findViewById(R.id.pano_control_layout);
417        mModePicker = (ModePicker) findViewById(R.id.mode_picker);
418        mModePicker.setVisibility(View.VISIBLE);
419        mModePicker.setOnModeChangeListener(this);
420        mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
421    }
422
423    @OnClickAttr
424    public void onStartButtonClicked(View v) {
425        // If mSurfaceTexture == null then GL setup is not finished yet.
426        // No buttons can be pressed.
427        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
428        startCapture();
429    }
430
431    @OnClickAttr
432    public void onStopButtonClicked(View v) {
433        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
434        stopCapture();
435    }
436
437    @OnClickAttr
438    public void onOkButtonClicked(View v) {
439        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
440        runBackgroundThread(mGeneratePanoramaString, new Thread() {
441            @Override
442            public void run() {
443                byte[] jpegData = generateFinalMosaic(true);
444                savePanorama(jpegData);
445                mMainHandler.sendMessage(mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW));
446            }
447        });
448    }
449
450    private void runBackgroundThread(String str, Thread thread) {
451        mThreadRunning = true;
452        mProgressDialog = ProgressDialog.show(this, "", str);
453        thread.start();
454    }
455
456    private void onBackgroundThreadFinished() {
457        mThreadRunning = false;
458        mProgressDialog.dismiss();
459        mProgressDialog = null;
460    }
461
462    @OnClickAttr
463    public void onRetakeButtonClicked(View v) {
464        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
465        resetToPreview();
466    }
467
468    private void resetToPreview() {
469        mCaptureState = CAPTURE_VIEWFINDER;
470
471        mReviewLayout.setVisibility(View.GONE);
472        mStopCaptureButton.setVisibility(View.GONE);
473        mCaptureView.setVisibility(View.GONE);
474        mPanoControlLayout.setVisibility(View.VISIBLE);
475        mCaptureLayout.setVisibility(View.VISIBLE);
476        mMosaicFrameProcessor.reset();
477
478        mSurfaceTexture.setOnFrameAvailableListener(this);
479
480        if (!mPausing) startCameraPreview();
481
482        mMosaicView.setVisibility(View.VISIBLE);
483    }
484
485    private void showFinalMosaic(Bitmap bitmap) {
486        if (bitmap != null) {
487            mReview.setImageBitmap(bitmap);
488        }
489        mCaptureLayout.setVisibility(View.GONE);
490        mReviewLayout.setVisibility(View.VISIBLE);
491        mCaptureView.setSweepAngle(0);
492    }
493
494    private void savePanorama(byte[] jpegData) {
495        if (jpegData != null) {
496            String imagePath = PanoUtil.createName(
497                    getResources().getString(R.string.pano_file_name_format), mTimeTaken);
498            Storage.addImage(getContentResolver(), imagePath, mTimeTaken, null, 0,
499                    jpegData);
500        }
501    }
502
503    private void clearMosaicFrameProcessorIfNeeded() {
504        if (!mPausing || mThreadRunning) return;
505        mMosaicFrameProcessor.clear();
506    }
507
508    private void initMosaicFrameProcessorIfNeeded() {
509        if (mPausing || mThreadRunning) return;
510        if (mMosaicFrameProcessor == null) {
511            // Start the activity for the first time.
512            mMosaicFrameProcessor = new MosaicFrameProcessor(DEFAULT_SWEEP_ANGLE - 5,
513                    mPreviewWidth, mPreviewHeight, getPreviewBufSize());
514        }
515        mMosaicFrameProcessor.initialize();
516    }
517
518    @Override
519    protected void onPause() {
520        super.onPause();
521
522        releaseCamera();
523        mPausing = true;
524        mMosaicView.onPause();
525        mSensorManager.unregisterListener(mListener);
526        clearMosaicFrameProcessorIfNeeded();
527        System.gc();
528    }
529
530    @Override
531    protected void onResume() {
532        super.onResume();
533
534        mPausing = false;
535        /*
536         * It is not necessary to get accelerometer events at a very high rate,
537         * by using a slower rate (SENSOR_DELAY_UI), we get an automatic
538         * low-pass filter, which "extracts" the gravity component of the
539         * acceleration. As an added benefit, we use less power and CPU
540         * resources.
541         */
542        mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI);
543        mCaptureState = CAPTURE_VIEWFINDER;
544        setupCamera();
545        if (mSurfaceTexture != null) {
546            mSurfaceTexture.setOnFrameAvailableListener(this);
547            startCameraPreview();
548        }
549        // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size
550        // has to be decided by camera device.
551        initMosaicFrameProcessorIfNeeded();
552        mMosaicView.onResume();
553    }
554
555    private final SensorEventListener mListener = new SensorEventListener() {
556        private float mCompassCurrX; // degrees
557        private float mCompassCurrY; // degrees
558        private float mTimestamp;
559
560        public void onSensorChanged(SensorEvent event) {
561            if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
562                if (mTimestamp != 0) {
563                    final float dT = (event.timestamp - mTimestamp) * NS2S;
564                    mCompassCurrX += event.values[1] * dT * 180.0f / Math.PI;
565                    mCompassCurrY += event.values[0] * dT * 180.0f / Math.PI;
566                }
567                mTimestamp = event.timestamp;
568
569            } else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) {
570                mCompassCurrX = event.values[0];
571                mCompassCurrY = event.values[1];
572            }
573
574            if (mMosaicFrameProcessor != null) {
575                mMosaicFrameProcessor.updateCompassValue(mCompassCurrX, mCompassCurrY);
576            }
577        }
578
579        @Override
580        public void onAccuracyChanged(Sensor sensor, int accuracy) {
581        }
582    };
583
584    public byte[] generateFinalMosaic(boolean highRes) {
585        mMosaicFrameProcessor.createMosaic(highRes);
586
587        byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
588        if (imageData == null) {
589            Log.e(TAG, "getFinalMosaicNV21() returned null.");
590            return null;
591        }
592
593        int len = imageData.length - 8;
594        int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
595                + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
596        int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
597                + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
598        Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
599
600        if (width <= 0 || height <= 0) {
601            // TODO: pop up a error meesage indicating that the final result is not generated.
602            Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
603                    height);
604            return null;
605        }
606
607        YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
608        ByteArrayOutputStream out = new ByteArrayOutputStream();
609        yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
610        try {
611            out.close();
612        } catch (Exception e) {
613            Log.e(TAG, "Exception in storing final mosaic", e);
614            return null;
615        }
616        return out.toByteArray();
617    }
618
619    private void setPreviewTexture(SurfaceTexture surface) {
620        try {
621            mCameraDevice.setPreviewTexture(surface);
622        } catch (Throwable ex) {
623            releaseCamera();
624            throw new RuntimeException("setPreviewTexture failed", ex);
625        }
626    }
627
628    private void startCameraPreview() {
629        // If we're previewing already, stop the preview first (this will blank
630        // the screen).
631        if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
632
633        setPreviewTexture(mSurfaceTexture);
634
635        try {
636            Log.v(TAG, "startPreview");
637            mCameraDevice.startPreview();
638        } catch (Throwable ex) {
639            releaseCamera();
640            throw new RuntimeException("startPreview failed", ex);
641        }
642        mCameraState = PREVIEW_ACTIVE;
643    }
644
645    private void stopCameraPreview() {
646        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
647            Log.v(TAG, "stopPreview");
648            mCameraDevice.stopPreview();
649        }
650        mCameraState = PREVIEW_STOPPED;
651    }
652}
653