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