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