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