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