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