PanoramaActivity.java revision 757b67951f40850b1ac3102dc0b6fe8977615a4a
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.Exif;
23import com.android.camera.MenuHelper;
24import com.android.camera.ModePicker;
25import com.android.camera.OnClickAttr;
26import com.android.camera.R;
27import com.android.camera.Storage;
28import com.android.camera.Thumbnail;
29import com.android.camera.Util;
30import com.android.camera.ui.RotateImageView;
31import com.android.camera.ui.SharePopup;
32
33import android.app.Activity;
34import android.app.AlertDialog;
35import android.app.ProgressDialog;
36import android.content.Context;
37import android.content.DialogInterface;
38import android.graphics.Bitmap;
39import android.graphics.BitmapFactory;
40import android.graphics.ImageFormat;
41import android.graphics.PixelFormat;
42import android.graphics.Rect;
43import android.graphics.SurfaceTexture;
44import android.graphics.YuvImage;
45import android.hardware.Camera;
46import android.hardware.Camera.Parameters;
47import android.hardware.Camera.Size;
48import android.hardware.Sensor;
49import android.hardware.SensorEvent;
50import android.hardware.SensorEventListener;
51import android.hardware.SensorManager;
52import android.net.Uri;
53import android.os.Bundle;
54import android.os.Handler;
55import android.os.Message;
56import android.util.Log;
57import android.view.Gravity;
58import android.view.OrientationEventListener;
59import android.view.View;
60import android.view.WindowManager;
61import android.view.animation.Animation;
62import android.view.animation.AnimationUtils;
63import android.widget.Button;
64import android.widget.ImageView;
65import android.widget.TextView;
66
67import java.io.ByteArrayOutputStream;
68import java.util.List;
69
70/**
71 * Activity to handle panorama capturing.
72 */
73public class PanoramaActivity extends Activity implements
74        ModePicker.OnModeChangeListener,
75        SurfaceTexture.OnFrameAvailableListener,
76        MosaicRendererSurfaceViewRenderer.MosaicSurfaceCreateListener {
77    public static final int DEFAULT_SWEEP_ANGLE = 160;
78    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
79    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
80
81    private static final int MSG_FINAL_MOSAIC_READY = 1;
82    private static final int MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL = 2;
83    private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 3;
84    private static final int MSG_DISMISS_ALERT_DIALOG_AND_RESET_TO_PREVIEW = 4;
85
86    private static final String TAG = "PanoramaActivity";
87    private static final int PREVIEW_STOPPED = 0;
88    private static final int PREVIEW_ACTIVE = 1;
89    private static final int CAPTURE_VIEWFINDER = 0;
90    private static final int CAPTURE_MOSAIC = 1;
91
92    // Speed is in unit of deg/sec
93    private static final float PANNING_SPEED_THRESHOLD = 30f;
94
95    // Ratio of nanosecond to second
96    private static final float NS2S = 1.0f / 1000000000.0f;
97
98    private boolean mPausing;
99
100    private View mPanoControlLayout;
101    private View mPanoLayout;
102    private View mCaptureLayout;
103    private Button mStopCaptureButton;
104    private View mReviewLayout;
105    private ImageView mReview;
106    private IndicationView mIndicationView;
107    private MosaicRendererSurfaceView mMosaicView;
108    private TextView mTooFastPrompt;
109    private Animation mSlideIn, mSlideOut;
110
111    private ProgressDialog mProgressDialog;
112    private String mPreparePreviewString;
113    private String mGeneratePanoramaString;
114    private String mDialogTitle;
115    private String mDialogOk;
116    private AlertDialog mAlertDialog;
117
118    private float mCompassValueX;
119    private float mCompassValueY;
120    private float mCompassValueXStart;
121    private float mCompassValueYStart;
122    private float mCompassValueXStartBuffer;
123    private float mCompassValueYStartBuffer;
124    private int mCompassThreshold;
125    private int mTraversedAngleX;
126    private int mTraversedAngleY;
127    private long mTimestamp;
128    // Control variables for the terminate condition.
129    private int mMinAngleX;
130    private int mMaxAngleX;
131    private int mMinAngleY;
132    private int mMaxAngleY;
133
134    private RotateImageView mThumbnailView;
135    private Thumbnail mThumbnail;
136    private SharePopup mSharePopup;
137
138    private int mPreviewWidth;
139    private int mPreviewHeight;
140    private Camera mCameraDevice;
141    private int mCameraState;
142    private int mCaptureState;
143    private SensorManager mSensorManager;
144    private Sensor mSensor;
145    private ModePicker mModePicker;
146    private MosaicFrameProcessor mMosaicFrameProcessor;
147    private long mTimeTaken;
148    private Handler mMainHandler;
149    private SurfaceTexture mSurfaceTexture;
150    private boolean mThreadRunning;
151    private float[] mTransformMatrix;
152    private float mHorizontalViewAngle;
153
154    private PanoOrientationEventListener mOrientationEventListener;
155    // The value could be 0, 1, 2, 3 for the 4 different orientations measured in clockwise
156    // respectively.
157    private int mDeviceOrientation;
158
159    private class MosaicJpeg {
160        public MosaicJpeg(byte[] data, int width, int height) {
161            this.data = data;
162            this.width = width;
163            this.height = height;
164        }
165
166        public final byte[] data;
167        public final int width;
168        public final int height;
169    }
170
171    private class PanoOrientationEventListener extends OrientationEventListener {
172        public PanoOrientationEventListener(Context context) {
173            super(context);
174        }
175
176        @Override
177        public void onOrientationChanged(int orientation) {
178            // Default to the last known orientation.
179            if (orientation == ORIENTATION_UNKNOWN) return;
180            mDeviceOrientation = ((orientation + 45) / 90) % 4;
181        }
182    }
183
184    @Override
185    public void onCreate(Bundle icicle) {
186        super.onCreate(icicle);
187
188        getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
189                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
190
191        createContentView();
192
193        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
194        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
195        if (mSensor == null) {
196            mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
197        }
198
199        mOrientationEventListener = new PanoOrientationEventListener(this);
200
201        mTransformMatrix = new float[16];
202
203        mPreparePreviewString =
204                getResources().getString(R.string.pano_dialog_prepare_preview);
205        mGeneratePanoramaString =
206                getResources().getString(R.string.pano_dialog_generate_panorama);
207        mDialogTitle = getResources().getString(R.string.pano_dialog_title);
208        mDialogOk = getResources().getString(R.string.dialog_ok);
209
210        Context context = getApplicationContext();
211        mSlideIn = AnimationUtils.loadAnimation(context, R.anim.slide_in_from_right);
212        mSlideOut = AnimationUtils.loadAnimation(context, R.anim.slide_out_to_right);
213
214        mMainHandler = new Handler() {
215            @Override
216            public void handleMessage(Message msg) {
217                switch (msg.what) {
218                    case MSG_FINAL_MOSAIC_READY:
219                        onBackgroundThreadFinished();
220                        showFinalMosaic((Bitmap) msg.obj);
221                        break;
222                    case MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL:
223                        onBackgroundThreadFinished();
224                        // Set the thumbnail bitmap here because mThumbnailView must be accessed
225                        // from the UI thread.
226                        if (mThumbnail != null) {
227                            mThumbnailView.setBitmap(mThumbnail.getBitmap());
228                        }
229                        resetToPreview();
230                        break;
231                    case MSG_GENERATE_FINAL_MOSAIC_ERROR:
232                        onBackgroundThreadFinished();
233                        mAlertDialog = (new AlertDialog.Builder(PanoramaActivity.this))
234                                .setTitle(mDialogTitle)
235                                .setMessage(R.string.pano_dialog_panorama_failed)
236                                .create();
237                        mAlertDialog.setButton(DialogInterface.BUTTON_POSITIVE, mDialogOk,
238                                obtainMessage(MSG_DISMISS_ALERT_DIALOG_AND_RESET_TO_PREVIEW));
239                        mAlertDialog.show();
240                        break;
241                    case MSG_DISMISS_ALERT_DIALOG_AND_RESET_TO_PREVIEW:
242                        mAlertDialog.dismiss();
243                        mAlertDialog = null;
244                        resetToPreview();
245                        break;
246                }
247                clearMosaicFrameProcessorIfNeeded();
248            }
249        };
250    }
251
252    private void setupCamera() {
253        openCamera();
254        Parameters parameters = mCameraDevice.getParameters();
255        setupCaptureParams(parameters);
256        configureCamera(parameters);
257    }
258
259    private void releaseCamera() {
260        if (mCameraDevice != null) {
261            mCameraDevice.setPreviewCallbackWithBuffer(null);
262            CameraHolder.instance().release();
263            mCameraDevice = null;
264            mCameraState = PREVIEW_STOPPED;
265        }
266    }
267
268    private void openCamera() {
269        try {
270            mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId());
271        } catch (CameraHardwareException e) {
272            Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
273            return;
274        } catch (CameraDisabledException e) {
275            Util.showErrorAndFinish(this, R.string.camera_disabled);
276            return;
277        }
278    }
279
280    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
281            boolean needSmaller) {
282        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
283        boolean hasFound = false;
284        for (Size size : supportedSizes) {
285            int h = size.height;
286            int w = size.width;
287            // we only want 4:3 format.
288            int d = DEFAULT_CAPTURE_PIXELS - h * w;
289            if (needSmaller && d < 0) { // no bigger preview than 960x720.
290                continue;
291            }
292            if (need4To3 && (h * 4 != w * 3)) {
293                continue;
294            }
295            d = Math.abs(d);
296            if (d < pixelsDiff) {
297                mPreviewWidth = w;
298                mPreviewHeight = h;
299                pixelsDiff = d;
300                hasFound = true;
301            }
302        }
303        return hasFound;
304    }
305
306    private void setupCaptureParams(Parameters parameters) {
307        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
308        if (!findBestPreviewSize(supportedSizes, true, true)) {
309            Log.w(TAG, "No 4:3 ratio preview size supported.");
310            if (!findBestPreviewSize(supportedSizes, false, true)) {
311                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
312                findBestPreviewSize(supportedSizes, false, false);
313            }
314        }
315        Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
316        parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
317
318        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
319        int last = frameRates.size() - 1;
320        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
321        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
322        parameters.setPreviewFpsRange(minFps, maxFps);
323        Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
324
325        parameters.setRecordingHint(false);
326
327        mHorizontalViewAngle = ((mDeviceOrientation % 2) == 0) ?
328                parameters.getHorizontalViewAngle() : parameters.getVerticalViewAngle();
329    }
330
331    public int getPreviewBufSize() {
332        PixelFormat pixelInfo = new PixelFormat();
333        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
334        // TODO: remove this extra 32 byte after the driver bug is fixed.
335        return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
336    }
337
338    private void configureCamera(Parameters parameters) {
339        mCameraDevice.setParameters(parameters);
340
341        int orientation = Util.getDisplayOrientation(Util.getDisplayRotation(this),
342                CameraHolder.instance().getBackCameraId());
343        mCameraDevice.setDisplayOrientation(orientation);
344    }
345
346    private boolean switchToOtherMode(int mode) {
347        if (isFinishing()) {
348            return false;
349        }
350        MenuHelper.gotoMode(mode, this);
351        finish();
352        return true;
353    }
354
355    public boolean onModeChanged(int mode) {
356        if (mode != ModePicker.MODE_PANORAMA) {
357            return switchToOtherMode(mode);
358        } else {
359            return true;
360        }
361    }
362
363    @Override
364    public void onMosaicSurfaceCreated(final int textureID) {
365        runOnUiThread(new Runnable() {
366            @Override
367            public void run() {
368                if (mSurfaceTexture != null) {
369                    mSurfaceTexture.release();
370                }
371                mSurfaceTexture = new SurfaceTexture(textureID);
372                if (!mPausing) {
373                    mSurfaceTexture.setOnFrameAvailableListener(PanoramaActivity.this);
374                    startCameraPreview();
375                }
376            }
377        });
378    }
379
380    public void runViewFinder() {
381        mMosaicView.setWarping(false);
382        // Call preprocess to render it to low-res and high-res RGB textures.
383        mMosaicView.preprocess(mTransformMatrix);
384        mMosaicView.setReady();
385        mMosaicView.requestRender();
386    }
387
388    public void runMosaicCapture() {
389        mMosaicView.setWarping(true);
390        // Call preprocess to render it to low-res and high-res RGB textures.
391        mMosaicView.preprocess(mTransformMatrix);
392        // Lock the conditional variable to ensure the order of transferGPUtoCPU and
393        // mMosaicFrame.processFrame().
394        mMosaicView.lockPreviewReadyFlag();
395        // Now, transfer the textures from GPU to CPU memory for processing
396        mMosaicView.transferGPUtoCPU();
397        // Wait on the condition variable (will be opened when GPU->CPU transfer is done).
398        mMosaicView.waitUntilPreviewReady();
399        mMosaicFrameProcessor.processFrame();
400    }
401
402    public synchronized void onFrameAvailable(SurfaceTexture surface) {
403        /* This function may be called by some random thread,
404         * so let's be safe and use synchronize. No OpenGL calls can be done here.
405         */
406        // Updating the texture should be done in the GL thread which mMosaicView is attached.
407        mMosaicView.queueEvent(new Runnable() {
408            @Override
409            public void run() {
410                mSurfaceTexture.updateTexImage();
411                mSurfaceTexture.getTransformMatrix(mTransformMatrix);
412            }
413        });
414        // Update the transformation matrix for mosaic pre-process.
415        if (mCaptureState == CAPTURE_VIEWFINDER) {
416            runViewFinder();
417        } else {
418            runMosaicCapture();
419        }
420    }
421
422    public void startCapture() {
423        // Reset values so we can do this again.
424        mTimeTaken = System.currentTimeMillis();
425        mCaptureState = CAPTURE_MOSAIC;
426        mPanoControlLayout.startAnimation(mSlideOut);
427        mPanoControlLayout.setVisibility(View.GONE);
428
429        mCompassValueXStart = mCompassValueXStartBuffer;
430        mCompassValueYStart = mCompassValueYStartBuffer;
431        mMinAngleX = 0;
432        mMaxAngleX = 0;
433        mMinAngleY = 0;
434        mMaxAngleY = 0;
435        mTimestamp = 0;
436
437        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
438            @Override
439            public void onProgress(boolean isFinished, float panningRateX, float panningRateY) {
440                if (isFinished
441                        || (mMaxAngleX - mMinAngleX >= DEFAULT_SWEEP_ANGLE)
442                        || (mMaxAngleY - mMinAngleY >= DEFAULT_SWEEP_ANGLE)) {
443                    stopCapture();
444                } else {
445                    updateProgress(panningRateX);
446                }
447            }
448        });
449
450        mStopCaptureButton.setVisibility(View.VISIBLE);
451        mIndicationView.resetAngles();
452        mIndicationView.setVisibility(View.VISIBLE);
453        mMosaicView.setVisibility(View.VISIBLE);
454    }
455
456    private void stopCapture() {
457        mCaptureState = CAPTURE_VIEWFINDER;
458        mTooFastPrompt.setVisibility(View.GONE);
459
460        mMosaicFrameProcessor.setProgressListener(null);
461        stopCameraPreview();
462
463        mSurfaceTexture.setOnFrameAvailableListener(null);
464
465        if (!mThreadRunning) {
466            runBackgroundThreadAndShowDialog(mPreparePreviewString, false, new Thread() {
467                @Override
468                public void run() {
469                    MosaicJpeg jpeg = generateFinalMosaic(false);
470                    Bitmap bitmap = null;
471                    if (jpeg != null) {
472                        bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
473                    }
474                    mMainHandler.sendMessage(mMainHandler.obtainMessage(
475                            MSG_FINAL_MOSAIC_READY, bitmap));
476                }
477            });
478            reportProgress(false);
479        }
480    }
481
482    private void updateProgress(float panningRate) {
483        mMosaicView.setReady();
484        mMosaicView.requestRender();
485
486        // TODO: Now we just display warning message by the panning speed.
487        // Since we only support horizontal panning, we should display a warning message
488        // in UI when there're significant vertical movements.
489        if (Math.abs(panningRate * mHorizontalViewAngle) > PANNING_SPEED_THRESHOLD) {
490            // TODO: draw speed indication according to the UI spec.
491            mTooFastPrompt.setVisibility(View.VISIBLE);
492        } else {
493            mTooFastPrompt.setVisibility(View.GONE);
494        }
495    }
496
497    private void createContentView() {
498        setContentView(R.layout.panorama);
499
500        mCaptureState = CAPTURE_VIEWFINDER;
501
502        mCaptureLayout = (View) findViewById(R.id.pano_capture_layout);
503        mIndicationView = (IndicationView) findViewById(R.id.pano_capture_view);
504        mIndicationView.setMaxSweepAngle(DEFAULT_SWEEP_ANGLE);
505        mStopCaptureButton = (Button) findViewById(R.id.pano_capture_stop_button);
506        mTooFastPrompt = (TextView) findViewById(R.id.pano_capture_too_fast_textview);
507
508        mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail);
509
510        mReviewLayout = (View) findViewById(R.id.pano_review_layout);
511        mReview = (ImageView) findViewById(R.id.pano_reviewarea);
512        mMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer);
513        mMosaicView.getRenderer().setMosaicSurfaceCreateListener(this);
514        mMosaicView.setVisibility(View.VISIBLE);
515
516        mPanoControlLayout = (View) findViewById(R.id.pano_control_layout);
517        mModePicker = (ModePicker) findViewById(R.id.mode_picker);
518        mModePicker.setVisibility(View.VISIBLE);
519        mModePicker.setOnModeChangeListener(this);
520        mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
521
522        mPanoLayout = findViewById(R.id.pano_layout);
523    }
524
525    @OnClickAttr
526    public void onStartButtonClicked(View v) {
527        // If mSurfaceTexture == null then GL setup is not finished yet.
528        // No buttons can be pressed.
529        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
530        startCapture();
531    }
532
533    @OnClickAttr
534    public void onStopButtonClicked(View v) {
535        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
536        stopCapture();
537    }
538
539    public void reportProgress(final boolean highRes) {
540        Thread t = new Thread() {
541            @Override
542            public void run() {
543                while (mThreadRunning) {
544                    final int progress = mMosaicFrameProcessor.reportProgress(highRes);
545
546                    try {
547                        Thread.sleep(50);
548                    } catch (Exception e) {
549                        throw new RuntimeException("Panorama reportProgress failed", e);
550                    }
551                    // Update the progress bar
552                    runOnUiThread(new Runnable() {
553                        public void run() {
554                            // Check if mProgressDialog is null because the background thread
555                            // finished.
556                            if (mProgressDialog != null) {
557                                mProgressDialog.setProgress(progress);
558                            }
559                        }
560                    });
561                }
562            }
563        };
564        t.start();
565    }
566
567    @OnClickAttr
568    public void onOkButtonClicked(View v) {
569        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
570        runBackgroundThreadAndShowDialog(mGeneratePanoramaString, true, new Thread() {
571            @Override
572            public void run() {
573                MosaicJpeg jpeg = generateFinalMosaic(true);
574                if (jpeg == null) {
575                    mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
576                } else {
577                    int orientation = Exif.getOrientation(jpeg.data);
578                    Uri uri = savePanorama(jpeg.data, orientation);
579                    if (uri != null) {
580                        // Create a thumbnail whose width is equal or bigger than the entire screen.
581                        int ratio = (int) Math.ceil((double) jpeg.width / mPanoLayout.getWidth());
582                        int inSampleSize = Integer.highestOneBit(ratio);
583                        mThumbnail = Thumbnail.createThumbnail(
584                                jpeg.data, orientation, inSampleSize, uri);
585                    }
586                    mMainHandler.sendMessage(
587                            mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL));
588                }
589            }
590        });
591        reportProgress(true);
592    }
593
594    /**
595     * If the style is horizontal one, the maximum progress is assumed to be 100.
596     */
597    private void runBackgroundThreadAndShowDialog(
598            String str, boolean showPercentageProgress, Thread thread) {
599        mThreadRunning = true;
600        mProgressDialog = new ProgressDialog(this);
601        if (showPercentageProgress) {
602            mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
603            mProgressDialog.setMax(100);
604        }
605        mProgressDialog.setMessage(str);
606        mProgressDialog.show();
607        thread.start();
608    }
609
610    private void onBackgroundThreadFinished() {
611        mThreadRunning = false;
612        mProgressDialog.dismiss();
613        mProgressDialog = null;
614    }
615
616    @OnClickAttr
617    public void onRetakeButtonClicked(View v) {
618        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
619        resetToPreview();
620    }
621
622    @OnClickAttr
623    public void onThumbnailClicked(View v) {
624        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
625        showSharePopup();
626    }
627
628    private void showSharePopup() {
629        if (mThumbnail == null) return;
630        Uri uri = mThumbnail.getUri();
631        if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) {
632            // The orientation compensation is set to 0 here because we only support landscape.
633            // Panorama picture is long. Use pano_layout so the share popup can be full-screen.
634            mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(), 0,
635                    findViewById(R.id.pano_layout));
636        }
637        mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0);
638    }
639
640    private void resetToPreview() {
641        mCaptureState = CAPTURE_VIEWFINDER;
642
643        mReviewLayout.setVisibility(View.GONE);
644        mStopCaptureButton.setVisibility(View.GONE);
645        mIndicationView.setVisibility(View.GONE);
646        mPanoControlLayout.setVisibility(View.VISIBLE);
647        mPanoControlLayout.startAnimation(mSlideIn);
648        mCaptureLayout.setVisibility(View.VISIBLE);
649        mMosaicFrameProcessor.reset();
650
651        mSurfaceTexture.setOnFrameAvailableListener(this);
652
653        if (!mPausing) startCameraPreview();
654
655        mMosaicView.setVisibility(View.VISIBLE);
656    }
657
658    private void showFinalMosaic(Bitmap bitmap) {
659        if (bitmap != null) {
660            mReview.setImageBitmap(bitmap);
661        }
662        mCaptureLayout.setVisibility(View.GONE);
663        mReviewLayout.setVisibility(View.VISIBLE);
664    }
665
666    private Uri savePanorama(byte[] jpegData, int orientation) {
667        if (jpegData != null) {
668            String imagePath = PanoUtil.createName(
669                    getResources().getString(R.string.pano_file_name_format), mTimeTaken);
670            return Storage.addImage(getContentResolver(), imagePath, mTimeTaken, null,
671                    orientation, jpegData);
672        }
673        return null;
674    }
675
676    private void clearMosaicFrameProcessorIfNeeded() {
677        if (!mPausing || mThreadRunning) return;
678        mMosaicFrameProcessor.clear();
679    }
680
681    private void initMosaicFrameProcessorIfNeeded() {
682        if (mPausing || mThreadRunning) return;
683        if (mMosaicFrameProcessor == null) {
684            // Start the activity for the first time.
685            mMosaicFrameProcessor = new MosaicFrameProcessor(
686                    mPreviewWidth, mPreviewHeight, getPreviewBufSize());
687        }
688        mMosaicFrameProcessor.initialize();
689    }
690
691    @Override
692    protected void onPause() {
693        super.onPause();
694
695        releaseCamera();
696        mPausing = true;
697        mMosaicView.onPause();
698        mSensorManager.unregisterListener(mListener);
699        clearMosaicFrameProcessorIfNeeded();
700        mOrientationEventListener.disable();
701        System.gc();
702    }
703
704    @Override
705    protected void onResume() {
706        super.onResume();
707
708        mPausing = false;
709        mOrientationEventListener.enable();
710        /*
711         * It is not necessary to get accelerometer events at a very high rate,
712         * by using a game rate (SENSOR_DELAY_UI), we get an automatic
713         * low-pass filter, which "extracts" the gravity component of the
714         * acceleration. As an added benefit, we use less power and CPU
715         * resources.
716         */
717        mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI);
718        mCaptureState = CAPTURE_VIEWFINDER;
719        setupCamera();
720        if (mSurfaceTexture != null) {
721            mSurfaceTexture.setOnFrameAvailableListener(this);
722            startCameraPreview();
723        }
724        // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size
725        // has to be decided by camera device.
726        initMosaicFrameProcessorIfNeeded();
727        mMosaicView.onResume();
728    }
729
730    private void updateCompassValue() {
731        // By what angle has the camera moved since start of capture?
732        mTraversedAngleX = (int) (mCompassValueX - mCompassValueXStart);
733        mTraversedAngleY = (int) (mCompassValueY - mCompassValueYStart);
734        mMinAngleX = Math.min(mMinAngleX, mTraversedAngleX);
735        mMaxAngleX = Math.max(mMaxAngleX, mTraversedAngleX);
736        mMinAngleY = Math.min(mMinAngleY, mTraversedAngleY);
737        mMaxAngleY = Math.max(mMaxAngleY, mTraversedAngleY);
738
739        // Use orientation to identify if the user is panning to the right or the left.
740        switch (mDeviceOrientation) {
741            case 0:
742                mIndicationView.setSweepAngle(-mTraversedAngleX);
743                break;
744            case 1:
745                mIndicationView.setSweepAngle(mTraversedAngleY);
746                break;
747            case 2:
748                mIndicationView.setSweepAngle(mTraversedAngleX);
749                break;
750            case 3:
751                mIndicationView.setSweepAngle(-mTraversedAngleY);
752                break;
753        }
754        mIndicationView.invalidate();
755    }
756
757    private final SensorEventListener mListener = new SensorEventListener() {
758        public void onSensorChanged(SensorEvent event) {
759            if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
760                if (mTimestamp != 0) {
761                    final float dT = (event.timestamp - mTimestamp) * NS2S;
762                    mCompassValueX += event.values[1] * dT * 180.0f / Math.PI;
763                    mCompassValueY += event.values[0] * dT * 180.0f / Math.PI;
764                    mCompassValueXStartBuffer = mCompassValueX;
765                    mCompassValueYStartBuffer = mCompassValueY;
766                    updateCompassValue();
767                }
768                mTimestamp = event.timestamp;
769
770            }
771        }
772
773        @Override
774        public void onAccuracyChanged(Sensor sensor, int accuracy) {
775        }
776    };
777
778    public MosaicJpeg generateFinalMosaic(boolean highRes) {
779        mMosaicFrameProcessor.createMosaic(highRes);
780
781        byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
782        if (imageData == null) {
783            Log.e(TAG, "getFinalMosaicNV21() returned null.");
784            return null;
785        }
786
787        int len = imageData.length - 8;
788        int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
789                + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
790        int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
791                + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
792        Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
793
794        if (width <= 0 || height <= 0) {
795            // TODO: pop up a error meesage indicating that the final result is not generated.
796            Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
797                    height);
798            return null;
799        }
800
801        YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
802        ByteArrayOutputStream out = new ByteArrayOutputStream();
803        yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
804        try {
805            out.close();
806        } catch (Exception e) {
807            Log.e(TAG, "Exception in storing final mosaic", e);
808            return null;
809        }
810        return new MosaicJpeg(out.toByteArray(), width, height);
811    }
812
813    private void setPreviewTexture(SurfaceTexture surface) {
814        try {
815            mCameraDevice.setPreviewTexture(surface);
816        } catch (Throwable ex) {
817            releaseCamera();
818            throw new RuntimeException("setPreviewTexture failed", ex);
819        }
820    }
821
822    private void startCameraPreview() {
823        // If we're previewing already, stop the preview first (this will blank
824        // the screen).
825        if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
826
827        setPreviewTexture(mSurfaceTexture);
828
829        try {
830            Log.v(TAG, "startPreview");
831            mCameraDevice.startPreview();
832        } catch (Throwable ex) {
833            releaseCamera();
834            throw new RuntimeException("startPreview failed", ex);
835        }
836        mCameraState = PREVIEW_ACTIVE;
837    }
838
839    private void stopCameraPreview() {
840        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
841            Log.v(TAG, "stopPreview");
842            mCameraDevice.stopPreview();
843        }
844        mCameraState = PREVIEW_STOPPED;
845    }
846}
847