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