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