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