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