PanoramaActivity.java revision d628e27f561c4bedf0d457f64270fc12740b5280
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.net.Uri;
56import android.os.Bundle;
57import android.os.Handler;
58import android.os.Message;
59import android.os.ParcelFileDescriptor;
60import android.util.Log;
61import android.view.Gravity;
62import android.view.Menu;
63import android.view.OrientationEventListener;
64import android.view.View;
65import android.view.ViewGroup;
66import android.view.Window;
67import android.view.WindowManager;
68import android.widget.ImageView;
69import android.widget.TextView;
70
71import java.io.ByteArrayOutputStream;
72import java.io.File;
73import java.util.List;
74
75/**
76 * Activity to handle panorama capturing.
77 */
78public class PanoramaActivity extends ActivityBase implements
79        ModePicker.OnModeChangeListener, SurfaceTexture.OnFrameAvailableListener,
80        ShutterButton.OnShutterButtonListener,
81        MosaicRendererSurfaceViewRenderer.MosaicSurfaceCreateListener {
82    public static final int DEFAULT_SWEEP_ANGLE = 160;
83    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
84    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
85
86    private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1;
87    private static final int MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL = 2;
88    private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 3;
89    private static final int MSG_RESET_TO_PREVIEW = 4;
90    private static final int MSG_CLEAR_SCREEN_DELAY = 5;
91
92    private static final int SCREEN_DELAY = 2 * 60 * 1000;
93
94    private static final String TAG = "PanoramaActivity";
95    private static final int PREVIEW_STOPPED = 0;
96    private static final int PREVIEW_ACTIVE = 1;
97    private static final int CAPTURE_STATE_VIEWFINDER = 0;
98    private static final int CAPTURE_STATE_MOSAIC = 1;
99
100    // Speed is in unit of deg/sec
101    private static final float PANNING_SPEED_THRESHOLD = 20f;
102
103    // Ratio of nanosecond to second
104    private static final float NS2S = 1.0f / 1000000000.0f;
105
106    private boolean mPausing;
107
108    private View mPanoLayout;
109    private View mCaptureLayout;
110    private View mReviewLayout;
111    private ImageView mReview;
112    private RotateLayout mCaptureIndicator;
113    private PanoProgressBar mPanoProgressBar;
114    private PanoProgressBar mSavingProgressBar;
115    private View mFastIndicationBorder;
116    private View mLeftIndicator;
117    private View mRightIndicator;
118    private MosaicRendererSurfaceView mMosaicView;
119    private TextView mTooFastPrompt;
120    private ShutterButton mShutterButton;
121    private Object mWaitObject = new Object();
122
123    private String mPreparePreviewString;
124    private String mDialogTitle;
125    private String mDialogOkString;
126    private String mDialogPanoramaFailedString;
127
128    private int mIndicatorColor;
129    private int mIndicatorColorFast;
130
131    private float mCompassValueX;
132    private float mCompassValueY;
133    private float mCompassValueXStart;
134    private float mCompassValueYStart;
135    private float mCompassValueXStartBuffer;
136    private float mCompassValueYStartBuffer;
137    private int mCompassThreshold;
138    private int mTraversedAngleX;
139    private int mTraversedAngleY;
140    private long mTimestamp;
141    // Control variables for the terminate condition.
142    private int mMinAngleX;
143    private int mMaxAngleX;
144    private int mMinAngleY;
145    private int mMaxAngleY;
146
147    private RotateImageView mThumbnailView;
148    private Thumbnail mThumbnail;
149    private SharePopup mSharePopup;
150
151    private int mPreviewWidth;
152    private int mPreviewHeight;
153    private int mCameraState;
154    private int mCaptureState;
155    private SensorManager mSensorManager;
156    private Sensor mSensor;
157    private ModePicker mModePicker;
158    private MosaicFrameProcessor mMosaicFrameProcessor;
159    private long mTimeTaken;
160    private Handler mMainHandler;
161    private SurfaceTexture mSurfaceTexture;
162    private boolean mThreadRunning;
163    private boolean mCancelComputation;
164    private float[] mTransformMatrix;
165    private float mHorizontalViewAngle;
166
167    // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of
168    // getting a better image quality by the former.
169    private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY;
170
171    private PanoOrientationEventListener mOrientationEventListener;
172    // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise
173    // respectively.
174    private int mDeviceOrientation;
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            mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId());
342        } catch (CameraHardwareException e) {
343            Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
344            return;
345        } catch (CameraDisabledException e) {
346            Util.showErrorAndFinish(this, R.string.camera_disabled);
347            return;
348        }
349    }
350
351    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
352            boolean needSmaller) {
353        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
354        boolean hasFound = false;
355        for (Size size : supportedSizes) {
356            int h = size.height;
357            int w = size.width;
358            // we only want 4:3 format.
359            int d = DEFAULT_CAPTURE_PIXELS - h * w;
360            if (needSmaller && d < 0) { // no bigger preview than 960x720.
361                continue;
362            }
363            if (need4To3 && (h * 4 != w * 3)) {
364                continue;
365            }
366            d = Math.abs(d);
367            if (d < pixelsDiff) {
368                mPreviewWidth = w;
369                mPreviewHeight = h;
370                pixelsDiff = d;
371                hasFound = true;
372            }
373        }
374        return hasFound;
375    }
376
377    private void setupCaptureParams(Parameters parameters) {
378        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
379        if (!findBestPreviewSize(supportedSizes, true, true)) {
380            Log.w(TAG, "No 4:3 ratio preview size supported.");
381            if (!findBestPreviewSize(supportedSizes, false, true)) {
382                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
383                findBestPreviewSize(supportedSizes, false, false);
384            }
385        }
386        Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
387        parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
388
389        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
390        int last = frameRates.size() - 1;
391        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
392        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
393        parameters.setPreviewFpsRange(minFps, maxFps);
394        Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
395
396        List<String> supportedFocusModes = parameters.getSupportedFocusModes();
397        if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) {
398            parameters.setFocusMode(mTargetFocusMode);
399        } else {
400            // Use the default focus mode and log a message
401            Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode +
402                  " becuase the mode is not supported.");
403        }
404
405        parameters.setRecordingHint(false);
406
407        mHorizontalViewAngle = (((mDeviceOrientation / 90) % 2) == 0) ?
408                parameters.getHorizontalViewAngle() : parameters.getVerticalViewAngle();
409    }
410
411    public int getPreviewBufSize() {
412        PixelFormat pixelInfo = new PixelFormat();
413        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
414        // TODO: remove this extra 32 byte after the driver bug is fixed.
415        return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
416    }
417
418    private void configureCamera(Parameters parameters) {
419        mCameraDevice.setParameters(parameters);
420    }
421
422    private boolean switchToOtherMode(int mode) {
423        if (isFinishing()) {
424            return false;
425        }
426        MenuHelper.gotoMode(mode, this);
427        finish();
428        return true;
429    }
430
431    public boolean onModeChanged(int mode) {
432        if (mode != ModePicker.MODE_PANORAMA) {
433            return switchToOtherMode(mode);
434        } else {
435            return true;
436        }
437    }
438
439    @Override
440    public void onMosaicSurfaceChanged() {
441        runOnUiThread(new Runnable() {
442            @Override
443            public void run() {
444                if (!mPausing) {
445                    startCameraPreview();
446                }
447            }
448        });
449    }
450
451    @Override
452    public void onMosaicSurfaceCreated(final int textureID) {
453        runOnUiThread(new Runnable() {
454            @Override
455            public void run() {
456                if (mSurfaceTexture != null) {
457                    mSurfaceTexture.release();
458                }
459                mSurfaceTexture = new SurfaceTexture(textureID);
460                if (!mPausing) {
461                    mSurfaceTexture.setOnFrameAvailableListener(PanoramaActivity.this);
462                }
463            }
464        });
465    }
466
467    public void runViewFinder() {
468        mMosaicView.setWarping(false);
469        // Call preprocess to render it to low-res and high-res RGB textures.
470        mMosaicView.preprocess(mTransformMatrix);
471        mMosaicView.setReady();
472        mMosaicView.requestRender();
473    }
474
475    public void runMosaicCapture() {
476        mMosaicView.setWarping(true);
477        // Call preprocess to render it to low-res and high-res RGB textures.
478        mMosaicView.preprocess(mTransformMatrix);
479        // Lock the conditional variable to ensure the order of transferGPUtoCPU and
480        // mMosaicFrame.processFrame().
481        mMosaicView.lockPreviewReadyFlag();
482        // Now, transfer the textures from GPU to CPU memory for processing
483        mMosaicView.transferGPUtoCPU();
484        // Wait on the condition variable (will be opened when GPU->CPU transfer is done).
485        mMosaicView.waitUntilPreviewReady();
486        mMosaicFrameProcessor.processFrame();
487    }
488
489    public synchronized void onFrameAvailable(SurfaceTexture surface) {
490        /* This function may be called by some random thread,
491         * so let's be safe and use synchronize. No OpenGL calls can be done here.
492         */
493        // Updating the texture should be done in the GL thread which mMosaicView is attached.
494        mMosaicView.queueEvent(new Runnable() {
495            @Override
496            public void run() {
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        mMinAngleX = 0;
544        mMaxAngleX = 0;
545        mMinAngleY = 0;
546        mMaxAngleY = 0;
547        mTimestamp = 0;
548
549        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
550            @Override
551            public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
552                    float progressX, float progressY) {
553                if (isFinished
554                        || (mMaxAngleX - mMinAngleX >= DEFAULT_SWEEP_ANGLE)
555                        || (mMaxAngleY - mMinAngleY >= DEFAULT_SWEEP_ANGLE)) {
556                    stopCapture(false);
557                } else {
558                    updateProgress(panningRateX, progressX, progressY);
559                }
560            }
561        });
562
563        if (mModePicker != null) mModePicker.setEnabled(false);
564
565        mPanoProgressBar.reset();
566        // TODO: calculate the indicator width according to different devices to reflect the actual
567        // angle of view of the camera device.
568        mPanoProgressBar.setIndicatorWidth(20);
569        mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE);
570        mPanoProgressBar.setVisibility(View.VISIBLE);
571        keepScreenOn();
572    }
573
574    private void stopCapture(boolean aborted) {
575        mCaptureState = CAPTURE_STATE_VIEWFINDER;
576        mCaptureIndicator.setVisibility(View.GONE);
577        hideTooFastIndication();
578        hideDirectionIndicators();
579        mThumbnailView.setEnabled(true);
580
581        mMosaicFrameProcessor.setProgressListener(null);
582        stopCameraPreview();
583
584        mSurfaceTexture.setOnFrameAvailableListener(null);
585
586        if (!aborted && !mThreadRunning) {
587            mRotateDialog.showWaitingDialog(mPreparePreviewString);
588            runBackgroundThread(new Thread() {
589                @Override
590                public void run() {
591                    MosaicJpeg jpeg = generateFinalMosaic(false);
592
593                    if (jpeg != null && jpeg.isValid) {
594                        Bitmap bitmap = null;
595                        bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
596                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
597                                MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
598                    } else {
599                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
600                                MSG_RESET_TO_PREVIEW));
601                    }
602                }
603            });
604        }
605        // do we have to wait for the thread to complete before enabling this?
606        if (mModePicker != null) mModePicker.setEnabled(true);
607        keepScreenOnAwhile();
608    }
609
610    private void showTooFastIndication() {
611        mTooFastPrompt.setVisibility(View.VISIBLE);
612        mFastIndicationBorder.setVisibility(View.VISIBLE);
613        mPanoProgressBar.setIndicatorColor(mIndicatorColorFast);
614        mLeftIndicator.setEnabled(true);
615        mRightIndicator.setEnabled(true);
616    }
617
618    private void hideTooFastIndication() {
619        mTooFastPrompt.setVisibility(View.GONE);
620        mFastIndicationBorder.setVisibility(View.GONE);
621        mPanoProgressBar.setIndicatorColor(mIndicatorColor);
622        mLeftIndicator.setEnabled(false);
623        mRightIndicator.setEnabled(false);
624    }
625
626    private void updateProgress(float panningRate, float progressX, float progressY) {
627        mMosaicView.setReady();
628        mMosaicView.requestRender();
629
630        // TODO: Now we just display warning message by the panning speed.
631        // Since we only support horizontal panning, we should display a warning message
632        // in UI when there're significant vertical movements.
633        if (Math.abs(panningRate * mHorizontalViewAngle) > PANNING_SPEED_THRESHOLD) {
634            showTooFastIndication();
635        } else {
636            hideTooFastIndication();
637        }
638        mPanoProgressBar.setProgress((int) (progressX * mHorizontalViewAngle));
639    }
640
641    private void createContentView() {
642        setContentView(R.layout.panorama);
643
644        mCaptureState = CAPTURE_STATE_VIEWFINDER;
645
646        Resources appRes = getResources();
647
648        mCaptureLayout = (View) findViewById(R.id.pano_capture_layout);
649        mPanoProgressBar = (PanoProgressBar) findViewById(R.id.pano_pan_progress_bar);
650        mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
651        mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done));
652        mIndicatorColor = appRes.getColor(R.color.pano_progress_indication);
653        mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast);
654        mPanoProgressBar.setIndicatorColor(mIndicatorColor);
655        mPanoProgressBar.setOnDirectionChangeListener(
656                new PanoProgressBar.OnDirectionChangeListener () {
657                    @Override
658                    public void onDirectionChange(int direction) {
659                        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
660                            showDirectionIndicators(direction);
661                        }
662                    }
663                });
664
665        mLeftIndicator = (ImageView) findViewById(R.id.pano_pan_left_indicator);
666        mRightIndicator = (ImageView) findViewById(R.id.pano_pan_right_indicator);
667        mLeftIndicator.setEnabled(false);
668        mRightIndicator.setEnabled(false);
669        mTooFastPrompt = (TextView) findViewById(R.id.pano_capture_too_fast_textview);
670        mFastIndicationBorder = (View) findViewById(R.id.pano_speed_indication_border);
671
672        mSavingProgressBar = (PanoProgressBar) findViewById(R.id.pano_saving_progress_bar);
673        mSavingProgressBar.setIndicatorWidth(0);
674        mSavingProgressBar.setMaxProgress(100);
675        mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
676        mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication));
677
678        mCaptureIndicator = (RotateLayout) findViewById(R.id.pano_capture_indicator);
679
680        mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail);
681        mThumbnailView.enableFilter(false);
682
683        mReviewLayout = (View) findViewById(R.id.pano_review_layout);
684        mReview = (ImageView) findViewById(R.id.pano_reviewarea);
685        mMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer);
686        mMosaicView.getRenderer().setMosaicSurfaceCreateListener(this);
687
688        mModePicker = (ModePicker) findViewById(R.id.mode_picker);
689        mModePicker.setVisibility(View.VISIBLE);
690        mModePicker.setOnModeChangeListener(this);
691        mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
692
693        mShutterButton = (ShutterButton) findViewById(R.id.shutter_button);
694        mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
695        mShutterButton.setOnShutterButtonListener(this);
696
697        mPanoLayout = findViewById(R.id.pano_layout);
698
699        mRotateDialog = new RotateDialogController(this, R.layout.rotate_dialog);
700
701        if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
702            Rotatable[] rotateLayout = {
703                    (Rotatable) findViewById(R.id.pano_pan_progress_bar_layout),
704                    (Rotatable) findViewById(R.id.pano_capture_too_fast_textview_layout),
705                    (Rotatable) findViewById(R.id.pano_review_saving_indication_layout),
706                    (Rotatable) findViewById(R.id.pano_saving_progress_bar_layout),
707                    (Rotatable) findViewById(R.id.pano_review_cancel_button_layout),
708                    (Rotatable) mRotateDialog,
709                    (Rotatable) mCaptureIndicator,
710                    (Rotatable) mModePicker,
711                    (Rotatable) mThumbnailView};
712            for (Rotatable r : rotateLayout) {
713                r.setOrientation(270);
714            }
715        }
716    }
717
718    @Override
719    public void onShutterButtonClick() {
720        // If mSurfaceTexture == null then GL setup is not finished yet.
721        // No buttons can be pressed.
722        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
723        // Since this button will stay on the screen when capturing, we need to check the state
724        // right now.
725        switch (mCaptureState) {
726            case CAPTURE_STATE_VIEWFINDER:
727                mCameraDevice.playSound(Sound.START_VIDEO_RECORDING);
728                startCapture();
729                break;
730            case CAPTURE_STATE_MOSAIC:
731                mCameraDevice.playSound(Sound.STOP_VIDEO_RECORDING);
732                stopCapture(false);
733        }
734    }
735
736    @Override
737    public void onShutterButtonFocus(boolean pressed) {
738    }
739
740    public void reportProgress() {
741        mSavingProgressBar.reset();
742        mSavingProgressBar.setRightIncreasing(true);
743        Thread t = new Thread() {
744            @Override
745            public void run() {
746                while (mThreadRunning) {
747                    final int progress = mMosaicFrameProcessor.reportProgress(
748                            true, mCancelComputation);
749
750                    try {
751                        synchronized (mWaitObject) {
752                            mWaitObject.wait(50);
753                        }
754                    } catch (InterruptedException e) {
755                        throw new RuntimeException("Panorama reportProgress failed", e);
756                    }
757                    // Update the progress bar
758                    runOnUiThread(new Runnable() {
759                        public void run() {
760                            mSavingProgressBar.setProgress(progress);
761                        }
762                    });
763                }
764            }
765        };
766        t.start();
767    }
768
769    private void initThumbnailButton() {
770        // Load the thumbnail from the disk.
771        if (mThumbnail == null) {
772            mThumbnail = Thumbnail.loadFrom(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
773        }
774        updateThumbnailButton();
775    }
776
777    private void updateThumbnailButton() {
778        // Update last image if URI is invalid and the storage is ready.
779        ContentResolver contentResolver = getContentResolver();
780        if ((mThumbnail == null || !Util.isUriValid(mThumbnail.getUri(), contentResolver))) {
781            mThumbnail = Thumbnail.getLastThumbnail(contentResolver);
782        }
783        if (mThumbnail != null) {
784            mThumbnailView.setBitmap(mThumbnail.getBitmap());
785        } else {
786            mThumbnailView.setBitmap(null);
787        }
788    }
789
790    public void saveHighResMosaic() {
791        runBackgroundThread(new Thread() {
792            @Override
793            public void run() {
794                MosaicJpeg jpeg = generateFinalMosaic(true);
795
796                if (jpeg == null) {  // Cancelled by user.
797                    mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
798                } else if (!jpeg.isValid) {  // Error when generating mosaic.
799                    mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
800                } else {
801                    int orientation = Exif.getOrientation(jpeg.data);
802                    Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
803                    if (uri != null) {
804                        // Create a thumbnail whose width or height is equal or bigger
805                        // than the screen's width or height.
806                        int widthRatio = (int) Math.ceil((double) jpeg.width
807                                / mPanoLayout.getWidth());
808                        int heightRatio = (int) Math.ceil((double) jpeg.height
809                                / mPanoLayout.getHeight());
810                        int inSampleSize = Integer.highestOneBit(
811                                Math.max(widthRatio, heightRatio));
812                        mThumbnail = Thumbnail.createThumbnail(
813                                jpeg.data, orientation, inSampleSize, uri);
814                    }
815                    mMainHandler.sendMessage(
816                            mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL));
817                }
818            }
819        });
820        reportProgress();
821    }
822
823    private void runBackgroundThread(Thread thread) {
824        mThreadRunning = true;
825        thread.start();
826    }
827
828    private void onBackgroundThreadFinished() {
829        mThreadRunning = false;
830        mRotateDialog.dismissDialog();
831    }
832
833    private void cancelHighResComputation() {
834        mCancelComputation = true;
835        synchronized (mWaitObject) {
836            mWaitObject.notify();
837        }
838    }
839
840    @OnClickAttr
841    public void onCancelButtonClicked(View v) {
842        if (mPausing || mSurfaceTexture == null) return;
843        cancelHighResComputation();
844    }
845
846    @OnClickAttr
847    public void onThumbnailClicked(View v) {
848        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
849        showSharePopup();
850    }
851
852    private void showSharePopup() {
853        if (mThumbnail == null) return;
854        Uri uri = mThumbnail.getUri();
855        if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) {
856            // The orientation compensation is set to 0 here because we only support landscape.
857            mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(),
858                    mOrientationCompensation,
859                    findViewById(R.id.frame_layout));
860        }
861        mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0);
862    }
863
864    private void reset() {
865        mCaptureState = CAPTURE_STATE_VIEWFINDER;
866
867        mReviewLayout.setVisibility(View.GONE);
868        mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
869        mPanoProgressBar.setVisibility(View.GONE);
870        mCaptureLayout.setVisibility(View.VISIBLE);
871        mMosaicFrameProcessor.reset();
872
873        mSurfaceTexture.setOnFrameAvailableListener(this);
874    }
875
876    private void resetToPreview() {
877        reset();
878        if (!mPausing) startCameraPreview();
879    }
880
881    private void showFinalMosaic(Bitmap bitmap) {
882        if (bitmap != null) {
883            mReview.setImageBitmap(bitmap);
884        }
885        mCaptureLayout.setVisibility(View.GONE);
886        mReviewLayout.setVisibility(View.VISIBLE);
887    }
888
889    private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) {
890        if (jpegData != null) {
891            String imagePath = PanoUtil.createName(
892                    getResources().getString(R.string.pano_file_name_format), mTimeTaken);
893            return Storage.addImage(getContentResolver(), imagePath, mTimeTaken, null,
894                    orientation, jpegData, width, height);
895        }
896        return null;
897    }
898
899    private void clearMosaicFrameProcessorIfNeeded() {
900        if (!mPausing || mThreadRunning) return;
901        mMosaicFrameProcessor.clear();
902    }
903
904    private void initMosaicFrameProcessorIfNeeded() {
905        if (mPausing || mThreadRunning) return;
906        if (mMosaicFrameProcessor == null) {
907            // Start the activity for the first time.
908            mMosaicFrameProcessor = new MosaicFrameProcessor(
909                    mPreviewWidth, mPreviewHeight, getPreviewBufSize());
910        }
911        mMosaicFrameProcessor.initialize();
912    }
913
914    @Override
915    protected void onPause() {
916        super.onPause();
917
918        mPausing = true;
919        cancelHighResComputation();
920        // Stop the capturing first.
921        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
922            stopCapture(true);
923            reset();
924        }
925        if (mSharePopup != null) mSharePopup.dismiss();
926
927        if (mThumbnail != null && !mThumbnail.fromFile()) {
928            mThumbnail.saveTo(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
929        }
930
931        releaseCamera();
932        mMosaicView.onPause();
933        clearMosaicFrameProcessorIfNeeded();
934        mOrientationEventListener.disable();
935        resetScreenOn();
936        System.gc();
937    }
938
939    @Override
940    protected void doOnResume() {
941        mPausing = false;
942        mOrientationEventListener.enable();
943
944        mCaptureState = CAPTURE_STATE_VIEWFINDER;
945        setupCamera();
946
947        // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size
948        // has to be decided by camera device.
949        initMosaicFrameProcessorIfNeeded();
950        mMosaicView.onResume();
951
952        initThumbnailButton();
953        keepScreenOnAwhile();
954    }
955
956    /**
957     * Generate the final mosaic image.
958     *
959     * @param highRes flag to indicate whether we want to get a high-res version.
960     * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation
961     *         process is cancelled; and a MosaicJpeg with its isValid flag set to false if there
962     *         is an error in generating the final mosaic.
963     */
964    public MosaicJpeg generateFinalMosaic(boolean highRes) {
965        int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes);
966        if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) {
967            return null;
968        } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) {
969            return new MosaicJpeg();
970        }
971
972        byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
973        if (imageData == null) {
974            Log.e(TAG, "getFinalMosaicNV21() returned null.");
975            return new MosaicJpeg();
976        }
977
978        int len = imageData.length - 8;
979        int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
980                + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
981        int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
982                + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
983        Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
984
985        if (width <= 0 || height <= 0) {
986            // TODO: pop up a error meesage indicating that the final result is not generated.
987            Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
988                    height);
989            return new MosaicJpeg();
990        }
991
992        YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
993        ByteArrayOutputStream out = new ByteArrayOutputStream();
994        yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
995        try {
996            out.close();
997        } catch (Exception e) {
998            Log.e(TAG, "Exception in storing final mosaic", e);
999            return new MosaicJpeg();
1000        }
1001        return new MosaicJpeg(out.toByteArray(), width, height);
1002    }
1003
1004    private void setPreviewTexture(SurfaceTexture surface) {
1005        try {
1006            mCameraDevice.setPreviewTexture(surface);
1007        } catch (Throwable ex) {
1008            releaseCamera();
1009            throw new RuntimeException("setPreviewTexture failed", ex);
1010        }
1011    }
1012
1013    private void startCameraPreview() {
1014        // If we're previewing already, stop the preview first (this will blank
1015        // the screen).
1016        if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
1017
1018        // Set the display orientation to 0, so that the underlying mosaic library
1019        // can always get undistorted mPreviewWidth x mPreviewHeight image data
1020        // from SurfaceTexture.
1021        mCameraDevice.setDisplayOrientation(0);
1022
1023        setPreviewTexture(mSurfaceTexture);
1024
1025        try {
1026            Log.v(TAG, "startPreview");
1027            mCameraDevice.startPreview();
1028        } catch (Throwable ex) {
1029            releaseCamera();
1030            throw new RuntimeException("startPreview failed", ex);
1031        }
1032        mCameraState = PREVIEW_ACTIVE;
1033    }
1034
1035    private void stopCameraPreview() {
1036        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
1037            Log.v(TAG, "stopPreview");
1038            mCameraDevice.stopPreview();
1039        }
1040        mCameraState = PREVIEW_STOPPED;
1041    }
1042
1043    @Override
1044    public void onUserInteraction() {
1045        super.onUserInteraction();
1046        if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile();
1047    }
1048
1049    private void resetScreenOn() {
1050        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1051        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1052    }
1053
1054    private void keepScreenOnAwhile() {
1055        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1056        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1057        mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1058    }
1059
1060    private void keepScreenOn() {
1061        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1062        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1063    }
1064}
1065