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