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