PanoramaActivity.java revision 90a21dd768fae18d670a338a72679e4c77ee1170
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.CameraDisabledException;
20import com.android.camera.CameraHardwareException;
21import com.android.camera.CameraHolder;
22import com.android.camera.Exif;
23import com.android.camera.MenuHelper;
24import com.android.camera.ModePicker;
25import com.android.camera.OnClickAttr;
26import com.android.camera.R;
27import com.android.camera.ShutterButton;
28import com.android.camera.Storage;
29import com.android.camera.Thumbnail;
30import com.android.camera.Util;
31import com.android.camera.ui.RotateImageView;
32import com.android.camera.ui.SharePopup;
33
34import android.animation.Animator;
35import android.animation.AnimatorInflater;
36import android.animation.ObjectAnimator;
37import android.app.Activity;
38import android.app.AlertDialog;
39import android.app.ProgressDialog;
40import android.content.Context;
41import android.content.DialogInterface;
42import android.graphics.Bitmap;
43import android.graphics.BitmapFactory;
44import android.graphics.ImageFormat;
45import android.graphics.PixelFormat;
46import android.graphics.Rect;
47import android.graphics.SurfaceTexture;
48import android.graphics.YuvImage;
49import android.hardware.Camera;
50import android.hardware.Camera.Parameters;
51import android.hardware.Camera.Size;
52import android.hardware.Sensor;
53import android.hardware.SensorEvent;
54import android.hardware.SensorEventListener;
55import android.hardware.SensorManager;
56import android.net.Uri;
57import android.os.Bundle;
58import android.os.Handler;
59import android.os.Message;
60import android.util.Log;
61import android.view.Gravity;
62import android.view.OrientationEventListener;
63import android.view.View;
64import android.view.WindowManager;
65import android.widget.ImageView;
66import android.widget.RelativeLayout;
67import android.widget.TextView;
68
69import java.io.ByteArrayOutputStream;
70import java.util.List;
71
72/**
73 * Activity to handle panorama capturing.
74 */
75public class PanoramaActivity extends Activity implements
76        ModePicker.OnModeChangeListener, SurfaceTexture.OnFrameAvailableListener,
77        ShutterButton.OnShutterButtonListener,
78        MosaicRendererSurfaceViewRenderer.MosaicSurfaceCreateListener {
79    public static final int DEFAULT_SWEEP_ANGLE = 160;
80    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
81    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
82
83    private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1;
84    private static final int MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL = 2;
85    private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 3;
86    private static final int MSG_DISMISS_ALERT_DIALOG_AND_RESET_TO_PREVIEW = 4;
87
88    private static final String TAG = "PanoramaActivity";
89    private static final int PREVIEW_STOPPED = 0;
90    private static final int PREVIEW_ACTIVE = 1;
91    private static final int CAPTURE_STATE_VIEWFINDER = 0;
92    private static final int CAPTURE_STATE_MOSAIC = 1;
93
94    // Speed is in unit of deg/sec
95    private static final float PANNING_SPEED_THRESHOLD = 20f;
96
97    // Ratio of nanosecond to second
98    private static final float NS2S = 1.0f / 1000000000.0f;
99
100    private boolean mPausing;
101
102    private View mPanoLayout;
103    private View mCaptureLayout;
104    private View mReviewLayout;
105    private ImageView mReview;
106    private IndicationView mIndicationView;
107    private MosaicRendererSurfaceView mMosaicView;
108    private TextView mTooFastPrompt;
109    private ShutterButton mShutterButton;
110
111    private ProgressDialog mProgressDialog;
112    private String mPreparePreviewString;
113    private String mGeneratePanoramaString;
114    private AlertDialog mAlertDialog;
115    private String mDialogTitle;
116    private String mDialogOk;
117
118    // This custom dialog is to follow the UI spec to produce a dialog with a spinner in the top
119    // center part and a text view in the bottom part. The background is a rounded rectangle. The
120    // system dialog cannot be used because there will be a rectangle with 3D-like edges.
121    private RelativeLayout mPanoramaPrepareDialog;
122    private Animator mPanoramaPrepareDialogFadeIn;
123    private Animator mPanoramaPrepareDialogFadeOut;
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 float[] mTransformMatrix;
159    private float mHorizontalViewAngle;
160
161    private PanoOrientationEventListener mOrientationEventListener;
162    // The value could be 0, 1, 2, 3 for the 4 different orientations measured in clockwise
163    // respectively.
164    private int mDeviceOrientation;
165
166    private class MosaicJpeg {
167        public MosaicJpeg(byte[] data, int width, int height) {
168            this.data = data;
169            this.width = width;
170            this.height = height;
171        }
172
173        public final byte[] data;
174        public final int width;
175        public final int height;
176    }
177
178    private class PanoOrientationEventListener extends OrientationEventListener {
179        public PanoOrientationEventListener(Context context) {
180            super(context);
181        }
182
183        @Override
184        public void onOrientationChanged(int orientation) {
185            // Default to the last known orientation.
186            if (orientation == ORIENTATION_UNKNOWN) return;
187            mDeviceOrientation = ((orientation + 45) / 90) % 4;
188        }
189    }
190
191    @Override
192    public void onCreate(Bundle icicle) {
193        super.onCreate(icicle);
194
195        getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
196                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
197        Util.enterLightsOutMode(getWindow());
198
199        createContentView();
200
201        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
202        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
203        if (mSensor == null) {
204            mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
205        }
206
207        mOrientationEventListener = new PanoOrientationEventListener(this);
208
209        mTransformMatrix = new float[16];
210
211        mPreparePreviewString =
212                getResources().getString(R.string.pano_dialog_prepare_preview);
213        mGeneratePanoramaString =
214                getResources().getString(R.string.pano_dialog_generate_panorama);
215        mDialogTitle = getResources().getString(R.string.pano_dialog_title);
216        mDialogOk = getResources().getString(R.string.dialog_ok);
217
218        mMainHandler = new Handler() {
219            @Override
220            public void handleMessage(Message msg) {
221                switch (msg.what) {
222                    case MSG_LOW_RES_FINAL_MOSAIC_READY:
223                        onBackgroundThreadFinished();
224                        showFinalMosaic((Bitmap) msg.obj);
225                        break;
226                    case MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL:
227                        onBackgroundThreadFinished();
228                        // Set the thumbnail bitmap here because mThumbnailView must be accessed
229                        // from the UI thread.
230                        if (mThumbnail != null) {
231                            mThumbnailView.setBitmap(mThumbnail.getBitmap());
232                        }
233                        resetToPreview();
234                        break;
235                    case MSG_GENERATE_FINAL_MOSAIC_ERROR:
236                        onBackgroundThreadFinished();
237                        mAlertDialog = (new AlertDialog.Builder(PanoramaActivity.this))
238                                .setTitle(mDialogTitle)
239                                .setMessage(R.string.pano_dialog_panorama_failed)
240                                .create();
241                        mAlertDialog.setButton(DialogInterface.BUTTON_POSITIVE, mDialogOk,
242                                obtainMessage(MSG_DISMISS_ALERT_DIALOG_AND_RESET_TO_PREVIEW));
243                        mAlertDialog.show();
244                        break;
245                    case MSG_DISMISS_ALERT_DIALOG_AND_RESET_TO_PREVIEW:
246                        mAlertDialog.dismiss();
247                        mAlertDialog = null;
248                        resetToPreview();
249                        break;
250                }
251                clearMosaicFrameProcessorIfNeeded();
252            }
253        };
254    }
255
256    private void setupCamera() {
257        openCamera();
258        Parameters parameters = mCameraDevice.getParameters();
259        setupCaptureParams(parameters);
260        configureCamera(parameters);
261    }
262
263    private void releaseCamera() {
264        if (mCameraDevice != null) {
265            mCameraDevice.setPreviewCallbackWithBuffer(null);
266            CameraHolder.instance().release();
267            mCameraDevice = null;
268            mCameraState = PREVIEW_STOPPED;
269        }
270    }
271
272    private void openCamera() {
273        try {
274            mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId());
275        } catch (CameraHardwareException e) {
276            Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
277            return;
278        } catch (CameraDisabledException e) {
279            Util.showErrorAndFinish(this, R.string.camera_disabled);
280            return;
281        }
282    }
283
284    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
285            boolean needSmaller) {
286        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
287        boolean hasFound = false;
288        for (Size size : supportedSizes) {
289            int h = size.height;
290            int w = size.width;
291            // we only want 4:3 format.
292            int d = DEFAULT_CAPTURE_PIXELS - h * w;
293            if (needSmaller && d < 0) { // no bigger preview than 960x720.
294                continue;
295            }
296            if (need4To3 && (h * 4 != w * 3)) {
297                continue;
298            }
299            d = Math.abs(d);
300            if (d < pixelsDiff) {
301                mPreviewWidth = w;
302                mPreviewHeight = h;
303                pixelsDiff = d;
304                hasFound = true;
305            }
306        }
307        return hasFound;
308    }
309
310    private void setupCaptureParams(Parameters parameters) {
311        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
312        if (!findBestPreviewSize(supportedSizes, true, true)) {
313            Log.w(TAG, "No 4:3 ratio preview size supported.");
314            if (!findBestPreviewSize(supportedSizes, false, true)) {
315                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
316                findBestPreviewSize(supportedSizes, false, false);
317            }
318        }
319        Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
320        parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
321
322        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
323        int last = frameRates.size() - 1;
324        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
325        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
326        parameters.setPreviewFpsRange(minFps, maxFps);
327        Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
328
329        parameters.setRecordingHint(false);
330
331        mHorizontalViewAngle = ((mDeviceOrientation % 2) == 0) ?
332                parameters.getHorizontalViewAngle() : parameters.getVerticalViewAngle();
333    }
334
335    public int getPreviewBufSize() {
336        PixelFormat pixelInfo = new PixelFormat();
337        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
338        // TODO: remove this extra 32 byte after the driver bug is fixed.
339        return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
340    }
341
342    private void configureCamera(Parameters parameters) {
343        mCameraDevice.setParameters(parameters);
344
345        int orientation = Util.getDisplayOrientation(Util.getDisplayRotation(this),
346                CameraHolder.instance().getBackCameraId());
347        mCameraDevice.setDisplayOrientation(orientation);
348    }
349
350    private boolean switchToOtherMode(int mode) {
351        if (isFinishing()) {
352            return false;
353        }
354        MenuHelper.gotoMode(mode, this);
355        finish();
356        return true;
357    }
358
359    public boolean onModeChanged(int mode) {
360        if (mode != ModePicker.MODE_PANORAMA) {
361            return switchToOtherMode(mode);
362        } else {
363            return true;
364        }
365    }
366
367    @Override
368    public void onMosaicSurfaceCreated(final int textureID) {
369        runOnUiThread(new Runnable() {
370            @Override
371            public void run() {
372                if (mSurfaceTexture != null) {
373                    mSurfaceTexture.release();
374                }
375                mSurfaceTexture = new SurfaceTexture(textureID);
376                if (!mPausing) {
377                    mSurfaceTexture.setOnFrameAvailableListener(PanoramaActivity.this);
378                    startCameraPreview();
379                }
380            }
381        });
382    }
383
384    public void runViewFinder() {
385        mMosaicView.setWarping(false);
386        // Call preprocess to render it to low-res and high-res RGB textures.
387        mMosaicView.preprocess(mTransformMatrix);
388        mMosaicView.setReady();
389        mMosaicView.requestRender();
390    }
391
392    public void runMosaicCapture() {
393        mMosaicView.setWarping(true);
394        // Call preprocess to render it to low-res and high-res RGB textures.
395        mMosaicView.preprocess(mTransformMatrix);
396        // Lock the conditional variable to ensure the order of transferGPUtoCPU and
397        // mMosaicFrame.processFrame().
398        mMosaicView.lockPreviewReadyFlag();
399        // Now, transfer the textures from GPU to CPU memory for processing
400        mMosaicView.transferGPUtoCPU();
401        // Wait on the condition variable (will be opened when GPU->CPU transfer is done).
402        mMosaicView.waitUntilPreviewReady();
403        mMosaicFrameProcessor.processFrame();
404    }
405
406    public synchronized void onFrameAvailable(SurfaceTexture surface) {
407        /* This function may be called by some random thread,
408         * so let's be safe and use synchronize. No OpenGL calls can be done here.
409         */
410        // Updating the texture should be done in the GL thread which mMosaicView is attached.
411        mMosaicView.queueEvent(new Runnable() {
412            @Override
413            public void run() {
414                mSurfaceTexture.updateTexImage();
415                mSurfaceTexture.getTransformMatrix(mTransformMatrix);
416            }
417        });
418        // Update the transformation matrix for mosaic pre-process.
419        if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
420            runViewFinder();
421        } else {
422            runMosaicCapture();
423        }
424    }
425
426    public void startCapture() {
427        // Reset values so we can do this again.
428        mTimeTaken = System.currentTimeMillis();
429        mCaptureState = CAPTURE_STATE_MOSAIC;
430        mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan_recording);
431
432        mCompassValueXStart = mCompassValueXStartBuffer;
433        mCompassValueYStart = mCompassValueYStartBuffer;
434        mMinAngleX = 0;
435        mMaxAngleX = 0;
436        mMinAngleY = 0;
437        mMaxAngleY = 0;
438        mTimestamp = 0;
439
440        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
441            @Override
442            public void onProgress(boolean isFinished, float panningRateX, float panningRateY) {
443                if (isFinished
444                        || (mMaxAngleX - mMinAngleX >= DEFAULT_SWEEP_ANGLE)
445                        || (mMaxAngleY - mMinAngleY >= DEFAULT_SWEEP_ANGLE)) {
446                    stopCapture();
447                } else {
448                    updateProgress(panningRateX);
449                }
450            }
451        });
452
453        mIndicationView.resetAngles();
454        mIndicationView.setVisibility(View.VISIBLE);
455        mMosaicView.setVisibility(View.VISIBLE);
456    }
457
458    private void stopCapture() {
459        mCaptureState = CAPTURE_STATE_VIEWFINDER;
460        mTooFastPrompt.setVisibility(View.GONE);
461
462        mMosaicFrameProcessor.setProgressListener(null);
463        stopCameraPreview();
464
465        mSurfaceTexture.setOnFrameAvailableListener(null);
466
467        if (!mThreadRunning) {
468            runBackgroundThreadAndShowDialog(mPreparePreviewString, false, new Thread() {
469                @Override
470                public void run() {
471                    MosaicJpeg jpeg = generateFinalMosaic(false);
472                    Bitmap bitmap = null;
473                    if (jpeg != null) {
474                        bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
475                    }
476                    mMainHandler.sendMessage(mMainHandler.obtainMessage(
477                            MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
478                }
479            });
480            reportProgress(false);
481        }
482    }
483
484    private void updateProgress(float panningRate) {
485        mMosaicView.setReady();
486        mMosaicView.requestRender();
487
488        // TODO: Now we just display warning message by the panning speed.
489        // Since we only support horizontal panning, we should display a warning message
490        // in UI when there're significant vertical movements.
491        if (Math.abs(panningRate * mHorizontalViewAngle) > PANNING_SPEED_THRESHOLD) {
492            // TODO: draw speed indication according to the UI spec.
493            mTooFastPrompt.setVisibility(View.VISIBLE);
494            mTooFastPrompt.invalidate();
495        } else {
496            mTooFastPrompt.setVisibility(View.GONE);
497            mTooFastPrompt.invalidate();
498        }
499    }
500
501    private void createContentView() {
502        setContentView(R.layout.panorama);
503
504        mCaptureState = CAPTURE_STATE_VIEWFINDER;
505
506        mCaptureLayout = (View) findViewById(R.id.pano_capture_layout);
507        mIndicationView = (IndicationView) findViewById(R.id.pano_capture_view);
508        mIndicationView.setMaxSweepAngle(DEFAULT_SWEEP_ANGLE);
509        mTooFastPrompt = (TextView) findViewById(R.id.pano_capture_too_fast_textview);
510
511        mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail);
512
513        mReviewLayout = (View) findViewById(R.id.pano_review_layout);
514        mReview = (ImageView) findViewById(R.id.pano_reviewarea);
515        mMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer);
516        mMosaicView.getRenderer().setMosaicSurfaceCreateListener(this);
517        mMosaicView.setVisibility(View.VISIBLE);
518
519        mModePicker = (ModePicker) findViewById(R.id.mode_picker);
520        mModePicker.setVisibility(View.VISIBLE);
521        mModePicker.setOnModeChangeListener(this);
522        mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
523
524        mShutterButton = (ShutterButton) findViewById(R.id.shutter_button);
525        mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
526        mShutterButton.setOnShutterButtonListener(this);
527
528        mPanoramaPrepareDialog = (RelativeLayout)
529                findViewById(R.id.pano_preview_progress_dialog);
530
531        mPanoramaPrepareDialogFadeIn = AnimatorInflater.loadAnimator(this, R.anim.fade_in_quick);
532        mPanoramaPrepareDialogFadeIn.setTarget(mPanoramaPrepareDialog);
533        mPanoramaPrepareDialogFadeOut = AnimatorInflater.loadAnimator(this, R.anim.fade_out_quick);
534        mPanoramaPrepareDialogFadeOut.setTarget(mPanoramaPrepareDialog);
535
536        mPanoLayout = findViewById(R.id.pano_layout);
537    }
538
539    @Override
540    public void onShutterButtonClick(ShutterButton b) {
541        // If mSurfaceTexture == null then GL setup is not finished yet.
542        // No buttons can be pressed.
543        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
544        // Since this button will stay on the screen when capturing, we need to check the state
545        // right now.
546        switch (mCaptureState) {
547            case CAPTURE_STATE_VIEWFINDER:
548                startCapture();
549                break;
550            case CAPTURE_STATE_MOSAIC:
551                stopCapture();
552        }
553    }
554
555    @Override
556    public void onShutterButtonFocus(ShutterButton b, boolean pressed) {
557    }
558
559    public void reportProgress(final boolean highRes) {
560        Thread t = new Thread() {
561            @Override
562            public void run() {
563                while (mThreadRunning) {
564                    final int progress = mMosaicFrameProcessor.reportProgress(highRes);
565
566                    try {
567                        Thread.sleep(50);
568                    } catch (Exception e) {
569                        throw new RuntimeException("Panorama reportProgress failed", e);
570                    }
571                    // Update the progress bar
572                    runOnUiThread(new Runnable() {
573                        public void run() {
574                            // Check if mProgressDialog is null because the background thread
575                            // finished.
576                            if (mProgressDialog != null) {
577                                mProgressDialog.setProgress(progress);
578                            }
579                        }
580                    });
581                }
582            }
583        };
584        t.start();
585    }
586
587    @OnClickAttr
588    public void onOkButtonClicked(View v) {
589        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
590        runBackgroundThreadAndShowDialog(mGeneratePanoramaString, true, new Thread() {
591            @Override
592            public void run() {
593                MosaicJpeg jpeg = generateFinalMosaic(true);
594                if (jpeg == null) {
595                    mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
596                } else {
597                    int orientation = Exif.getOrientation(jpeg.data);
598                    Uri uri = savePanorama(jpeg.data, orientation);
599                    if (uri != null) {
600                        // Create a thumbnail whose width is equal or bigger than the entire screen.
601                        int ratio = (int) Math.ceil((double) jpeg.width / mPanoLayout.getWidth());
602                        int inSampleSize = Integer.highestOneBit(ratio);
603                        mThumbnail = Thumbnail.createThumbnail(
604                                jpeg.data, orientation, inSampleSize, uri);
605                    }
606                    mMainHandler.sendMessage(
607                            mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL));
608                }
609            }
610        });
611        reportProgress(true);
612    }
613
614    /**
615     * If the style is horizontal one, the maximum progress is assumed to be 100.
616     */
617    private void runBackgroundThreadAndShowDialog(
618            String str, boolean showPercentageProgress, Thread thread) {
619        mThreadRunning = true;
620        if (showPercentageProgress) {
621            mProgressDialog = new ProgressDialog(this);
622            mProgressDialog.setMax(100);
623            mProgressDialog.setMessage(str);
624            mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
625            mProgressDialog.setCancelable(false);  // Don't allow back key to dismiss this dialog.
626            mProgressDialog.show();
627        } else {
628            mPanoramaPrepareDialogFadeIn.start();
629            mPanoramaPrepareDialog.setVisibility(View.VISIBLE);
630        }
631        thread.start();
632    }
633
634    private void onBackgroundThreadFinished() {
635        mThreadRunning = false;
636        if (mProgressDialog != null) {
637            mProgressDialog.dismiss();
638            mProgressDialog = null;
639        }
640        if (mPanoramaPrepareDialog.getVisibility() == View.VISIBLE) {
641            mPanoramaPrepareDialogFadeOut.start();
642            mPanoramaPrepareDialog.setVisibility(View.GONE);
643        }
644    }
645
646    @OnClickAttr
647    public void onRetakeButtonClicked(View v) {
648        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
649        resetToPreview();
650    }
651
652    @OnClickAttr
653    public void onThumbnailClicked(View v) {
654        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
655        showSharePopup();
656    }
657
658    private void showSharePopup() {
659        if (mThumbnail == null) return;
660        Uri uri = mThumbnail.getUri();
661        if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) {
662            // The orientation compensation is set to 0 here because we only support landscape.
663            // Panorama picture is long. Use pano_layout so the share popup can be full-screen.
664            mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(), 0,
665                    findViewById(R.id.pano_layout));
666        }
667        mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0);
668    }
669
670    private void resetToPreview() {
671        mCaptureState = CAPTURE_STATE_VIEWFINDER;
672
673        mReviewLayout.setVisibility(View.GONE);
674        mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
675        mIndicationView.setVisibility(View.GONE);
676        mCaptureLayout.setVisibility(View.VISIBLE);
677        mMosaicFrameProcessor.reset();
678
679        mSurfaceTexture.setOnFrameAvailableListener(this);
680
681        if (!mPausing) startCameraPreview();
682
683        mMosaicView.setVisibility(View.VISIBLE);
684    }
685
686    private void showFinalMosaic(Bitmap bitmap) {
687        if (bitmap != null) {
688            mReview.setImageBitmap(bitmap);
689        }
690        mCaptureLayout.setVisibility(View.GONE);
691        mReviewLayout.setVisibility(View.VISIBLE);
692    }
693
694    private Uri savePanorama(byte[] jpegData, int orientation) {
695        if (jpegData != null) {
696            String imagePath = PanoUtil.createName(
697                    getResources().getString(R.string.pano_file_name_format), mTimeTaken);
698            return Storage.addImage(getContentResolver(), imagePath, mTimeTaken, null,
699                    orientation, jpegData);
700        }
701        return null;
702    }
703
704    private void clearMosaicFrameProcessorIfNeeded() {
705        if (!mPausing || mThreadRunning) return;
706        mMosaicFrameProcessor.clear();
707    }
708
709    private void initMosaicFrameProcessorIfNeeded() {
710        if (mPausing || mThreadRunning) return;
711        if (mMosaicFrameProcessor == null) {
712            // Start the activity for the first time.
713            mMosaicFrameProcessor = new MosaicFrameProcessor(
714                    mPreviewWidth, mPreviewHeight, getPreviewBufSize());
715        }
716        mMosaicFrameProcessor.initialize();
717    }
718
719    @Override
720    protected void onPause() {
721        super.onPause();
722
723        releaseCamera();
724        mPausing = true;
725        mMosaicView.onPause();
726        mSensorManager.unregisterListener(mListener);
727        clearMosaicFrameProcessorIfNeeded();
728        mOrientationEventListener.disable();
729        System.gc();
730    }
731
732    @Override
733    protected void onResume() {
734        super.onResume();
735
736        mPausing = false;
737        mOrientationEventListener.enable();
738        /*
739         * It is not necessary to get accelerometer events at a very high rate,
740         * by using a game rate (SENSOR_DELAY_UI), we get an automatic
741         * low-pass filter, which "extracts" the gravity component of the
742         * acceleration. As an added benefit, we use less power and CPU
743         * resources.
744         */
745        mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI);
746        mCaptureState = CAPTURE_STATE_VIEWFINDER;
747        setupCamera();
748        if (mSurfaceTexture != null) {
749            mSurfaceTexture.setOnFrameAvailableListener(this);
750            startCameraPreview();
751        }
752        // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size
753        // has to be decided by camera device.
754        initMosaicFrameProcessorIfNeeded();
755        mMosaicView.onResume();
756    }
757
758    private void updateCompassValue() {
759        // By what angle has the camera moved since start of capture?
760        mTraversedAngleX = (int) (mCompassValueX - mCompassValueXStart);
761        mTraversedAngleY = (int) (mCompassValueY - mCompassValueYStart);
762        mMinAngleX = Math.min(mMinAngleX, mTraversedAngleX);
763        mMaxAngleX = Math.max(mMaxAngleX, mTraversedAngleX);
764        mMinAngleY = Math.min(mMinAngleY, mTraversedAngleY);
765        mMaxAngleY = Math.max(mMaxAngleY, mTraversedAngleY);
766
767        // Use orientation to identify if the user is panning to the right or the left.
768        switch (mDeviceOrientation) {
769            case 0:
770                mIndicationView.setSweepAngle(-mTraversedAngleX);
771                break;
772            case 1:
773                mIndicationView.setSweepAngle(mTraversedAngleY);
774                break;
775            case 2:
776                mIndicationView.setSweepAngle(mTraversedAngleX);
777                break;
778            case 3:
779                mIndicationView.setSweepAngle(-mTraversedAngleY);
780                break;
781        }
782        mIndicationView.invalidate();
783    }
784
785    private final SensorEventListener mListener = new SensorEventListener() {
786        public void onSensorChanged(SensorEvent event) {
787            if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
788                if (mTimestamp != 0) {
789                    final float dT = (event.timestamp - mTimestamp) * NS2S;
790                    mCompassValueX += event.values[1] * dT * 180.0f / Math.PI;
791                    mCompassValueY += event.values[0] * dT * 180.0f / Math.PI;
792                    mCompassValueXStartBuffer = mCompassValueX;
793                    mCompassValueYStartBuffer = mCompassValueY;
794                    updateCompassValue();
795                }
796                mTimestamp = event.timestamp;
797
798            }
799        }
800
801        @Override
802        public void onAccuracyChanged(Sensor sensor, int accuracy) {
803        }
804    };
805
806    public MosaicJpeg generateFinalMosaic(boolean highRes) {
807        mMosaicFrameProcessor.createMosaic(highRes);
808
809        byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
810        if (imageData == null) {
811            Log.e(TAG, "getFinalMosaicNV21() returned null.");
812            return null;
813        }
814
815        int len = imageData.length - 8;
816        int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
817                + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
818        int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
819                + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
820        Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
821
822        if (width <= 0 || height <= 0) {
823            // TODO: pop up a error meesage indicating that the final result is not generated.
824            Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
825                    height);
826            return null;
827        }
828
829        YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
830        ByteArrayOutputStream out = new ByteArrayOutputStream();
831        yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
832        try {
833            out.close();
834        } catch (Exception e) {
835            Log.e(TAG, "Exception in storing final mosaic", e);
836            return null;
837        }
838        return new MosaicJpeg(out.toByteArray(), width, height);
839    }
840
841    private void setPreviewTexture(SurfaceTexture surface) {
842        try {
843            mCameraDevice.setPreviewTexture(surface);
844        } catch (Throwable ex) {
845            releaseCamera();
846            throw new RuntimeException("setPreviewTexture failed", ex);
847        }
848    }
849
850    private void startCameraPreview() {
851        // If we're previewing already, stop the preview first (this will blank
852        // the screen).
853        if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
854
855        setPreviewTexture(mSurfaceTexture);
856
857        try {
858            Log.v(TAG, "startPreview");
859            mCameraDevice.startPreview();
860        } catch (Throwable ex) {
861            releaseCamera();
862            throw new RuntimeException("startPreview failed", ex);
863        }
864        mCameraState = PREVIEW_ACTIVE;
865    }
866
867    private void stopCameraPreview() {
868        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
869            Log.v(TAG, "stopPreview");
870            mCameraDevice.stopPreview();
871        }
872        mCameraState = PREVIEW_STOPPED;
873    }
874}
875