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