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