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