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