PanoramaActivity.java revision 13c101cf7829c98da4341a39ecc142afc024cdb7
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() throws CameraHardwareException, CameraDisabledException {
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() throws CameraHardwareException, CameraDisabledException {
340        int backCameraId = CameraHolder.instance().getBackCameraId();
341        mCameraDevice = Util.openCamera(this, backCameraId);
342        mCameraOrientation = Util.getCameraOrientation(backCameraId);
343    }
344
345    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
346            boolean needSmaller) {
347        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
348        boolean hasFound = false;
349        for (Size size : supportedSizes) {
350            int h = size.height;
351            int w = size.width;
352            // we only want 4:3 format.
353            int d = DEFAULT_CAPTURE_PIXELS - h * w;
354            if (needSmaller && d < 0) { // no bigger preview than 960x720.
355                continue;
356            }
357            if (need4To3 && (h * 4 != w * 3)) {
358                continue;
359            }
360            d = Math.abs(d);
361            if (d < pixelsDiff) {
362                mPreviewWidth = w;
363                mPreviewHeight = h;
364                pixelsDiff = d;
365                hasFound = true;
366            }
367        }
368        return hasFound;
369    }
370
371    private void setupCaptureParams(Parameters parameters) {
372        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
373        if (!findBestPreviewSize(supportedSizes, true, true)) {
374            Log.w(TAG, "No 4:3 ratio preview size supported.");
375            if (!findBestPreviewSize(supportedSizes, false, true)) {
376                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
377                findBestPreviewSize(supportedSizes, false, false);
378            }
379        }
380        Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
381        parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
382
383        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
384        int last = frameRates.size() - 1;
385        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
386        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
387        parameters.setPreviewFpsRange(minFps, maxFps);
388        Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
389
390        List<String> supportedFocusModes = parameters.getSupportedFocusModes();
391        if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) {
392            parameters.setFocusMode(mTargetFocusMode);
393        } else {
394            // Use the default focus mode and log a message
395            Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode +
396                  " becuase the mode is not supported.");
397        }
398
399        parameters.setRecordingHint(false);
400
401        mHorizontalViewAngle = parameters.getHorizontalViewAngle();
402        mVerticalViewAngle =  parameters.getVerticalViewAngle();
403    }
404
405    public int getPreviewBufSize() {
406        PixelFormat pixelInfo = new PixelFormat();
407        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
408        // TODO: remove this extra 32 byte after the driver bug is fixed.
409        return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
410    }
411
412    private void configureCamera(Parameters parameters) {
413        mCameraDevice.setParameters(parameters);
414    }
415
416    private boolean switchToOtherMode(int mode) {
417        if (isFinishing()) {
418            return false;
419        }
420        MenuHelper.gotoMode(mode, this);
421        finish();
422        return true;
423    }
424
425    public boolean onModeChanged(int mode) {
426        if (mode != ModePicker.MODE_PANORAMA) {
427            return switchToOtherMode(mode);
428        } else {
429            return true;
430        }
431    }
432
433    @Override
434    public void onMosaicSurfaceChanged() {
435        runOnUiThread(new Runnable() {
436            @Override
437            public void run() {
438                if (!mPausing) {
439                    startCameraPreview();
440                }
441            }
442        });
443    }
444
445    @Override
446    public void onMosaicSurfaceCreated(final int textureID) {
447        runOnUiThread(new Runnable() {
448            @Override
449            public void run() {
450                if (mSurfaceTexture != null) {
451                    mSurfaceTexture.release();
452                }
453                mSurfaceTexture = new SurfaceTexture(textureID);
454                if (!mPausing) {
455                    mSurfaceTexture.setOnFrameAvailableListener(PanoramaActivity.this);
456                }
457            }
458        });
459    }
460
461    public void runViewFinder() {
462        mMosaicView.setWarping(false);
463        // Call preprocess to render it to low-res and high-res RGB textures.
464        mMosaicView.preprocess(mTransformMatrix);
465        mMosaicView.setReady();
466        mMosaicView.requestRender();
467    }
468
469    public void runMosaicCapture() {
470        mMosaicView.setWarping(true);
471        // Call preprocess to render it to low-res and high-res RGB textures.
472        mMosaicView.preprocess(mTransformMatrix);
473        // Lock the conditional variable to ensure the order of transferGPUtoCPU and
474        // mMosaicFrame.processFrame().
475        mMosaicView.lockPreviewReadyFlag();
476        // Now, transfer the textures from GPU to CPU memory for processing
477        mMosaicView.transferGPUtoCPU();
478        // Wait on the condition variable (will be opened when GPU->CPU transfer is done).
479        mMosaicView.waitUntilPreviewReady();
480        mMosaicFrameProcessor.processFrame();
481    }
482
483    public synchronized void onFrameAvailable(SurfaceTexture surface) {
484        /* This function may be called by some random thread,
485         * so let's be safe and use synchronize. No OpenGL calls can be done here.
486         */
487        // Frames might still be available after the activity is paused. If we call onFrameAvailable
488        // after pausing, the GL thread will crash.
489        if (mPausing) return;
490
491        // Updating the texture should be done in the GL thread which mMosaicView is attached.
492        mMosaicView.queueEvent(new Runnable() {
493            @Override
494            public void run() {
495                // Check if the activity is paused here can speed up the onPause() process.
496                if (mPausing) return;
497                mSurfaceTexture.updateTexImage();
498                mSurfaceTexture.getTransformMatrix(mTransformMatrix);
499            }
500        });
501        // Update the transformation matrix for mosaic pre-process.
502        if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
503            runViewFinder();
504        } else {
505            runMosaicCapture();
506        }
507    }
508
509    private void hideDirectionIndicators() {
510        mLeftIndicator.setVisibility(View.GONE);
511        mRightIndicator.setVisibility(View.GONE);
512    }
513
514    private void showDirectionIndicators(int direction) {
515        switch (direction) {
516            case PanoProgressBar.DIRECTION_NONE:
517                mLeftIndicator.setVisibility(View.VISIBLE);
518                mRightIndicator.setVisibility(View.VISIBLE);
519                break;
520            case PanoProgressBar.DIRECTION_LEFT:
521                mLeftIndicator.setVisibility(View.VISIBLE);
522                mRightIndicator.setVisibility(View.GONE);
523                break;
524            case PanoProgressBar.DIRECTION_RIGHT:
525                mLeftIndicator.setVisibility(View.GONE);
526                mRightIndicator.setVisibility(View.VISIBLE);
527                break;
528        }
529    }
530
531    public void startCapture() {
532        // Reset values so we can do this again.
533        mCancelComputation = false;
534        mTimeTaken = System.currentTimeMillis();
535        mCaptureState = CAPTURE_STATE_MOSAIC;
536        mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan_recording);
537        mCaptureIndicator.setVisibility(View.VISIBLE);
538        showDirectionIndicators(PanoProgressBar.DIRECTION_NONE);
539        mThumbnailView.setEnabled(false);
540
541        mCompassValueXStart = mCompassValueXStartBuffer;
542        mCompassValueYStart = mCompassValueYStartBuffer;
543        mTimestamp = 0;
544
545        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
546            @Override
547            public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
548                    float progressX, float progressY) {
549                float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle;
550                float accumulatedVerticalAngle = progressY * mVerticalViewAngle;
551                if (isFinished
552                        || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE)
553                        || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) {
554                    stopCapture(false);
555                } else {
556                    float panningRateXInDegree = panningRateX * mHorizontalViewAngle;
557                    float panningRateYInDegree = panningRateY * mVerticalViewAngle;
558                    updateProgress(panningRateXInDegree, panningRateYInDegree,
559                            accumulatedHorizontalAngle, accumulatedVerticalAngle);
560                }
561            }
562        });
563
564        if (mModePicker != null) mModePicker.setEnabled(false);
565
566        mPanoProgressBar.reset();
567        // TODO: calculate the indicator width according to different devices to reflect the actual
568        // angle of view of the camera device.
569        mPanoProgressBar.setIndicatorWidth(20);
570        mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE);
571        mPanoProgressBar.setVisibility(View.VISIBLE);
572        mDeviceOrientationAtCapture = mDeviceOrientation;
573        keepScreenOn();
574    }
575
576    private void stopCapture(boolean aborted) {
577        mCaptureState = CAPTURE_STATE_VIEWFINDER;
578        mCaptureIndicator.setVisibility(View.GONE);
579        hideTooFastIndication();
580        hideDirectionIndicators();
581        mThumbnailView.setEnabled(true);
582
583        mMosaicFrameProcessor.setProgressListener(null);
584        stopCameraPreview();
585
586        mSurfaceTexture.setOnFrameAvailableListener(null);
587
588        if (!aborted && !mThreadRunning) {
589            mRotateDialog.showWaitingDialog(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        keepScreenOnAwhile();
610    }
611
612    private void showTooFastIndication() {
613        mTooFastPrompt.setVisibility(View.VISIBLE);
614        mFastIndicationBorder.setVisibility(View.VISIBLE);
615        mPanoProgressBar.setIndicatorColor(mIndicatorColorFast);
616        mLeftIndicator.setEnabled(true);
617        mRightIndicator.setEnabled(true);
618    }
619
620    private void hideTooFastIndication() {
621        mTooFastPrompt.setVisibility(View.GONE);
622        mFastIndicationBorder.setVisibility(View.GONE);
623        mPanoProgressBar.setIndicatorColor(mIndicatorColor);
624        mLeftIndicator.setEnabled(false);
625        mRightIndicator.setEnabled(false);
626    }
627
628    private void updateProgress(float panningRateXInDegree, float panningRateYInDegree,
629            float progressHorizontalAngle, float progressVerticalAngle) {
630        mMosaicView.setReady();
631        mMosaicView.requestRender();
632
633        // TODO: Now we just display warning message by the panning speed.
634        // Since we only support horizontal panning, we should display a warning message
635        // in UI when there're significant vertical movements.
636        if ((Math.abs(panningRateXInDegree) > PANNING_SPEED_THRESHOLD)
637            || (Math.abs(panningRateYInDegree) > PANNING_SPEED_THRESHOLD)) {
638            showTooFastIndication();
639        } else {
640            hideTooFastIndication();
641        }
642        int angleInMajorDirection =
643                (Math.abs(progressHorizontalAngle) > Math.abs(progressVerticalAngle))
644                ? (int) progressHorizontalAngle
645                : (int) progressVerticalAngle;
646        mPanoProgressBar.setProgress((angleInMajorDirection));
647    }
648
649    private void createContentView() {
650        setContentView(R.layout.panorama);
651
652        mCaptureState = CAPTURE_STATE_VIEWFINDER;
653
654        Resources appRes = getResources();
655
656        mCaptureLayout = (View) findViewById(R.id.pano_capture_layout);
657        mPanoProgressBar = (PanoProgressBar) findViewById(R.id.pano_pan_progress_bar);
658        mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
659        mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done));
660        mIndicatorColor = appRes.getColor(R.color.pano_progress_indication);
661        mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast);
662        mPanoProgressBar.setIndicatorColor(mIndicatorColor);
663        mPanoProgressBar.setOnDirectionChangeListener(
664                new PanoProgressBar.OnDirectionChangeListener () {
665                    @Override
666                    public void onDirectionChange(int direction) {
667                        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
668                            showDirectionIndicators(direction);
669                        }
670                    }
671                });
672
673        mLeftIndicator = (ImageView) findViewById(R.id.pano_pan_left_indicator);
674        mRightIndicator = (ImageView) findViewById(R.id.pano_pan_right_indicator);
675        mLeftIndicator.setEnabled(false);
676        mRightIndicator.setEnabled(false);
677        mTooFastPrompt = (TextView) findViewById(R.id.pano_capture_too_fast_textview);
678        mFastIndicationBorder = (View) findViewById(R.id.pano_speed_indication_border);
679
680        mSavingProgressBar = (PanoProgressBar) findViewById(R.id.pano_saving_progress_bar);
681        mSavingProgressBar.setIndicatorWidth(0);
682        mSavingProgressBar.setMaxProgress(100);
683        mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
684        mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication));
685
686        mCaptureIndicator = (RotateLayout) findViewById(R.id.pano_capture_indicator);
687
688        mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail);
689        mThumbnailView.enableFilter(false);
690
691        mReviewLayout = (View) findViewById(R.id.pano_review_layout);
692        mReview = (ImageView) findViewById(R.id.pano_reviewarea);
693        mMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer);
694        mMosaicView.getRenderer().setMosaicSurfaceCreateListener(this);
695
696        mModePicker = (ModePicker) findViewById(R.id.mode_picker);
697        mModePicker.setVisibility(View.VISIBLE);
698        mModePicker.setOnModeChangeListener(this);
699        mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
700
701        mShutterButton = (ShutterButton) findViewById(R.id.shutter_button);
702        mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
703        mShutterButton.setOnShutterButtonListener(this);
704
705        mPanoLayout = findViewById(R.id.pano_layout);
706
707        mRotateDialog = new RotateDialogController(this, R.layout.rotate_dialog);
708
709        if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
710            Rotatable[] rotateLayout = {
711                    (Rotatable) findViewById(R.id.pano_pan_progress_bar_layout),
712                    (Rotatable) findViewById(R.id.pano_capture_too_fast_textview_layout),
713                    (Rotatable) findViewById(R.id.pano_review_saving_indication_layout),
714                    (Rotatable) findViewById(R.id.pano_saving_progress_bar_layout),
715                    (Rotatable) findViewById(R.id.pano_review_cancel_button_layout),
716                    (Rotatable) findViewById(R.id.pano_rotate_reviewarea),
717                    (Rotatable) mRotateDialog,
718                    (Rotatable) mCaptureIndicator,
719                    (Rotatable) mModePicker,
720                    (Rotatable) mThumbnailView};
721            for (Rotatable r : rotateLayout) {
722                r.setOrientation(270);
723            }
724        }
725    }
726
727    @Override
728    public void onShutterButtonClick() {
729        // If mSurfaceTexture == null then GL setup is not finished yet.
730        // No buttons can be pressed.
731        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
732        // Since this button will stay on the screen when capturing, we need to check the state
733        // right now.
734        switch (mCaptureState) {
735            case CAPTURE_STATE_VIEWFINDER:
736                mCameraDevice.playSound(Sound.START_VIDEO_RECORDING);
737                startCapture();
738                break;
739            case CAPTURE_STATE_MOSAIC:
740                mCameraDevice.playSound(Sound.STOP_VIDEO_RECORDING);
741                stopCapture(false);
742        }
743    }
744
745    @Override
746    public void onShutterButtonFocus(boolean pressed) {
747    }
748
749    public void reportProgress() {
750        mSavingProgressBar.reset();
751        mSavingProgressBar.setRightIncreasing(true);
752        Thread t = new Thread() {
753            @Override
754            public void run() {
755                while (mThreadRunning) {
756                    final int progress = mMosaicFrameProcessor.reportProgress(
757                            true, mCancelComputation);
758
759                    try {
760                        synchronized (mWaitObject) {
761                            mWaitObject.wait(50);
762                        }
763                    } catch (InterruptedException e) {
764                        throw new RuntimeException("Panorama reportProgress failed", e);
765                    }
766                    // Update the progress bar
767                    runOnUiThread(new Runnable() {
768                        public void run() {
769                            mSavingProgressBar.setProgress(progress);
770                        }
771                    });
772                }
773            }
774        };
775        t.start();
776    }
777
778    private void initThumbnailButton() {
779        // Load the thumbnail from the disk.
780        if (mThumbnail == null) {
781            mThumbnail = Thumbnail.loadFrom(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
782        }
783        updateThumbnailButton();
784    }
785
786    private void updateThumbnailButton() {
787        // Update last image if URI is invalid and the storage is ready.
788        ContentResolver contentResolver = getContentResolver();
789        if ((mThumbnail == null || !Util.isUriValid(mThumbnail.getUri(), contentResolver))) {
790            mThumbnail = Thumbnail.getLastThumbnail(contentResolver);
791        }
792        if (mThumbnail != null) {
793            mThumbnailView.setBitmap(mThumbnail.getBitmap());
794        } else {
795            mThumbnailView.setBitmap(null);
796        }
797    }
798
799    public void saveHighResMosaic() {
800        runBackgroundThread(new Thread() {
801            @Override
802            public void run() {
803                MosaicJpeg jpeg = generateFinalMosaic(true);
804
805                if (jpeg == null) {  // Cancelled by user.
806                    mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
807                } else if (!jpeg.isValid) {  // Error when generating mosaic.
808                    mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
809                } else {
810                    // The panorama image returned from the library is orientated based on the
811                    // natural orientation of a camera. We need to set an orientation for the image
812                    // in its EXIF header, so the image can be displayed correctly.
813                    // The orientation is calculated from compensating the
814                    // device orientation at capture and the camera orientation respective to
815                    // the natural orientation of the device.
816                    int orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360;
817                    Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
818                    if (uri != null) {
819                        // Create a thumbnail whose width or height is equal or bigger
820                        // than the screen's width or height.
821                        int widthRatio = (int) Math.ceil((double) jpeg.width
822                                / mPanoLayout.getWidth());
823                        int heightRatio = (int) Math.ceil((double) jpeg.height
824                                / mPanoLayout.getHeight());
825                        int inSampleSize = Integer.highestOneBit(
826                                Math.max(widthRatio, heightRatio));
827                        mThumbnail = Thumbnail.createThumbnail(
828                                jpeg.data, orientation, inSampleSize, uri);
829                        Util.broadcastNewPicture(PanoramaActivity.this, uri);
830                    }
831                    mMainHandler.sendMessage(
832                            mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL));
833                }
834            }
835        });
836        reportProgress();
837    }
838
839    private void runBackgroundThread(Thread thread) {
840        mThreadRunning = true;
841        thread.start();
842    }
843
844    private void onBackgroundThreadFinished() {
845        mThreadRunning = false;
846        mRotateDialog.dismissDialog();
847    }
848
849    private void cancelHighResComputation() {
850        mCancelComputation = true;
851        synchronized (mWaitObject) {
852            mWaitObject.notify();
853        }
854    }
855
856    @OnClickAttr
857    public void onCancelButtonClicked(View v) {
858        if (mPausing || mSurfaceTexture == null) return;
859        cancelHighResComputation();
860    }
861
862    @OnClickAttr
863    public void onThumbnailClicked(View v) {
864        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
865        showSharePopup();
866    }
867
868    private void showSharePopup() {
869        if (mThumbnail == null) return;
870        Uri uri = mThumbnail.getUri();
871        if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) {
872            // The orientation compensation is set to 0 here because we only support landscape.
873            mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(),
874                    mOrientationCompensation,
875                    findViewById(R.id.frame_layout));
876        }
877        mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0);
878    }
879
880    private void reset() {
881        mCaptureState = CAPTURE_STATE_VIEWFINDER;
882
883        mReviewLayout.setVisibility(View.GONE);
884        mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
885        mPanoProgressBar.setVisibility(View.GONE);
886        mCaptureLayout.setVisibility(View.VISIBLE);
887        mMosaicFrameProcessor.reset();
888
889        mSurfaceTexture.setOnFrameAvailableListener(this);
890    }
891
892    private void resetToPreview() {
893        reset();
894        if (!mPausing) startCameraPreview();
895    }
896
897    private void showFinalMosaic(Bitmap bitmap) {
898        if (bitmap != null) {
899            mReview.setImageBitmap(bitmap);
900        }
901        mCaptureLayout.setVisibility(View.GONE);
902        mReviewLayout.setVisibility(View.VISIBLE);
903    }
904
905    private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) {
906        if (jpegData != null) {
907            String filename = PanoUtil.createName(
908                    getResources().getString(R.string.pano_file_name_format), mTimeTaken);
909            Uri uri = Storage.addImage(getContentResolver(), filename, mTimeTaken, null,
910                    orientation, jpegData, width, height);
911            if (uri != null && orientation != 0) {
912                String filepath = Storage.generateFilepath(filename);
913                try {
914                    // Save the orientation in EXIF.
915                    ExifInterface exif = new ExifInterface(filepath);
916                    exif.setAttribute(ExifInterface.TAG_ORIENTATION,
917                            getExifOrientation(orientation));
918                    exif.saveAttributes();
919                } catch (IOException e) {
920                    Log.e(TAG, "cannot set exif data: " + filepath);
921                }
922            }
923            return uri;
924        }
925        return null;
926    }
927
928    private static String getExifOrientation(int orientation) {
929        switch (orientation) {
930            case 0:
931                return String.valueOf(ExifInterface.ORIENTATION_NORMAL);
932            case 90:
933                return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90);
934            case 180:
935                return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180);
936            case 270:
937                return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270);
938            default:
939                throw new AssertionError("invalid: " + orientation);
940        }
941    }
942
943    private void clearMosaicFrameProcessorIfNeeded() {
944        if (!mPausing || mThreadRunning) return;
945        mMosaicFrameProcessor.clear();
946    }
947
948    private void initMosaicFrameProcessorIfNeeded() {
949        if (mPausing || mThreadRunning) return;
950        if (mMosaicFrameProcessor == null) {
951            // Start the activity for the first time.
952            mMosaicFrameProcessor = new MosaicFrameProcessor(
953                    mPreviewWidth, mPreviewHeight, getPreviewBufSize());
954        }
955        mMosaicFrameProcessor.initialize();
956    }
957
958    @Override
959    protected void onPause() {
960        super.onPause();
961
962        mPausing = true;
963        cancelHighResComputation();
964        // Stop the capturing first.
965        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
966            stopCapture(true);
967            reset();
968        }
969        if (mSharePopup != null) mSharePopup.dismiss();
970
971        if (mThumbnail != null && !mThumbnail.fromFile()) {
972            mThumbnail.saveTo(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
973        }
974
975        releaseCamera();
976        mMosaicView.onPause();
977        clearMosaicFrameProcessorIfNeeded();
978        mOrientationEventListener.disable();
979        resetScreenOn();
980        System.gc();
981    }
982
983    @Override
984    protected void doOnResume() {
985        mPausing = false;
986        mOrientationEventListener.enable();
987
988        mCaptureState = CAPTURE_STATE_VIEWFINDER;
989        try {
990            setupCamera();
991
992            // Camera must be initialized before MosaicFrameProcessor is initialized.
993            // The preview size has to be decided by camera device.
994            initMosaicFrameProcessorIfNeeded();
995            mMosaicView.onResume();
996
997            initThumbnailButton();
998            keepScreenOnAwhile();
999        } catch (CameraHardwareException e) {
1000            Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
1001        } catch (CameraDisabledException e) {
1002            Util.showErrorAndFinish(this, R.string.camera_disabled);
1003        }
1004    }
1005
1006    /**
1007     * Generate the final mosaic image.
1008     *
1009     * @param highRes flag to indicate whether we want to get a high-res version.
1010     * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation
1011     *         process is cancelled; and a MosaicJpeg with its isValid flag set to false if there
1012     *         is an error in generating the final mosaic.
1013     */
1014    public MosaicJpeg generateFinalMosaic(boolean highRes) {
1015        int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes);
1016        if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) {
1017            return null;
1018        } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) {
1019            return new MosaicJpeg();
1020        }
1021
1022        byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
1023        if (imageData == null) {
1024            Log.e(TAG, "getFinalMosaicNV21() returned null.");
1025            return new MosaicJpeg();
1026        }
1027
1028        int len = imageData.length - 8;
1029        int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
1030                + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
1031        int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
1032                + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
1033        Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
1034
1035        if (width <= 0 || height <= 0) {
1036            // TODO: pop up a error meesage indicating that the final result is not generated.
1037            Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
1038                    height);
1039            return new MosaicJpeg();
1040        }
1041
1042        YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
1043        ByteArrayOutputStream out = new ByteArrayOutputStream();
1044        yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
1045        try {
1046            out.close();
1047        } catch (Exception e) {
1048            Log.e(TAG, "Exception in storing final mosaic", e);
1049            return new MosaicJpeg();
1050        }
1051        return new MosaicJpeg(out.toByteArray(), width, height);
1052    }
1053
1054    private void setPreviewTexture(SurfaceTexture surface) {
1055        try {
1056            mCameraDevice.setPreviewTexture(surface);
1057        } catch (Throwable ex) {
1058            releaseCamera();
1059            throw new RuntimeException("setPreviewTexture failed", ex);
1060        }
1061    }
1062
1063    private void startCameraPreview() {
1064        // If we're previewing already, stop the preview first (this will blank
1065        // the screen).
1066        if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
1067
1068        // Set the display orientation to 0, so that the underlying mosaic library
1069        // can always get undistorted mPreviewWidth x mPreviewHeight image data
1070        // from SurfaceTexture.
1071        mCameraDevice.setDisplayOrientation(0);
1072
1073        setPreviewTexture(mSurfaceTexture);
1074
1075        try {
1076            Log.v(TAG, "startPreview");
1077            mCameraDevice.startPreview();
1078        } catch (Throwable ex) {
1079            releaseCamera();
1080            throw new RuntimeException("startPreview failed", ex);
1081        }
1082        mCameraState = PREVIEW_ACTIVE;
1083    }
1084
1085    private void stopCameraPreview() {
1086        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
1087            Log.v(TAG, "stopPreview");
1088            mCameraDevice.stopPreview();
1089        }
1090        mCameraState = PREVIEW_STOPPED;
1091    }
1092
1093    @Override
1094    public void onUserInteraction() {
1095        super.onUserInteraction();
1096        if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile();
1097    }
1098
1099    private void resetScreenOn() {
1100        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1101        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1102    }
1103
1104    private void keepScreenOnAwhile() {
1105        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1106        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1107        mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1108    }
1109
1110    private void keepScreenOn() {
1111        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1112        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1113    }
1114}
1115