PanoramaModule.java revision 032dea1d8406cde556ec0a441e4c90409edf9d63
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;
18
19import android.annotation.TargetApi;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.res.Configuration;
24import android.content.res.Resources;
25import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.graphics.ImageFormat;
28import android.graphics.PixelFormat;
29import android.graphics.Rect;
30import android.graphics.SurfaceTexture;
31import android.graphics.YuvImage;
32import android.graphics.drawable.Drawable;
33import android.hardware.Camera.Parameters;
34import android.hardware.Camera.Size;
35import android.media.ExifInterface;
36import android.net.Uri;
37import android.os.AsyncTask;
38import android.os.Handler;
39import android.os.Message;
40import android.os.PowerManager;
41import android.util.Log;
42import android.view.KeyEvent;
43import android.view.LayoutInflater;
44import android.view.MotionEvent;
45import android.view.OrientationEventListener;
46import android.view.View;
47import android.view.View.OnClickListener;
48import android.view.ViewGroup;
49import android.view.WindowManager;
50import android.widget.ImageView;
51import android.widget.LinearLayout;
52import android.widget.TextView;
53
54import com.android.camera.CameraManager.CameraProxy;
55import com.android.camera.ui.LayoutChangeNotifier;
56import com.android.camera.ui.LayoutNotifyView;
57import com.android.camera.ui.PopupManager;
58import com.android.camera.ui.Rotatable;
59import com.android.camera.ui.RotateLayout;
60import com.android.gallery3d.common.ApiHelper;
61import com.android.gallery3d.ui.GLRootView;
62
63import java.io.ByteArrayOutputStream;
64import java.io.IOException;
65import java.text.DateFormat;
66import java.text.SimpleDateFormat;
67import java.util.List;
68import java.util.TimeZone;
69
70/**
71 * Activity to handle panorama capturing.
72 */
73@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture
74public class PanoramaModule implements CameraModule,
75        SurfaceTexture.OnFrameAvailableListener,
76        ShutterButton.OnShutterButtonListener,
77        LayoutChangeNotifier.Listener {
78
79    public static final int DEFAULT_SWEEP_ANGLE = 160;
80    public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL;
81    public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720;
82
83    private static final int MSG_LOW_RES_FINAL_MOSAIC_READY = 1;
84    private static final int MSG_GENERATE_FINAL_MOSAIC_ERROR = 2;
85    private static final int MSG_RESET_TO_PREVIEW = 3;
86    private static final int MSG_CLEAR_SCREEN_DELAY = 4;
87
88    private static final int SCREEN_DELAY = 2 * 60 * 1000;
89
90    private static final String TAG = "CAM PanoModule";
91    private static final int PREVIEW_STOPPED = 0;
92    private static final int PREVIEW_ACTIVE = 1;
93    private static final int CAPTURE_STATE_VIEWFINDER = 0;
94    private static final int CAPTURE_STATE_MOSAIC = 1;
95
96    private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd";
97    private static final String GPS_TIME_FORMAT_STR = "kk/1,mm/1,ss/1";
98    private static final String DATETIME_FORMAT_STR = "yyyy:MM:dd kk:mm:ss";
99
100    // Speed is in unit of deg/sec
101    private static final float PANNING_SPEED_THRESHOLD = 25f;
102
103    private ContentResolver mContentResolver;
104
105    private GLRootView mGLRootView;
106    private ViewGroup mPanoLayout;
107    private LinearLayout mCaptureLayout;
108    private View mReviewLayout;
109    private ImageView mReview;
110    private RotateLayout mCaptureIndicator;
111    private PanoProgressBar mPanoProgressBar;
112    private PanoProgressBar mSavingProgressBar;
113    private LayoutNotifyView mPreviewArea;
114    private View mLeftIndicator;
115    private View mRightIndicator;
116    private MosaicPreviewRenderer mMosaicPreviewRenderer;
117    private TextView mTooFastPrompt;
118    private ShutterButton mShutterButton;
119    private Object mWaitObject = new Object();
120
121    private DateFormat mGPSDateStampFormat;
122    private DateFormat mGPSTimeStampFormat;
123    private DateFormat mDateTimeStampFormat;
124
125    private String mPreparePreviewString;
126    private String mDialogTitle;
127    private String mDialogOkString;
128    private String mDialogPanoramaFailedString;
129    private String mDialogWaitingPreviousString;
130
131    private int mIndicatorColor;
132    private int mIndicatorColorFast;
133
134    private boolean mUsingFrontCamera;
135    private int mPreviewWidth;
136    private int mPreviewHeight;
137    private int mCameraState;
138    private int mCaptureState;
139    private PowerManager.WakeLock mPartialWakeLock;
140    private MosaicFrameProcessor mMosaicFrameProcessor;
141    private boolean mMosaicFrameProcessorInitialized;
142    private AsyncTask <Void, Void, Void> mWaitProcessorTask;
143    private long mTimeTaken;
144    private Handler mMainHandler;
145    private SurfaceTexture mCameraTexture;
146    private boolean mThreadRunning;
147    private boolean mCancelComputation;
148    private float mHorizontalViewAngle;
149    private float mVerticalViewAngle;
150
151    // Prefer FOCUS_MODE_INFINITY to FOCUS_MODE_CONTINUOUS_VIDEO because of
152    // getting a better image quality by the former.
153    private String mTargetFocusMode = Parameters.FOCUS_MODE_INFINITY;
154
155    private PanoOrientationEventListener mOrientationEventListener;
156    // The value could be 0, 90, 180, 270 for the 4 different orientations measured in clockwise
157    // respectively.
158    private int mDeviceOrientation;
159    private int mDeviceOrientationAtCapture;
160    private int mCameraOrientation;
161    private int mOrientationCompensation;
162
163    private RotateDialogController mRotateDialog;
164
165    private SoundClips.Player mSoundPlayer;
166
167    private Runnable mOnFrameAvailableRunnable;
168
169    private CameraActivity mActivity;
170    private View mRootView;
171    private CameraProxy mCameraDevice;
172    private boolean mPaused;
173
174    private class MosaicJpeg {
175        public MosaicJpeg(byte[] data, int width, int height) {
176            this.data = data;
177            this.width = width;
178            this.height = height;
179            this.isValid = true;
180        }
181
182        public MosaicJpeg() {
183            this.data = null;
184            this.width = 0;
185            this.height = 0;
186            this.isValid = false;
187        }
188
189        public final byte[] data;
190        public final int width;
191        public final int height;
192        public final boolean isValid;
193    }
194
195    private class PanoOrientationEventListener extends OrientationEventListener {
196        public PanoOrientationEventListener(Context context) {
197            super(context);
198        }
199
200        @Override
201        public void onOrientationChanged(int orientation) {
202            // We keep the last known orientation. So if the user first orient
203            // the camera then point the camera to floor or sky, we still have
204            // the correct orientation.
205            if (orientation == ORIENTATION_UNKNOWN) return;
206            mDeviceOrientation = Util.roundOrientation(orientation, mDeviceOrientation);
207            // When the screen is unlocked, display rotation may change. Always
208            // calculate the up-to-date orientationCompensation.
209            int orientationCompensation = mDeviceOrientation
210                    + Util.getDisplayRotation(mActivity) % 360;
211            if (mOrientationCompensation != orientationCompensation) {
212                mOrientationCompensation = orientationCompensation;
213            }
214        }
215    }
216
217    @Override
218    public void init(CameraActivity activity, View parent, boolean reuseScreenNail) {
219        mActivity = activity;
220        mRootView = (ViewGroup) parent;
221
222        createContentView();
223
224        mContentResolver = mActivity.getContentResolver();
225        if (reuseScreenNail) {
226            mActivity.reuseCameraScreenNail(true);
227        } else {
228            mActivity.createCameraScreenNail(true);
229        }
230
231        // This runs in UI thread.
232        mOnFrameAvailableRunnable = new Runnable() {
233            @Override
234            public void run() {
235                // Frames might still be available after the activity is paused.
236                // If we call onFrameAvailable after pausing, the GL thread will crash.
237                if (mPaused) return;
238
239                if (mGLRootView.getVisibility() != View.VISIBLE) {
240                    mMosaicPreviewRenderer.showPreviewFrameSync();
241                    mGLRootView.setVisibility(View.VISIBLE);
242                } else {
243                    if (mCaptureState == CAPTURE_STATE_VIEWFINDER) {
244                        mMosaicPreviewRenderer.showPreviewFrame();
245                    } else {
246                        mMosaicPreviewRenderer.alignFrame();
247                        mMosaicFrameProcessor.processFrame();
248                    }
249                }
250            }
251        };
252
253        mGPSDateStampFormat = new SimpleDateFormat(GPS_DATE_FORMAT_STR);
254        mGPSTimeStampFormat = new SimpleDateFormat(GPS_TIME_FORMAT_STR);
255        mDateTimeStampFormat = new SimpleDateFormat(DATETIME_FORMAT_STR);
256        TimeZone tzUTC = TimeZone.getTimeZone("UTC");
257        mGPSDateStampFormat.setTimeZone(tzUTC);
258        mGPSTimeStampFormat.setTimeZone(tzUTC);
259
260        PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE);
261        mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Panorama");
262
263        mOrientationEventListener = new PanoOrientationEventListener(mActivity);
264
265        mMosaicFrameProcessor = MosaicFrameProcessor.getInstance();
266
267        Resources appRes = mActivity.getResources();
268        mPreparePreviewString = appRes.getString(R.string.pano_dialog_prepare_preview);
269        mDialogTitle = appRes.getString(R.string.pano_dialog_title);
270        mDialogOkString = appRes.getString(R.string.dialog_ok);
271        mDialogPanoramaFailedString = appRes.getString(R.string.pano_dialog_panorama_failed);
272        mDialogWaitingPreviousString = appRes.getString(R.string.pano_dialog_waiting_previous);
273
274        mGLRootView = (GLRootView) mActivity.getGLRoot();
275
276        mMainHandler = new Handler() {
277            @Override
278            public void handleMessage(Message msg) {
279                switch (msg.what) {
280                    case MSG_LOW_RES_FINAL_MOSAIC_READY:
281                        onBackgroundThreadFinished();
282                        showFinalMosaic((Bitmap) msg.obj);
283                        saveHighResMosaic();
284                        break;
285                    case MSG_GENERATE_FINAL_MOSAIC_ERROR:
286                        onBackgroundThreadFinished();
287                        if (mPaused) {
288                            resetToPreview();
289                        } else {
290                            mRotateDialog.showAlertDialog(
291                                    mDialogTitle, mDialogPanoramaFailedString,
292                                    mDialogOkString, new Runnable() {
293                                        @Override
294                                        public void run() {
295                                            resetToPreview();
296                                        }},
297                                    null, null);
298                        }
299                        clearMosaicFrameProcessorIfNeeded();
300                        break;
301                    case MSG_RESET_TO_PREVIEW:
302                        onBackgroundThreadFinished();
303                        resetToPreview();
304                        clearMosaicFrameProcessorIfNeeded();
305                        break;
306                    case MSG_CLEAR_SCREEN_DELAY:
307                        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.
308                                FLAG_KEEP_SCREEN_ON);
309                        break;
310                }
311            }
312        };
313    }
314
315    @Override
316    public boolean dispatchTouchEvent(MotionEvent m) {
317        return mActivity.superDispatchTouchEvent(m);
318    }
319
320    private void setupCamera() throws CameraHardwareException, CameraDisabledException {
321        openCamera();
322        Parameters parameters = mCameraDevice.getParameters();
323        setupCaptureParams(parameters);
324        configureCamera(parameters);
325    }
326
327    private void releaseCamera() {
328        if (mCameraDevice != null) {
329            mCameraDevice.setPreviewCallbackWithBuffer(null);
330            CameraHolder.instance().release();
331            mCameraDevice = null;
332            mCameraState = PREVIEW_STOPPED;
333        }
334    }
335
336    private void openCamera() throws CameraHardwareException, CameraDisabledException {
337        int cameraId = CameraHolder.instance().getBackCameraId();
338        // If there is no back camera, use the first camera. Camera id starts
339        // from 0. Currently if a camera is not back facing, it is front facing.
340        // This is also forward compatible if we have a new facing other than
341        // back or front in the future.
342        if (cameraId == -1) cameraId = 0;
343        mCameraDevice = Util.openCamera(mActivity, cameraId);
344        mCameraOrientation = Util.getCameraOrientation(cameraId);
345        if (cameraId == CameraHolder.instance().getFrontCameraId()) mUsingFrontCamera = true;
346    }
347
348    private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3,
349            boolean needSmaller) {
350        int pixelsDiff = DEFAULT_CAPTURE_PIXELS;
351        boolean hasFound = false;
352        for (Size size : supportedSizes) {
353            int h = size.height;
354            int w = size.width;
355            // we only want 4:3 format.
356            int d = DEFAULT_CAPTURE_PIXELS - h * w;
357            if (needSmaller && d < 0) { // no bigger preview than 960x720.
358                continue;
359            }
360            if (need4To3 && (h * 4 != w * 3)) {
361                continue;
362            }
363            d = Math.abs(d);
364            if (d < pixelsDiff) {
365                mPreviewWidth = w;
366                mPreviewHeight = h;
367                pixelsDiff = d;
368                hasFound = true;
369            }
370        }
371        return hasFound;
372    }
373
374    private void setupCaptureParams(Parameters parameters) {
375        List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
376        if (!findBestPreviewSize(supportedSizes, true, true)) {
377            Log.w(TAG, "No 4:3 ratio preview size supported.");
378            if (!findBestPreviewSize(supportedSizes, false, true)) {
379                Log.w(TAG, "Can't find a supported preview size smaller than 960x720.");
380                findBestPreviewSize(supportedSizes, false, false);
381            }
382        }
383        Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth);
384        parameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
385
386        List<int[]> frameRates = parameters.getSupportedPreviewFpsRange();
387        int last = frameRates.size() - 1;
388        int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX];
389        int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX];
390        parameters.setPreviewFpsRange(minFps, maxFps);
391        Log.v(TAG, "preview fps: " + minFps + ", " + maxFps);
392
393        List<String> supportedFocusModes = parameters.getSupportedFocusModes();
394        if (supportedFocusModes.indexOf(mTargetFocusMode) >= 0) {
395            parameters.setFocusMode(mTargetFocusMode);
396        } else {
397            // Use the default focus mode and log a message
398            Log.w(TAG, "Cannot set the focus mode to " + mTargetFocusMode +
399                  " becuase the mode is not supported.");
400        }
401
402        parameters.set(Util.RECORDING_HINT, Util.FALSE);
403
404        mHorizontalViewAngle = parameters.getHorizontalViewAngle();
405        mVerticalViewAngle =  parameters.getVerticalViewAngle();
406    }
407
408    public int getPreviewBufSize() {
409        PixelFormat pixelInfo = new PixelFormat();
410        PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo);
411        // TODO: remove this extra 32 byte after the driver bug is fixed.
412        return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32;
413    }
414
415    private void configureCamera(Parameters parameters) {
416        mCameraDevice.setParameters(parameters);
417    }
418
419    private void configMosaicPreview(int w, int h) {
420        stopCameraPreview();
421        CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
422        screenNail.setSize(w, h);
423        if (screenNail.getSurfaceTexture() == null) {
424            screenNail.acquireSurfaceTexture();
425        } else {
426            screenNail.releaseSurfaceTexture();
427            screenNail.acquireSurfaceTexture();
428            mActivity.notifyScreenNailChanged();
429        }
430        boolean isLandscape = (mActivity.getResources().getConfiguration().orientation
431                == Configuration.ORIENTATION_LANDSCAPE);
432        if (mMosaicPreviewRenderer != null) mMosaicPreviewRenderer.release();
433        mMosaicPreviewRenderer = new MosaicPreviewRenderer(
434                screenNail.getSurfaceTexture(), w, h, isLandscape);
435
436        mCameraTexture = mMosaicPreviewRenderer.getInputSurfaceTexture();
437        if (!mPaused && !mThreadRunning && mWaitProcessorTask == null) {
438            resetToPreview();
439        }
440    }
441
442    // Receives the layout change event from the preview area. So we can set
443    // the camera preview screennail to the same size and initialize the mosaic
444    // preview renderer.
445    @Override
446    public void onLayoutChange(View v, int l, int t, int r, int b) {
447        Log.i(TAG, "layout change: "+(r - l) + "/" +(b - t));
448        mActivity.onLayoutChange(v, l, t, r, b);
449        configMosaicPreview(r - l, b - t);
450    }
451
452    @Override
453    public void onFrameAvailable(SurfaceTexture surface) {
454        /* This function may be called by some random thread,
455         * so let's be safe and jump back to ui thread.
456         * No OpenGL calls can be done here. */
457        mActivity.runOnUiThread(mOnFrameAvailableRunnable);
458    }
459
460    private void hideDirectionIndicators() {
461        mLeftIndicator.setVisibility(View.GONE);
462        mRightIndicator.setVisibility(View.GONE);
463    }
464
465    private void showDirectionIndicators(int direction) {
466        switch (direction) {
467            case PanoProgressBar.DIRECTION_NONE:
468                mLeftIndicator.setVisibility(View.VISIBLE);
469                mRightIndicator.setVisibility(View.VISIBLE);
470                break;
471            case PanoProgressBar.DIRECTION_LEFT:
472                mLeftIndicator.setVisibility(View.VISIBLE);
473                mRightIndicator.setVisibility(View.GONE);
474                break;
475            case PanoProgressBar.DIRECTION_RIGHT:
476                mLeftIndicator.setVisibility(View.GONE);
477                mRightIndicator.setVisibility(View.VISIBLE);
478                break;
479        }
480    }
481
482    public void startCapture() {
483        // Reset values so we can do this again.
484        mCancelComputation = false;
485        mTimeTaken = System.currentTimeMillis();
486        mActivity.setSwipingEnabled(false);
487        mActivity.hideSwitcher();
488        mShutterButton.setImageResource(R.drawable.btn_shutter_recording);
489        mCaptureState = CAPTURE_STATE_MOSAIC;
490        mCaptureIndicator.setVisibility(View.VISIBLE);
491        showDirectionIndicators(PanoProgressBar.DIRECTION_NONE);
492
493        mMosaicFrameProcessor.setProgressListener(new MosaicFrameProcessor.ProgressListener() {
494            @Override
495            public void onProgress(boolean isFinished, float panningRateX, float panningRateY,
496                    float progressX, float progressY) {
497                float accumulatedHorizontalAngle = progressX * mHorizontalViewAngle;
498                float accumulatedVerticalAngle = progressY * mVerticalViewAngle;
499                if (isFinished
500                        || (Math.abs(accumulatedHorizontalAngle) >= DEFAULT_SWEEP_ANGLE)
501                        || (Math.abs(accumulatedVerticalAngle) >= DEFAULT_SWEEP_ANGLE)) {
502                    stopCapture(false);
503                } else {
504                    float panningRateXInDegree = panningRateX * mHorizontalViewAngle;
505                    float panningRateYInDegree = panningRateY * mVerticalViewAngle;
506                    updateProgress(panningRateXInDegree, panningRateYInDegree,
507                            accumulatedHorizontalAngle, accumulatedVerticalAngle);
508                }
509            }
510        });
511
512        mPanoProgressBar.reset();
513        // TODO: calculate the indicator width according to different devices to reflect the actual
514        // angle of view of the camera device.
515        mPanoProgressBar.setIndicatorWidth(20);
516        mPanoProgressBar.setMaxProgress(DEFAULT_SWEEP_ANGLE);
517        mPanoProgressBar.setVisibility(View.VISIBLE);
518        mDeviceOrientationAtCapture = mDeviceOrientation;
519        keepScreenOn();
520        mActivity.getOrientationManager().lockOrientation();
521    }
522
523    private void stopCapture(boolean aborted) {
524        mCaptureState = CAPTURE_STATE_VIEWFINDER;
525        mCaptureIndicator.setVisibility(View.GONE);
526        hideTooFastIndication();
527        hideDirectionIndicators();
528
529        mMosaicFrameProcessor.setProgressListener(null);
530        stopCameraPreview();
531
532        mCameraTexture.setOnFrameAvailableListener(null);
533
534        if (!aborted && !mThreadRunning) {
535            mRotateDialog.showWaitingDialog(mPreparePreviewString);
536            // Hide shutter button, shutter icon, etc when waiting for
537            // panorama to stitch
538            mActivity.hideUI();
539            runBackgroundThread(new Thread() {
540                @Override
541                public void run() {
542                    MosaicJpeg jpeg = generateFinalMosaic(false);
543
544                    if (jpeg != null && jpeg.isValid) {
545                        Bitmap bitmap = null;
546                        bitmap = BitmapFactory.decodeByteArray(jpeg.data, 0, jpeg.data.length);
547                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
548                                MSG_LOW_RES_FINAL_MOSAIC_READY, bitmap));
549                    } else {
550                        mMainHandler.sendMessage(mMainHandler.obtainMessage(
551                                MSG_RESET_TO_PREVIEW));
552                    }
553                }
554            });
555        }
556        keepScreenOnAwhile();
557        mActivity.getOrientationManager().unlockOrientation();
558    }
559
560    private void showTooFastIndication() {
561        mTooFastPrompt.setVisibility(View.VISIBLE);
562        // The PreviewArea also contains the border for "too fast" indication.
563        mPreviewArea.setVisibility(View.VISIBLE);
564        mPanoProgressBar.setIndicatorColor(mIndicatorColorFast);
565        mLeftIndicator.setEnabled(true);
566        mRightIndicator.setEnabled(true);
567    }
568
569    private void hideTooFastIndication() {
570        mTooFastPrompt.setVisibility(View.GONE);
571        // We set "INVISIBLE" instead of "GONE" here because we need mPreviewArea to have layout
572        // information so we can know the size and position for mCameraScreenNail.
573        mPreviewArea.setVisibility(View.INVISIBLE);
574        mPanoProgressBar.setIndicatorColor(mIndicatorColor);
575        mLeftIndicator.setEnabled(false);
576        mRightIndicator.setEnabled(false);
577    }
578
579    private void updateProgress(float panningRateXInDegree, float panningRateYInDegree,
580            float progressHorizontalAngle, float progressVerticalAngle) {
581        mGLRootView.requestRender();
582
583        // TODO: Now we just display warning message by the panning speed.
584        // Since we only support horizontal panning, we should display a warning message
585        // in UI when there're significant vertical movements.
586        if ((Math.abs(panningRateXInDegree) > PANNING_SPEED_THRESHOLD)
587            || (Math.abs(panningRateYInDegree) > PANNING_SPEED_THRESHOLD)) {
588            showTooFastIndication();
589        } else {
590            hideTooFastIndication();
591        }
592        int angleInMajorDirection =
593                (Math.abs(progressHorizontalAngle) > Math.abs(progressVerticalAngle))
594                ? (int) progressHorizontalAngle
595                : (int) progressVerticalAngle;
596        mPanoProgressBar.setProgress((angleInMajorDirection));
597    }
598
599    private void setViews(Resources appRes) {
600        mCaptureState = CAPTURE_STATE_VIEWFINDER;
601        mPanoProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_pan_progress_bar);
602        mPanoProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
603        mPanoProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_done));
604        mPanoProgressBar.setIndicatorColor(mIndicatorColor);
605        mPanoProgressBar.setOnDirectionChangeListener(
606                new PanoProgressBar.OnDirectionChangeListener () {
607                    @Override
608                    public void onDirectionChange(int direction) {
609                        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
610                            showDirectionIndicators(direction);
611                        }
612                    }
613                });
614
615        mLeftIndicator = mRootView.findViewById(R.id.pano_pan_left_indicator);
616        mRightIndicator = mRootView.findViewById(R.id.pano_pan_right_indicator);
617        mLeftIndicator.setEnabled(false);
618        mRightIndicator.setEnabled(false);
619        mTooFastPrompt = (TextView) mRootView.findViewById(R.id.pano_capture_too_fast_textview);
620        // This mPreviewArea also shows the border for visual "too fast" indication.
621        mPreviewArea = (LayoutNotifyView) mRootView.findViewById(R.id.pano_preview_area);
622        mPreviewArea.setOnLayoutChangeListener(this);
623
624        mSavingProgressBar = (PanoProgressBar) mRootView.findViewById(R.id.pano_saving_progress_bar);
625        mSavingProgressBar.setIndicatorWidth(0);
626        mSavingProgressBar.setMaxProgress(100);
627        mSavingProgressBar.setBackgroundColor(appRes.getColor(R.color.pano_progress_empty));
628        mSavingProgressBar.setDoneColor(appRes.getColor(R.color.pano_progress_indication));
629
630        mCaptureIndicator = (RotateLayout) mRootView.findViewById(R.id.pano_capture_indicator);
631
632        mReviewLayout = mRootView.findViewById(R.id.pano_review_layout);
633        mReview = (ImageView) mRootView.findViewById(R.id.pano_reviewarea);
634        View cancelButton = mRootView.findViewById(R.id.pano_review_cancel_button);
635        cancelButton.setOnClickListener(new OnClickListener() {
636            @Override
637            public void onClick(View arg0) {
638                if (mPaused || mCameraTexture == null) return;
639                cancelHighResComputation();
640            }
641        });
642
643        mShutterButton = mActivity.getShutterButton();
644        mShutterButton.setImageResource(R.drawable.btn_new_shutter);
645        mShutterButton.setOnShutterButtonListener(this);
646
647        if (mActivity.getResources().getConfiguration().orientation
648                == Configuration.ORIENTATION_PORTRAIT) {
649            Rotatable[] rotateLayout = {
650                    (Rotatable) mRootView.findViewById(R.id.pano_pan_progress_bar_layout),
651                    (Rotatable) mRootView.findViewById(R.id.pano_capture_too_fast_textview_layout),
652                    (Rotatable) mRootView.findViewById(R.id.pano_review_saving_indication_layout),
653                    (Rotatable) mRootView.findViewById(R.id.pano_saving_progress_bar_layout),
654                    (Rotatable) mRootView.findViewById(R.id.pano_review_cancel_button_layout),
655                    (Rotatable) mRootView.findViewById(R.id.pano_rotate_reviewarea),
656                    mRotateDialog,
657                    mCaptureIndicator,
658                    };
659            for (Rotatable r : rotateLayout) {
660                r.setOrientation(270, false);
661            }
662        } else {
663            // Even if the orientation is 0, we still need to set because it might be previously
664            // set when the configuration is portrait.
665            mRotateDialog.setOrientation(0, false);
666        }
667    }
668
669    private void createContentView() {
670        mActivity.getLayoutInflater().inflate(R.layout.panorama_module, (ViewGroup) mRootView);
671        Resources appRes = mActivity.getResources();
672        mCaptureLayout = (LinearLayout) mRootView.findViewById(R.id.camera_app_root);
673        mIndicatorColor = appRes.getColor(R.color.pano_progress_indication);
674        mIndicatorColorFast = appRes.getColor(R.color.pano_progress_indication_fast);
675        mPanoLayout = (ViewGroup) mRootView.findViewById(R.id.pano_layout);
676        mRotateDialog = new RotateDialogController(mActivity, R.layout.rotate_dialog);
677        setViews(appRes);
678    }
679
680    @Override
681    public void onShutterButtonClick() {
682        // If mCameraTexture == null then GL setup is not finished yet.
683        // No buttons can be pressed.
684        if (mPaused || mThreadRunning || mCameraTexture == null) return;
685        // Since this button will stay on the screen when capturing, we need to check the state
686        // right now.
687        switch (mCaptureState) {
688            case CAPTURE_STATE_VIEWFINDER:
689                if(mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) return;
690                mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING);
691                startCapture();
692                break;
693            case CAPTURE_STATE_MOSAIC:
694                mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING);
695                stopCapture(false);
696        }
697    }
698
699    @Override
700    public void onShutterButtonFocus(boolean pressed) {
701    }
702
703    public void reportProgress() {
704        mSavingProgressBar.reset();
705        mSavingProgressBar.setRightIncreasing(true);
706        Thread t = new Thread() {
707            @Override
708            public void run() {
709                while (mThreadRunning) {
710                    final int progress = mMosaicFrameProcessor.reportProgress(
711                            true, mCancelComputation);
712
713                    try {
714                        synchronized (mWaitObject) {
715                            mWaitObject.wait(50);
716                        }
717                    } catch (InterruptedException e) {
718                        throw new RuntimeException("Panorama reportProgress failed", e);
719                    }
720                    // Update the progress bar
721                    mActivity.runOnUiThread(new Runnable() {
722                        @Override
723                        public void run() {
724                            mSavingProgressBar.setProgress(progress);
725                        }
726                    });
727                }
728            }
729        };
730        t.start();
731    }
732
733    public void saveHighResMosaic() {
734        runBackgroundThread(new Thread() {
735            @Override
736            public void run() {
737                mPartialWakeLock.acquire();
738                MosaicJpeg jpeg;
739                try {
740                    jpeg = generateFinalMosaic(true);
741                } finally {
742                    mPartialWakeLock.release();
743                }
744
745                if (jpeg == null) {  // Cancelled by user.
746                    mMainHandler.sendEmptyMessage(MSG_RESET_TO_PREVIEW);
747                } else if (!jpeg.isValid) {  // Error when generating mosaic.
748                    mMainHandler.sendEmptyMessage(MSG_GENERATE_FINAL_MOSAIC_ERROR);
749                } else {
750                    // The panorama image returned from the library is oriented based on the
751                    // natural orientation of a camera. We need to set an orientation for the image
752                    // in its EXIF header, so the image can be displayed correctly.
753                    // The orientation is calculated from compensating the
754                    // device orientation at capture and the camera orientation respective to
755                    // the natural orientation of the device.
756                    int orientation;
757                    if (mUsingFrontCamera) {
758                        // mCameraOrientation is negative with respect to the front facing camera.
759                        // See document of android.hardware.Camera.Parameters.setRotation.
760                        orientation = (mDeviceOrientationAtCapture - mCameraOrientation + 360) % 360;
761                    } else {
762                        orientation = (mDeviceOrientationAtCapture + mCameraOrientation) % 360;
763                    }
764                    Uri uri = savePanorama(jpeg.data, jpeg.width, jpeg.height, orientation);
765                    if (uri != null) {
766                        mActivity.addSecureAlbumItemIfNeeded(false, uri);
767                        Util.broadcastNewPicture(mActivity, uri);
768                    }
769                    mMainHandler.sendMessage(
770                            mMainHandler.obtainMessage(MSG_RESET_TO_PREVIEW));
771                }
772            }
773        });
774        reportProgress();
775    }
776
777    private void runBackgroundThread(Thread thread) {
778        mThreadRunning = true;
779        thread.start();
780    }
781
782    private void onBackgroundThreadFinished() {
783        mThreadRunning = false;
784        mRotateDialog.dismissDialog();
785    }
786
787    private void cancelHighResComputation() {
788        mCancelComputation = true;
789        synchronized (mWaitObject) {
790            mWaitObject.notify();
791        }
792    }
793
794    // This function will be called upon the first camera frame is available.
795    private void reset() {
796        mCaptureState = CAPTURE_STATE_VIEWFINDER;
797
798        // We should set mGLRootView visible too. However, since there might be no
799        // frame available yet, setting mGLRootView visible should be done right after
800        // the first camera frame is available and therefore it is done by
801        // mOnFirstFrameAvailableRunnable.
802        mActivity.setSwipingEnabled(true);
803        mShutterButton.setImageResource(R.drawable.btn_new_shutter);
804        mReviewLayout.setVisibility(View.GONE);
805        mPanoProgressBar.setVisibility(View.GONE);
806        // Orientation change will trigger onLayoutChange->configMosaicPreview->
807        // resetToPreview. Do not show the capture UI in film strip.
808        if (mActivity.mShowCameraAppView) {
809            mCaptureLayout.setVisibility(View.VISIBLE);
810            mActivity.showUI();
811        }
812        mMosaicFrameProcessor.reset();
813    }
814
815    private void resetToPreview() {
816        reset();
817        if (!mPaused) startCameraPreview();
818    }
819
820    private void showFinalMosaic(Bitmap bitmap) {
821        if (bitmap != null) {
822            mReview.setImageBitmap(bitmap);
823        }
824
825        mGLRootView.setVisibility(View.GONE);
826        mCaptureLayout.setVisibility(View.GONE);
827        mReviewLayout.setVisibility(View.VISIBLE);
828    }
829
830    private Uri savePanorama(byte[] jpegData, int width, int height, int orientation) {
831        if (jpegData != null) {
832            String filename = PanoUtil.createName(
833                    mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken);
834            Uri uri = Storage.addImage(mContentResolver, filename, mTimeTaken, null,
835                    orientation, jpegData, width, height);
836            if (uri != null) {
837                String filepath = Storage.generateFilepath(filename);
838                try {
839                    ExifInterface exif = new ExifInterface(filepath);
840
841                    exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP,
842                            mGPSDateStampFormat.format(mTimeTaken));
843                    exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP,
844                            mGPSTimeStampFormat.format(mTimeTaken));
845                    exif.setAttribute(ExifInterface.TAG_DATETIME,
846                            mDateTimeStampFormat.format(mTimeTaken));
847                    exif.setAttribute(ExifInterface.TAG_ORIENTATION,
848                            getExifOrientation(orientation));
849
850                    exif.saveAttributes();
851                } catch (IOException e) {
852                    Log.e(TAG, "Cannot set EXIF for " + filepath, e);
853                }
854            }
855            return uri;
856        }
857        return null;
858    }
859
860    private static String getExifOrientation(int orientation) {
861        switch (orientation) {
862            case 0:
863                return String.valueOf(ExifInterface.ORIENTATION_NORMAL);
864            case 90:
865                return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90);
866            case 180:
867                return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180);
868            case 270:
869                return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270);
870            default:
871                throw new AssertionError("invalid: " + orientation);
872        }
873    }
874
875    private void clearMosaicFrameProcessorIfNeeded() {
876        if (!mPaused || mThreadRunning) return;
877        // Only clear the processor if it is initialized by this activity
878        // instance. Other activity instances may be using it.
879        if (mMosaicFrameProcessorInitialized) {
880            mMosaicFrameProcessor.clear();
881            mMosaicFrameProcessorInitialized = false;
882        }
883    }
884
885    private void initMosaicFrameProcessorIfNeeded() {
886        if (mPaused || mThreadRunning) return;
887        mMosaicFrameProcessor.initialize(
888                mPreviewWidth, mPreviewHeight, getPreviewBufSize());
889        mMosaicFrameProcessorInitialized = true;
890    }
891
892    @Override
893    public void onPauseBeforeSuper() {
894        mPaused = true;
895    }
896
897    @Override
898    public void onPauseAfterSuper() {
899        mOrientationEventListener.disable();
900        if (mCameraDevice == null) {
901            // Camera open failed. Nothing should be done here.
902            return;
903        }
904        // Stop the capturing first.
905        if (mCaptureState == CAPTURE_STATE_MOSAIC) {
906            stopCapture(true);
907            reset();
908        }
909
910        releaseCamera();
911        mCameraTexture = null;
912
913        // The preview renderer might not have a chance to be initialized before
914        // onPause().
915        if (mMosaicPreviewRenderer != null) {
916            mMosaicPreviewRenderer.release();
917            mMosaicPreviewRenderer = null;
918        }
919
920        clearMosaicFrameProcessorIfNeeded();
921        if (mWaitProcessorTask != null) {
922            mWaitProcessorTask.cancel(true);
923            mWaitProcessorTask = null;
924        }
925        resetScreenOn();
926        if (mSoundPlayer != null) {
927            mSoundPlayer.release();
928            mSoundPlayer = null;
929        }
930        CameraScreenNail screenNail = (CameraScreenNail) mActivity.mCameraScreenNail;
931        if (screenNail.getSurfaceTexture() != null) {
932            screenNail.releaseSurfaceTexture();
933        }
934        System.gc();
935    }
936
937    @Override
938    public void onConfigurationChanged(Configuration newConfig) {
939
940        Drawable lowResReview = null;
941        if (mThreadRunning) lowResReview = mReview.getDrawable();
942
943        // Change layout in response to configuration change
944        mCaptureLayout.setOrientation(
945                newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
946                ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
947        mCaptureLayout.removeAllViews();
948        LayoutInflater inflater = mActivity.getLayoutInflater();
949        inflater.inflate(R.layout.preview_frame_pano, mCaptureLayout);
950
951        mPanoLayout.removeView(mReviewLayout);
952        inflater.inflate(R.layout.pano_review, mPanoLayout);
953
954        setViews(mActivity.getResources());
955        if (mThreadRunning) {
956            mReview.setImageDrawable(lowResReview);
957            mCaptureLayout.setVisibility(View.GONE);
958            mReviewLayout.setVisibility(View.VISIBLE);
959        }
960    }
961
962    @Override
963    public void onOrientationChanged(int orientation) {
964    }
965
966    @Override
967    public void onResumeBeforeSuper() {
968        mPaused = false;
969    }
970
971    @Override
972    public void onResumeAfterSuper() {
973        mOrientationEventListener.enable();
974
975        mCaptureState = CAPTURE_STATE_VIEWFINDER;
976
977        try {
978            setupCamera();
979        } catch (CameraHardwareException e) {
980            Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
981            return;
982        } catch (CameraDisabledException e) {
983            Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
984            return;
985        }
986
987        // Set up sound playback for shutter button
988        mSoundPlayer = SoundClips.getPlayer(mActivity);
989
990        // Check if another panorama instance is using the mosaic frame processor.
991        mRotateDialog.dismissDialog();
992        if (!mThreadRunning && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
993            mGLRootView.setVisibility(View.GONE);
994            mRotateDialog.showWaitingDialog(mDialogWaitingPreviousString);
995            // If stitching is still going on, make sure switcher and shutter button
996            // are not showing
997            mActivity.hideUI();
998            mWaitProcessorTask = new WaitProcessorTask().execute();
999        } else {
1000            if (!mThreadRunning) mGLRootView.setVisibility(View.VISIBLE);
1001            // Camera must be initialized before MosaicFrameProcessor is
1002            // initialized. The preview size has to be decided by camera device.
1003            initMosaicFrameProcessorIfNeeded();
1004            int w = mPreviewArea.getWidth();
1005            int h = mPreviewArea.getHeight();
1006            if (w != 0 && h != 0) {  // The layout has been calculated.
1007                configMosaicPreview(w, h);
1008            }
1009        }
1010        keepScreenOnAwhile();
1011
1012        // Dismiss open menu if exists.
1013        PopupManager.getInstance(mActivity).notifyShowPopup(null);
1014        mRootView.requestLayout();
1015    }
1016
1017    /**
1018     * Generate the final mosaic image.
1019     *
1020     * @param highRes flag to indicate whether we want to get a high-res version.
1021     * @return a MosaicJpeg with its isValid flag set to true if successful; null if the generation
1022     *         process is cancelled; and a MosaicJpeg with its isValid flag set to false if there
1023     *         is an error in generating the final mosaic.
1024     */
1025    public MosaicJpeg generateFinalMosaic(boolean highRes) {
1026        int mosaicReturnCode = mMosaicFrameProcessor.createMosaic(highRes);
1027        if (mosaicReturnCode == Mosaic.MOSAIC_RET_CANCELLED) {
1028            return null;
1029        } else if (mosaicReturnCode == Mosaic.MOSAIC_RET_ERROR) {
1030            return new MosaicJpeg();
1031        }
1032
1033        byte[] imageData = mMosaicFrameProcessor.getFinalMosaicNV21();
1034        if (imageData == null) {
1035            Log.e(TAG, "getFinalMosaicNV21() returned null.");
1036            return new MosaicJpeg();
1037        }
1038
1039        int len = imageData.length - 8;
1040        int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16)
1041                + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF);
1042        int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16)
1043                + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF);
1044        Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height);
1045
1046        if (width <= 0 || height <= 0) {
1047            // TODO: pop up an error message indicating that the final result is not generated.
1048            Log.e(TAG, "width|height <= 0!!, len = " + (len) + ", W = " + width + ", H = " +
1049                    height);
1050            return new MosaicJpeg();
1051        }
1052
1053        YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null);
1054        ByteArrayOutputStream out = new ByteArrayOutputStream();
1055        yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
1056        try {
1057            out.close();
1058        } catch (Exception e) {
1059            Log.e(TAG, "Exception in storing final mosaic", e);
1060            return new MosaicJpeg();
1061        }
1062        return new MosaicJpeg(out.toByteArray(), width, height);
1063    }
1064
1065    private void startCameraPreview() {
1066        if (mCameraDevice == null) {
1067            // Camera open failed. Return.
1068            return;
1069        }
1070
1071        // This works around a driver issue. startPreview may fail if
1072        // stopPreview/setPreviewTexture/startPreview are called several times
1073        // in a row. mCameraTexture can be null after pressing home during
1074        // mosaic generation and coming back. Preview will be started later in
1075        // onLayoutChange->configMosaicPreview. This also reduces the latency.
1076        if (mCameraTexture == null) return;
1077
1078        // If we're previewing already, stop the preview first (this will blank
1079        // the screen).
1080        if (mCameraState != PREVIEW_STOPPED) stopCameraPreview();
1081
1082        // Set the display orientation to 0, so that the underlying mosaic library
1083        // can always get undistorted mPreviewWidth x mPreviewHeight image data
1084        // from SurfaceTexture.
1085        mCameraDevice.setDisplayOrientation(0);
1086
1087        if (mCameraTexture != null) mCameraTexture.setOnFrameAvailableListener(this);
1088        mCameraDevice.setPreviewTextureAsync(mCameraTexture);
1089
1090        mCameraDevice.startPreviewAsync();
1091        mCameraState = PREVIEW_ACTIVE;
1092    }
1093
1094    private void stopCameraPreview() {
1095        if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
1096            Log.v(TAG, "stopPreview");
1097            mCameraDevice.stopPreview();
1098        }
1099        mCameraState = PREVIEW_STOPPED;
1100    }
1101
1102    @Override
1103    public void onUserInteraction() {
1104        if (mCaptureState != CAPTURE_STATE_MOSAIC) keepScreenOnAwhile();
1105    }
1106
1107    @Override
1108    public boolean onBackPressed() {
1109        // If panorama is generating low res or high res mosaic, ignore back
1110        // key. So the activity will not be destroyed.
1111        if (mThreadRunning) return true;
1112        return false;
1113    }
1114
1115    private void resetScreenOn() {
1116        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1117        mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1118    }
1119
1120    private void keepScreenOnAwhile() {
1121        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1122        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1123        mMainHandler.sendEmptyMessageDelayed(MSG_CLEAR_SCREEN_DELAY, SCREEN_DELAY);
1124    }
1125
1126    private void keepScreenOn() {
1127        mMainHandler.removeMessages(MSG_CLEAR_SCREEN_DELAY);
1128        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1129    }
1130
1131    private class WaitProcessorTask extends AsyncTask<Void, Void, Void> {
1132        @Override
1133        protected Void doInBackground(Void... params) {
1134            synchronized (mMosaicFrameProcessor) {
1135                while (!isCancelled() && mMosaicFrameProcessor.isMosaicMemoryAllocated()) {
1136                    try {
1137                        mMosaicFrameProcessor.wait();
1138                    } catch (Exception e) {
1139                        // ignore
1140                    }
1141                }
1142            }
1143            return null;
1144        }
1145
1146        @Override
1147        protected void onPostExecute(Void result) {
1148            mWaitProcessorTask = null;
1149            mRotateDialog.dismissDialog();
1150            mGLRootView.setVisibility(View.VISIBLE);
1151            initMosaicFrameProcessorIfNeeded();
1152            int w = mPreviewArea.getWidth();
1153            int h = mPreviewArea.getHeight();
1154            if (w != 0 && h != 0) {  // The layout has been calculated.
1155                configMosaicPreview(w, h);
1156            }
1157            resetToPreview();
1158        }
1159    }
1160
1161    @Override
1162    public void onFullScreenChanged(boolean full) {
1163    }
1164
1165
1166    @Override
1167    public void onStop() {
1168    }
1169
1170    @Override
1171    public void installIntentFilter() {
1172    }
1173
1174    @Override
1175    public void onActivityResult(int requestCode, int resultCode, Intent data) {
1176    }
1177
1178
1179    @Override
1180    public boolean onKeyDown(int keyCode, KeyEvent event) {
1181        return false;
1182    }
1183
1184    @Override
1185    public boolean onKeyUp(int keyCode, KeyEvent event) {
1186        return false;
1187    }
1188
1189    @Override
1190    public void onSingleTapUp(View view, int x, int y) {
1191    }
1192
1193    @Override
1194    public void onPreviewTextureCopied() {
1195    }
1196
1197    @Override
1198    public void onCaptureTextureCopied() {
1199    }
1200
1201    @Override
1202    public boolean updateStorageHintOnResume() {
1203        return false;
1204    }
1205
1206    @Override
1207    public void updateCameraAppView() {
1208    }
1209
1210    @Override
1211    public boolean collapseCameraControls() {
1212        return false;
1213    }
1214
1215    @Override
1216    public boolean needsSwitcher() {
1217        return true;
1218    }
1219}
1220