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