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