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