PanoramaActivity.java revision 3a3fd8f6cf7c939cacbd3da09e80d3df074a00e4
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.ActivityBase;
20import com.android.camera.CameraDisabledException;
21import com.android.camera.CameraHardwareException;
22import com.android.camera.CameraHolder;
23import com.android.camera.Exif;
24import com.android.camera.MenuHelper;
25import com.android.camera.ModePicker;
26import com.android.camera.OnClickAttr;
27import com.android.camera.R;
28import com.android.camera.ShutterButton;
29import com.android.camera.SoundPlayer;
30import com.android.camera.Storage;
31import com.android.camera.Thumbnail;
32import com.android.camera.Util;
33import com.android.camera.ui.Rotatable;
34import com.android.camera.ui.RotateImageView;
35import com.android.camera.ui.RotateLayout;
36import com.android.camera.ui.SharePopup;
37
38import android.app.Dialog;
39import android.app.ProgressDialog;
40import android.content.ContentResolver;
41import android.content.Context;
42import android.content.DialogInterface;
43import android.content.res.AssetFileDescriptor;
44import android.content.pm.ActivityInfo;
45import android.content.res.Resources;
46import android.graphics.Bitmap;
47import android.graphics.BitmapFactory;
48import android.graphics.ImageFormat;
49import android.graphics.PixelFormat;
50import android.graphics.Rect;
51import android.graphics.SurfaceTexture;
52import android.graphics.YuvImage;
53import android.hardware.Camera;
54import android.hardware.Camera.Parameters;
55import android.hardware.Camera.Size;
56import android.hardware.Sensor;
57import android.hardware.SensorManager;
58import android.net.Uri;
59import android.os.Bundle;
60import android.os.Handler;
61import android.os.Message;
62import android.os.ParcelFileDescriptor;
63import android.os.SystemProperties;
64import android.util.Log;
65import android.view.Gravity;
66import android.view.LayoutInflater;
67import android.view.Menu;
68import android.view.OrientationEventListener;
69import android.view.View;
70import android.view.ViewGroup;
71import android.view.Window;
72import android.view.WindowManager;
73import android.widget.Button;
74import android.widget.ImageView;
75import android.widget.ProgressBar;
76import android.widget.TextView;
77
78import java.io.ByteArrayOutputStream;
79import java.io.FileNotFoundException;
80import java.io.File;
81import java.util.List;
82import java.util.Vector;
83
84/**
85 * Activity to handle panorama capturing.
86 */
87public class PanoramaActivity extends ActivityBase implements
88        ModePicker.OnModeChangeListener, SurfaceTexture.OnFrameAvailableListener,
89        ShutterButton.OnShutterButtonListener,
90        MosaicRendererSurfaceViewRenderer.MosaicSurfaceCreateListener {
91    public static final int DEFAULT_SWEEP_ANGLE = 160;
92    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
93    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
94
95    private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1;
96    private static final int MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL = 2;
97    private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 3;
98    private static final int MSG_RESET_TO_PREVIEW = 4;
99    private static final int MSG_CLEAR_SCREEN_DELAY = 5;
100
101    private static final int SCREEN_DELAY = 2 * 60 * 1000;
102
103    private static final String TAG = "PanoramaActivity";
104    private static final int PREVIEW_STOPPED = 0;
105    private static final int PREVIEW_ACTIVE = 1;
106    private static final int CAPTURE_STATE_VIEWFINDER = 0;
107    private static final int CAPTURE_STATE_MOSAIC = 1;
108
109    // Speed is in unit of deg/sec
110    private static final float PANNING_SPEED_THRESHOLD = 20f;
111
112    // Ratio of nanosecond to second
113    private static final float NS2S = 1.0f / 1000000000.0f;
114
115    private static final String VIDEO_RECORD_SOUND = "/system/media/audio/ui/VideoRecord.ogg";
116
117    private boolean mPausing;
118
119    private View mPanoLayout;
120    private View mCaptureLayout;
121    private View mReviewLayout;
122    private ImageView mReview;
123    private RotateLayout mCaptureIndicator;
124    private PanoProgressBar mPanoProgressBar;
125    private PanoProgressBar mSavingProgressBar;
126    private View mFastIndicationBorder;
127    private View mLeftIndicator;
128    private View mRightIndicator;
129    private MosaicRendererSurfaceView mMosaicView;
130    private TextView mTooFastPrompt;
131    private ShutterButton mShutterButton;
132    private Object mWaitObject = new Object();
133
134    private RotateLayout mRotateDialog;
135    private View mRotateDialogTitleLayout;
136    private View mRotateDialogButtonLayout;
137    private TextView mRotateDialogTitle;
138    private View mRotateDialogTitleSeperator;
139    private ProgressBar mRotateDialogSpinner;
140    private TextView mRotateDialogText;
141    private TextView mRotateDialogButton;
142
143    private String mPreparePreviewString;
144    private String mDialogTitle;
145    private String mDialogOkString;
146    private String mDialogPanoramaFailedString;
147
148    private int mIndicatorColor;
149    private int mIndicatorColorFast;
150
151    private float mCompassValueX;
152    private float mCompassValueY;
153    private float mCompassValueXStart;
154    private float mCompassValueYStart;
155    private float mCompassValueXStartBuffer;
156    private float mCompassValueYStartBuffer;
157    private int mCompassThreshold;
158    private int mTraversedAngleX;
159    private int mTraversedAngleY;
160    private long mTimestamp;
161    // Control variables for the terminate condition.
162    private int mMinAngleX;
163    private int mMaxAngleX;
164    private int mMinAngleY;
165    private int mMaxAngleY;
166
167    private RotateImageView mThumbnailView;
168    private Thumbnail mThumbnail;
169    private SharePopup mSharePopup;
170
171    private int mPreviewWidth;
172    private int mPreviewHeight;
173    private int mCameraState;
174    private int mCaptureState;
175    private SensorManager mSensorManager;
176    private Sensor mSensor;
177    private ModePicker mModePicker;
178    private MosaicFrameProcessor mMosaicFrameProcessor;
179    private long mTimeTaken;
180    private Handler mMainHandler;
181    private SurfaceTexture mSurfaceTexture;
182    private boolean mThreadRunning;
183    private boolean mCancelComputation;
184    private float[] mTransformMatrix;
185    private float mHorizontalViewAngle;
186
187    private SoundPlayer mRecordSound;
188
189    // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of
190    // getting a better image quality by the former.
191    private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY;
192
193    private PanoOrientationEventListener mOrientationEventListener;
194    // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise
195    // respectively.
196    private int mDeviceOrientation;
197    private int mOrientationCompensation;
198
199    private class MosaicJpeg {
200        public MosaicJpeg(byte[] data, int width, int height) {
201            this.data = data;
202            this.width = width;
203            this.height = height;
204            this.isValid = true;
205        }
206
207        public MosaicJpeg() {
208            this.data = null;
209            this.width = 0;
210            this.height = 0;
211            this.isValid = false;
212        }
213
214        public final byte[] data;
215        public final int width;
216        public final int height;
217        public final boolean isValid;
218    }
219
220    private class PanoOrientationEventListener extends OrientationEventListener {
221        public PanoOrientationEventListener(Context context) {
222            super(context);
223        }
224
225        @Override
226        public void onOrientationChanged(int orientation) {
227            // We keep the last known orientation. So if the user first orient
228            // the camera then point the camera to floor or sky, we still have
229            // the correct orientation.
230            if (orientation == ORIENTATION_UNKNOWN) return;
231            mDeviceOrientation = Util.roundOrientation(orientation, mDeviceOrientation);
232            // When the screen is unlocked, display rotation may change. Always
233            // calculate the up-to-date orientationCompensation.
234            int orientationCompensation = mDeviceOrientation
235                    + Util.getDisplayRotation(PanoramaActivity.this);
236            if (mOrientationCompensation != orientationCompensation) {
237                mOrientationCompensation = orientationCompensation;
238                setOrientationIndicator(mOrientationCompensation);
239            }
240        }
241    }
242
243    private void setOrientationIndicator(int degree) {
244        if (mSharePopup != null) mSharePopup.setOrientation(degree);
245    }
246
247    @Override
248    public boolean onCreateOptionsMenu(Menu menu) {
249        super.onCreateOptionsMenu(menu);
250
251        addBaseMenuItems(menu);
252        return true;
253    }
254
255    private void addBaseMenuItems(Menu menu) {
256        MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_CAMERA, new Runnable() {
257            public void run() {
258                switchToOtherMode(ModePicker.MODE_CAMERA);
259            }
260        });
261        MenuHelper.addSwitchModeMenuItem(menu, ModePicker.MODE_VIDEO, new Runnable() {
262            public void run() {
263                switchToOtherMode(ModePicker.MODE_VIDEO);
264            }
265        });
266    }
267
268    @Override
269    public void onCreate(Bundle icicle) {
270        super.onCreate(icicle);
271
272        Window window = getWindow();
273        Util.enterLightsOutMode(window);
274        Util.initializeScreenBrightness(window, getContentResolver());
275
276        createContentView();
277
278        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
279        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
280        if (mSensor == null) {
281            mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
282        }
283
284        mOrientationEventListener = new PanoOrientationEventListener(this);
285
286        mTransformMatrix = new float[16];
287
288        mPreparePreviewString =
289                getResources().getString(R.string.pano_dialog_prepare_preview);
290        mDialogTitle = getResources().getString(R.string.pano_dialog_title);
291        mDialogOkString = getResources().getString(R.string.dialog_ok);
292        mDialogPanoramaFailedString =
293                getResources().getString(R.string.pano_dialog_panorama_failed);
294
295        mMainHandler = new Handler() {
296            @Override
297            public void handleMessage(Message msg) {
298                switch (msg.what) {
299                    case MSG_LOW_RES_FINAL_MOSAIC_READY:
300                        onBackgroundThreadFinished();
301                        showFinalMosaic((Bitmap) msg.obj);
302                        saveHighResMosaic();
303                        break;
304                    case MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL:
305                        onBackgroundThreadFinished();
306                        // Set the thumbnail bitmap here because mThumbnailView must be accessed
307                        // from the UI thread.
308                        updateThumbnailButton();
309
310                        // Share popup may still have the reference to the old thumbnail. Clear it.
311                        mSharePopup = null;
312                        resetToPreview();
313                        break;
314                    case MSG_GENERATE_FINAL_MOSAIC_ERROR:
315                        onBackgroundThreadFinished();
316                        if (mPausing) {
317                            resetToPreview();
318                        } else {
319                            showAlertDialog(
320                                    mDialogTitle, mDialogPanoramaFailedString, mDialogOkString);
321                        }
322                        break;
323                    case MSG_RESET_TO_PREVIEW:
324                        onBackgroundThreadFinished();
325                        resetToPreview();
326                        break;
327                    case MSG_CLEAR_SCREEN_DELAY:
328                        getWindow().clearFlags(WindowManager.LayoutParams.
329                                FLAG_KEEP_SCREEN_ON);
330                        break;
331                }
332                clearMosaicFrameProcessorIfNeeded();
333            }
334        };
335    }
336
337    @OnClickAttr
338    public void onAlertDialogButtonClicked(View v) {
339        dismissDialog();
340        resetToPreview();
341    }
342
343    private void setupCamera() {
344        openCamera();
345        Parameters parameters = mCameraDevice.getParameters();
346        setupCaptureParams(parameters);
347        configureCamera(parameters);
348    }
349
350    private void releaseCamera() {
351        if (mCameraDevice != null) {
352            mCameraDevice.setPreviewCallbackWithBuffer(null);
353            CameraHolder.instance().release();
354            mCameraDevice = null;
355            mCameraState = PREVIEW_STOPPED;
356        }
357    }
358
359    private void openCamera() {
360        try {
361            mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId());
362        } catch (CameraHardwareException e) {
363            Util.showErrorAndFinish(this, R.string.cannot_connect_camera);
364            return;
365        } catch (CameraDisabledException e) {
366            Util.showErrorAndFinish(this, R.string.camera_disabled);
367            return;
368        }
369    }
370
371    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
372            boolean needSmaller) {
373        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
374        boolean hasFound = false;
375        for (Size size : supportedSizes) {
376            int h = size.height;
377            int w = size.width;
378            // we only want 4:3 format.
379            int d = DEFAULT_CAPTURE_PIXELS - h * w;
380            if (needSmaller && d < 0) { // no bigger preview than 960x720.
381                continue;
382            }
383            if (need4To3 && (h * 4 != w * 3)) {
384                continue;
385            }
386            d = Math.abs(d);
387            if (d < pixelsDiff) {
388                mPreviewWidth = w;
389                mPreviewHeight = h;
390                pixelsDiff = d;
391                hasFound = true;
392            }
393        }
394        return hasFound;
395    }
396
397    private void setupCaptureParams(Parameters parameters) {
398        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
399        if (!findBestPreviewSize(supportedSizes, true, true)) {
400            Log.w(TAG, "No 4:3 ratio preview size supported.");
401            if (!findBestPreviewSize(supportedSizes, false, true)) {
402                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
403                findBestPreviewSize(supportedSizes, false, false);
404            }
405        }
406        Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
407        parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
408
409        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
410        int last = frameRates.size() - 1;
411        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
412        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
413        parameters.setPreviewFpsRange(minFps, maxFps);
414        Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
415
416        List<String> supportedFocusModes = parameters.getSupportedFocusModes();
417        if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) {
418            parameters.setFocusMode(mTargetFocusMode);
419        } else {
420            // Use the default focus mode and log a message
421            Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode +
422                  " becuase the mode is not supported.");
423        }
424
425        parameters.setRecordingHint(false);
426
427        mHorizontalViewAngle = (((mDeviceOrientation / 90) % 2) == 0) ?
428                parameters.getHorizontalViewAngle() : parameters.getVerticalViewAngle();
429    }
430
431    public int getPreviewBufSize() {
432        PixelFormat pixelInfo = new PixelFormat();
433        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
434        // TODO: remove this extra 32 byte after the driver bug is fixed.
435        return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
436    }
437
438    private void configureCamera(Parameters parameters) {
439        mCameraDevice.setParameters(parameters);
440    }
441
442    private boolean switchToOtherMode(int mode) {
443        if (isFinishing()) {
444            return false;
445        }
446        MenuHelper.gotoMode(mode, this);
447        finish();
448        return true;
449    }
450
451    public boolean onModeChanged(int mode) {
452        if (mode != ModePicker.MODE_PANORAMA) {
453            return switchToOtherMode(mode);
454        } else {
455            return true;
456        }
457    }
458
459    @Override
460    public void onMosaicSurfaceChanged() {
461        runOnUiThread(new Runnable() {
462            @Override
463            public void run() {
464                if (!mPausing) {
465                    startCameraPreview();
466                }
467            }
468        });
469    }
470
471    @Override
472    public void onMosaicSurfaceCreated(final int textureID) {
473        runOnUiThread(new Runnable() {
474            @Override
475            public void run() {
476                if (mSurfaceTexture != null) {
477                    mSurfaceTexture.release();
478                }
479                mSurfaceTexture = new SurfaceTexture(textureID);
480                if (!mPausing) {
481                    mSurfaceTexture.setOnFrameAvailableListener(PanoramaActivity.this);
482                }
483            }
484        });
485    }
486
487    public void runViewFinder() {
488        mMosaicView.setWarping(false);
489        // Call preprocess to render it to low-res and high-res RGB textures.
490        mMosaicView.preprocess(mTransformMatrix);
491        mMosaicView.setReady();
492        mMosaicView.requestRender();
493    }
494
495    public void runMosaicCapture() {
496        mMosaicView.setWarping(true);
497        // Call preprocess to render it to low-res and high-res RGB textures.
498        mMosaicView.preprocess(mTransformMatrix);
499        // Lock the conditional variable to ensure the order of transferGPUtoCPU and
500        // mMosaicFrame.processFrame().
501        mMosaicView.lockPreviewReadyFlag();
502        // Now, transfer the textures from GPU to CPU memory for processing
503        mMosaicView.transferGPUtoCPU();
504        // Wait on the condition variable (will be opened when GPU->CPU transfer is done).
505        mMosaicView.waitUntilPreviewReady();
506        mMosaicFrameProcessor.processFrame();
507    }
508
509    public synchronized void onFrameAvailable(SurfaceTexture surface) {
510        /* This function may be called by some random thread,
511         * so let's be safe and use synchronize. No OpenGL calls can be done here.
512         */
513        // Updating the texture should be done in the GL thread which mMosaicView is attached.
514        mMosaicView.queueEvent(new Runnable() {
515            @Override
516            public void run() {
517                mSurfaceTexture.updateTexImage();
518                mSurfaceTexture.getTransformMatrix(mTransformMatrix);
519            }
520        });
521        // Update the transformation matrix for mosaic pre-process.
522        if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
523            runViewFinder();
524        } else {
525            runMosaicCapture();
526        }
527    }
528
529    private void hideDirectionIndicators() {
530        mLeftIndicator.setVisibility(View.GONE);
531        mRightIndicator.setVisibility(View.GONE);
532    }
533
534    private void showDirectionIndicators(int direction) {
535        switch (direction) {
536            case PanoProgressBar.DIRECTION_NONE:
537                mLeftIndicator.setVisibility(View.VISIBLE);
538                mRightIndicator.setVisibility(View.VISIBLE);
539                break;
540            case PanoProgressBar.DIRECTION_LEFT:
541                mLeftIndicator.setVisibility(View.VISIBLE);
542                mRightIndicator.setVisibility(View.GONE);
543                break;
544            case PanoProgressBar.DIRECTION_RIGHT:
545                mLeftIndicator.setVisibility(View.GONE);
546                mRightIndicator.setVisibility(View.VISIBLE);
547                break;
548        }
549    }
550
551    public void startCapture() {
552        // Reset values so we can do this again.
553        mCancelComputation = false;
554        mTimeTaken = System.currentTimeMillis();
555        mCaptureState = CAPTURE_STATE_MOSAIC;
556        mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan_recording);
557        mCaptureIndicator.setVisibility(View.VISIBLE);
558        showDirectionIndicators(PanoProgressBar.DIRECTION_NONE);
559        mThumbnailView.setEnabled(false);
560
561        mCompassValueXStart = mCompassValueXStartBuffer;
562        mCompassValueYStart = mCompassValueYStartBuffer;
563        mMinAngleX = 0;
564        mMaxAngleX = 0;
565        mMinAngleY = 0;
566        mMaxAngleY = 0;
567        mTimestamp = 0;
568
569        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
570            @Override
571            public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
572                    float progressX, float progressY) {
573                if (isFinished
574                        || (mMaxAngleX - mMinAngleX >= DEFAULT_SWEEP_ANGLE)
575                        || (mMaxAngleY - mMinAngleY >= DEFAULT_SWEEP_ANGLE)) {
576                    stopCapture(false);
577                } else {
578                    updateProgress(panningRateX, progressX, progressY);
579                }
580            }
581        });
582
583        if (mModePicker != null) mModePicker.setEnabled(false);
584
585        mPanoProgressBar.reset();
586        // TODO: calculate the indicator width according to different devices to reflect the actual
587        // angle of view of the camera device.
588        mPanoProgressBar.setIndicatorWidth(20);
589        mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE);
590        mPanoProgressBar.setVisibility(View.VISIBLE);
591        keepScreenOn();
592    }
593
594    private void stopCapture(boolean aborted) {
595        mCaptureState = CAPTURE_STATE_VIEWFINDER;
596        mCaptureIndicator.setVisibility(View.GONE);
597        hideTooFastIndication();
598        hideDirectionIndicators();
599        mThumbnailView.setEnabled(true);
600
601        mMosaicFrameProcessor.setProgressListener(null);
602        stopCameraPreview();
603
604        mSurfaceTexture.setOnFrameAvailableListener(null);
605
606        if (!aborted && !mThreadRunning) {
607            showWaitingDialog(mPreparePreviewString);
608            runBackgroundThread(new Thread() {
609                @Override
610                public void run() {
611                    MosaicJpeg jpeg = generateFinalMosaic(false);
612
613                    if (jpeg != null && jpeg.isValid) {
614                        Bitmap bitmap = null;
615                        bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
616                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
617                                MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
618                    } else {
619                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
620                                MSG_RESET_TO_PREVIEW));
621                    }
622                }
623            });
624        }
625        // do we have to wait for the thread to complete before enabling this?
626        if (mModePicker != null) mModePicker.setEnabled(true);
627        keepScreenOnAwhile();
628    }
629
630    private void showTooFastIndication() {
631        mTooFastPrompt.setVisibility(View.VISIBLE);
632        mFastIndicationBorder.setVisibility(View.VISIBLE);
633        mPanoProgressBar.setIndicatorColor(mIndicatorColorFast);
634        mLeftIndicator.setEnabled(true);
635        mRightIndicator.setEnabled(true);
636    }
637
638    private void hideTooFastIndication() {
639        mTooFastPrompt.setVisibility(View.GONE);
640        mFastIndicationBorder.setVisibility(View.GONE);
641        mPanoProgressBar.setIndicatorColor(mIndicatorColor);
642        mLeftIndicator.setEnabled(false);
643        mRightIndicator.setEnabled(false);
644    }
645
646    private void updateProgress(float panningRate, float progressX, float progressY) {
647        mMosaicView.setReady();
648        mMosaicView.requestRender();
649
650        // TODO: Now we just display warning message by the panning speed.
651        // Since we only support horizontal panning, we should display a warning message
652        // in UI when there're significant vertical movements.
653        if (Math.abs(panningRate * mHorizontalViewAngle) > PANNING_SPEED_THRESHOLD) {
654            showTooFastIndication();
655        } else {
656            hideTooFastIndication();
657        }
658        mPanoProgressBar.setProgress((int) (progressX * mHorizontalViewAngle));
659    }
660
661    private void createContentView() {
662        setContentView(R.layout.panorama);
663
664        mCaptureState = CAPTURE_STATE_VIEWFINDER;
665
666        Resources appRes = getResources();
667
668        mCaptureLayout = (View) findViewById(R.id.pano_capture_layout);
669        mPanoProgressBar = (PanoProgressBar) findViewById(R.id.pano_pan_progress_bar);
670        mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
671        mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done));
672        mIndicatorColor = appRes.getColor(R.color.pano_progress_indication);
673        mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast);
674        mPanoProgressBar.setIndicatorColor(mIndicatorColor);
675        mPanoProgressBar.setOnDirectionChangeListener(
676                new PanoProgressBar.OnDirectionChangeListener () {
677                    @Override
678                    public void onDirectionChange(int direction) {
679                        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
680                            showDirectionIndicators(direction);
681                        }
682                    }
683                });
684
685        mLeftIndicator = (ImageView) findViewById(R.id.pano_pan_left_indicator);
686        mRightIndicator = (ImageView) findViewById(R.id.pano_pan_right_indicator);
687        mLeftIndicator.setEnabled(false);
688        mRightIndicator.setEnabled(false);
689        mTooFastPrompt = (TextView) findViewById(R.id.pano_capture_too_fast_textview);
690        mFastIndicationBorder = (View) findViewById(R.id.pano_speed_indication_border);
691
692        mSavingProgressBar = (PanoProgressBar) findViewById(R.id.pano_saving_progress_bar);
693        mSavingProgressBar.setIndicatorWidth(0);
694        mSavingProgressBar.setMaxProgress(100);
695        mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
696        mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication));
697
698        mCaptureIndicator = (RotateLayout) findViewById(R.id.pano_capture_indicator);
699
700        mThumbnailView = (RotateImageView) findViewById(R.id.thumbnail);
701        mThumbnailView.enableFilter(false);
702
703        mReviewLayout = (View) findViewById(R.id.pano_review_layout);
704        mReview = (ImageView) findViewById(R.id.pano_reviewarea);
705        mMosaicView = (MosaicRendererSurfaceView) findViewById(R.id.pano_renderer);
706        mMosaicView.getRenderer().setMosaicSurfaceCreateListener(this);
707
708        mModePicker = (ModePicker) findViewById(R.id.mode_picker);
709        mModePicker.setVisibility(View.VISIBLE);
710        mModePicker.setOnModeChangeListener(this);
711        mModePicker.setCurrentMode(ModePicker.MODE_PANORAMA);
712
713        mShutterButton = (ShutterButton) findViewById(R.id.shutter_button);
714        mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
715        mShutterButton.setOnShutterButtonListener(this);
716
717        mPanoLayout = findViewById(R.id.pano_layout);
718
719        mRotateDialog = (RotateLayout) findViewById(R.id.rotate_dialog_layout);
720        mRotateDialogTitleLayout = findViewById(R.id.rotate_dialog_title_layout);
721        mRotateDialogButtonLayout = findViewById(R.id.rotate_dialog_button_layout);
722        mRotateDialogTitle = (TextView) findViewById(R.id.rotate_dialog_title);
723        mRotateDialogTitleSeperator = (View) findViewById(R.id.rotate_dialog_title_divider);
724        mRotateDialogSpinner = (ProgressBar) findViewById(R.id.rotate_dialog_spinner);
725        mRotateDialogText = (TextView) findViewById(R.id.rotate_dialog_text);
726        mRotateDialogButton = (Button) findViewById(R.id.rotate_dialog_button);
727
728        if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
729            Rotatable[] rotateLayout = {
730                    (Rotatable) findViewById(R.id.pano_pan_progress_bar_layout),
731                    (Rotatable) findViewById(R.id.pano_capture_too_fast_textview_layout),
732                    (Rotatable) findViewById(R.id.pano_review_saving_indication_layout),
733                    (Rotatable) findViewById(R.id.pano_saving_progress_bar_layout),
734                    (Rotatable) findViewById(R.id.pano_review_cancel_button_layout),
735                    (Rotatable) mRotateDialog,
736                    (Rotatable) mCaptureIndicator,
737                    (Rotatable) mModePicker,
738                    (Rotatable) mThumbnailView};
739            for (Rotatable r : rotateLayout) {
740                r.setOrientation(270);
741            }
742        }
743    }
744
745    @Override
746    public void onShutterButtonClick() {
747        // If mSurfaceTexture == null then GL setup is not finished yet.
748        // No buttons can be pressed.
749        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
750        // Since this button will stay on the screen when capturing, we need to check the state
751        // right now.
752        switch (mCaptureState) {
753            case CAPTURE_STATE_VIEWFINDER:
754                if (mRecordSound != null) mRecordSound.play();
755                startCapture();
756                break;
757            case CAPTURE_STATE_MOSAIC:
758                if (mRecordSound != null) mRecordSound.play();
759                stopCapture(false);
760        }
761    }
762
763    @Override
764    public void onShutterButtonFocus(boolean pressed) {
765    }
766
767    public void reportProgress() {
768        mSavingProgressBar.reset();
769        mSavingProgressBar.setRightIncreasing(true);
770        Thread t = new Thread() {
771            @Override
772            public void run() {
773                while (mThreadRunning) {
774                    final int progress = mMosaicFrameProcessor.reportProgress(
775                            true, mCancelComputation);
776
777                    try {
778                        synchronized (mWaitObject) {
779                            mWaitObject.wait(50);
780                        }
781                    } catch (InterruptedException e) {
782                        throw new RuntimeException("Panorama reportProgress failed", e);
783                    }
784                    // Update the progress bar
785                    runOnUiThread(new Runnable() {
786                        public void run() {
787                            mSavingProgressBar.setProgress(progress);
788                        }
789                    });
790                }
791            }
792        };
793        t.start();
794    }
795
796    private void initThumbnailButton() {
797        // Load the thumbnail from the disk.
798        if (mThumbnail == null) {
799            mThumbnail = Thumbnail.loadFrom(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
800        }
801        updateThumbnailButton();
802    }
803
804    private void updateThumbnailButton() {
805        // Update last image if URI is invalid and the storage is ready.
806        ContentResolver contentResolver = getContentResolver();
807        if ((mThumbnail == null || !Util.isUriValid(mThumbnail.getUri(), contentResolver))) {
808            mThumbnail = Thumbnail.getLastThumbnail(contentResolver);
809        }
810        if (mThumbnail != null) {
811            mThumbnailView.setBitmap(mThumbnail.getBitmap());
812        } else {
813            mThumbnailView.setBitmap(null);
814        }
815    }
816
817    public void saveHighResMosaic() {
818        runBackgroundThread(new Thread() {
819            @Override
820            public void run() {
821                MosaicJpeg jpeg = generateFinalMosaic(true);
822
823                if (jpeg == null) {  // Cancelled by user.
824                    mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
825                } else if (!jpeg.isValid) {  // Error when generating mosaic.
826                    mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
827                } else {
828                    int orientation = Exif.getOrientation(jpeg.data);
829                    Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
830                    if (uri != null) {
831                        // Create a thumbnail whose width or height is equal or bigger
832                        // than the screen's width or height.
833                        int widthRatio = (int) Math.ceil((double) jpeg.width
834                                / mPanoLayout.getWidth());
835                        int heightRatio = (int) Math.ceil((double) jpeg.height
836                                / mPanoLayout.getHeight());
837                        int inSampleSize = Integer.highestOneBit(
838                                Math.max(widthRatio, heightRatio));
839                        mThumbnail = Thumbnail.createThumbnail(
840                                jpeg.data, orientation, inSampleSize, uri);
841                    }
842                    mMainHandler.sendMessage(
843                            mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW_WITH_THUMBNAIL));
844                }
845            }
846        });
847        reportProgress();
848    }
849
850    private void resetRotateDialog() {
851        mRotateDialogTitleLayout.setVisibility(View.GONE);
852        mRotateDialogSpinner.setVisibility(View.GONE);
853        mRotateDialogButtonLayout.setVisibility(View.GONE);
854    }
855
856    private void dismissDialog() {
857        if (mRotateDialog.getVisibility() != View.INVISIBLE) {
858            mRotateDialog.setVisibility(View.INVISIBLE);
859        }
860    }
861
862    private void showAlertDialog(String title, String msg, String buttonText) {
863        resetRotateDialog();
864
865        mRotateDialogTitle.setText(title);
866        mRotateDialogTitleLayout.setVisibility(View.VISIBLE);
867
868        mRotateDialogText.setText(msg);
869
870        mRotateDialogButton.setText(buttonText);
871        mRotateDialogButtonLayout.setVisibility(View.VISIBLE);
872
873        mRotateDialog.setVisibility(View.VISIBLE);
874    }
875
876    private void showWaitingDialog(String msg) {
877        resetRotateDialog();
878
879        mRotateDialogText.setText(msg);
880        mRotateDialogSpinner.setVisibility(View.VISIBLE);
881
882        mRotateDialog.setVisibility(View.VISIBLE);
883    }
884
885    private void runBackgroundThread(Thread thread) {
886        mThreadRunning = true;
887        thread.start();
888    }
889
890    private void onBackgroundThreadFinished() {
891        mThreadRunning = false;
892        dismissDialog();
893    }
894
895    private void cancelHighResComputation() {
896        mCancelComputation = true;
897        synchronized (mWaitObject) {
898            mWaitObject.notify();
899        }
900    }
901
902    @OnClickAttr
903    public void onCancelButtonClicked(View v) {
904        if (mPausing || mSurfaceTexture == null) return;
905        cancelHighResComputation();
906    }
907
908    @OnClickAttr
909    public void onThumbnailClicked(View v) {
910        if (mPausing || mThreadRunning || mSurfaceTexture == null) return;
911        showSharePopup();
912    }
913
914    private void showSharePopup() {
915        if (mThumbnail == null) return;
916        Uri uri = mThumbnail.getUri();
917        if (mSharePopup == null || !uri.equals(mSharePopup.getUri())) {
918            // The orientation compensation is set to 0 here because we only support landscape.
919            mSharePopup = new SharePopup(this, uri, mThumbnail.getBitmap(),
920                    mOrientationCompensation,
921                    findViewById(R.id.frame_layout));
922        }
923        mSharePopup.showAtLocation(mThumbnailView, Gravity.NO_GRAVITY, 0, 0);
924    }
925
926    private void reset() {
927        mCaptureState = CAPTURE_STATE_VIEWFINDER;
928
929        mReviewLayout.setVisibility(View.GONE);
930        mShutterButton.setBackgroundResource(R.drawable.btn_shutter_pan);
931        mPanoProgressBar.setVisibility(View.GONE);
932        mCaptureLayout.setVisibility(View.VISIBLE);
933        mMosaicFrameProcessor.reset();
934
935        mSurfaceTexture.setOnFrameAvailableListener(this);
936    }
937
938    private void resetToPreview() {
939        reset();
940        if (!mPausing) startCameraPreview();
941    }
942
943    private void showFinalMosaic(Bitmap bitmap) {
944        if (bitmap != null) {
945            mReview.setImageBitmap(bitmap);
946        }
947        mCaptureLayout.setVisibility(View.GONE);
948        mReviewLayout.setVisibility(View.VISIBLE);
949    }
950
951    private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) {
952        if (jpegData != null) {
953            String imagePath = PanoUtil.createName(
954                    getResources().getString(R.string.pano_file_name_format), mTimeTaken);
955            return Storage.addImage(getContentResolver(), imagePath, mTimeTaken, null,
956                    orientation, jpegData, width, height);
957        }
958        return null;
959    }
960
961    private void clearMosaicFrameProcessorIfNeeded() {
962        if (!mPausing || mThreadRunning) return;
963        mMosaicFrameProcessor.clear();
964    }
965
966    private void initMosaicFrameProcessorIfNeeded() {
967        if (mPausing || mThreadRunning) return;
968        if (mMosaicFrameProcessor == null) {
969            // Start the activity for the first time.
970            mMosaicFrameProcessor = new MosaicFrameProcessor(
971                    mPreviewWidth, mPreviewHeight, getPreviewBufSize());
972        }
973        mMosaicFrameProcessor.initialize();
974    }
975
976    private void initSoundRecorder() {
977        // Construct sound player; use enforced sound output if necessary
978        File recordSoundFile = new File(VIDEO_RECORD_SOUND);
979        try {
980            ParcelFileDescriptor recordSoundParcel =
981                    ParcelFileDescriptor.open(recordSoundFile,
982                            ParcelFileDescriptor.MODE_READ_ONLY);
983            AssetFileDescriptor recordSoundAsset =
984                    new AssetFileDescriptor(recordSoundParcel, 0,
985                                            AssetFileDescriptor.UNKNOWN_LENGTH);
986            if (SystemProperties.get("ro.camera.sound.forced", "0").equals("0")) {
987                mRecordSound = new SoundPlayer(recordSoundAsset, false);
988            } else {
989                mRecordSound = new SoundPlayer(recordSoundAsset, true);
990            }
991        } catch (java.io.FileNotFoundException e) {
992            Log.e(TAG, "System video record sound not found");
993            mRecordSound = null;
994        }
995    }
996
997    private void releaseSoundRecorder() {
998        if (mRecordSound != null) {
999            mRecordSound.release();
1000            mRecordSound = null;
1001        }
1002    }
1003
1004    @Override
1005    protected void onPause() {
1006        super.onPause();
1007
1008        mPausing = true;
1009        cancelHighResComputation();
1010        // Stop the capturing first.
1011        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
1012            stopCapture(true);
1013            reset();
1014        }
1015        if (mSharePopup != null) mSharePopup.dismiss();
1016
1017        if (mThumbnail != null && !mThumbnail.fromFile()) {
1018            mThumbnail.saveTo(new File(getFilesDir(), Thumbnail.LAST_THUMB_FILENAME));
1019        }
1020
1021        releaseCamera();
1022        releaseSoundRecorder();
1023        mMosaicView.onPause();
1024        clearMosaicFrameProcessorIfNeeded();
1025        mOrientationEventListener.disable();
1026        resetScreenOn();
1027        System.gc();
1028    }
1029
1030    @Override
1031    protected void doOnResume() {
1032        mPausing = false;
1033        mOrientationEventListener.enable();
1034
1035        mCaptureState = CAPTURE_STATE_VIEWFINDER;
1036        setupCamera();
1037
1038        initSoundRecorder();
1039
1040        // Camera must be initialized before MosaicFrameProcessor is initialized. The preview size
1041        // has to be decided by camera device.
1042        initMosaicFrameProcessorIfNeeded();
1043        mMosaicView.onResume();
1044
1045        initThumbnailButton();
1046        keepScreenOnAwhile();
1047    }
1048
1049    public MosaicJpeg generateFinalMosaic(boolean highRes) {
1050        if (mMosaicFrameProcessor.createMosaic(highRes) == Mosaic.MOSAIC_RET_CANCELLED) {
1051            return null;
1052        }
1053
1054        byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
1055        if (imageData == null) {
1056            Log.e(TAG, "getFinalMosaicNV21() returned null.");
1057            return new MosaicJpeg();
1058        }
1059
1060        int len = imageData.length - 8;
1061        int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
1062                + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
1063        int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
1064                + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
1065        Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
1066
1067        if (width <= 0 || height <= 0) {
1068            // TODO: pop up a error meesage indicating that the final result is not generated.
1069            Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
1070                    height);
1071            return new MosaicJpeg();
1072        }
1073
1074        YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
1075        ByteArrayOutputStream out = new ByteArrayOutputStream();
1076        yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
1077        try {
1078            out.close();
1079        } catch (Exception e) {
1080            Log.e(TAG, "Exception in storing final mosaic", e);
1081            return new MosaicJpeg();
1082        }
1083        return new MosaicJpeg(out.toByteArray(), width, height);
1084    }
1085
1086    private void setPreviewTexture(SurfaceTexture surface) {
1087        try {
1088            mCameraDevice.setPreviewTexture(surface);
1089        } catch (Throwable ex) {
1090            releaseCamera();
1091            throw new RuntimeException("setPreviewTexture failed", ex);
1092        }
1093    }
1094
1095    private void startCameraPreview() {
1096        // If we're previewing already, stop the preview first (this will blank
1097        // the screen).
1098        if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
1099
1100        // Set the display orientation to 0, so that the underlying mosaic library
1101        // can always get undistorted mPreviewWidth x mPreviewHeight image data
1102        // from SurfaceTexture.
1103        mCameraDevice.setDisplayOrientation(0);
1104
1105        setPreviewTexture(mSurfaceTexture);
1106
1107        try {
1108            Log.v(TAG, "startPreview");
1109            mCameraDevice.startPreview();
1110        } catch (Throwable ex) {
1111            releaseCamera();
1112            throw new RuntimeException("startPreview failed", ex);
1113        }
1114        mCameraState = PREVIEW_ACTIVE;
1115    }
1116
1117    private void stopCameraPreview() {
1118        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
1119            Log.v(TAG, "stopPreview");
1120            mCameraDevice.stopPreview();
1121        }
1122        mCameraState = PREVIEW_STOPPED;
1123    }
1124
1125    @Override
1126    public void onUserInteraction() {
1127        super.onUserInteraction();
1128        if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile();
1129    }
1130
1131    private void resetScreenOn() {
1132        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1133        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1134    }
1135
1136    private void keepScreenOnAwhile() {
1137        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1138        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1139        mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1140    }
1141
1142    private void keepScreenOn() {
1143        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1144        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1145    }
1146}
1147