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