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